Added time offset sync, added web version multiplayer

This commit is contained in:
Vladislav Khorev 2026-02-09 22:56:48 +03:00
parent a59bcc0c4b
commit 2c1c077611
13 changed files with 534 additions and 324 deletions

View File

@ -65,6 +65,10 @@ set(SOURCES
../src/network/ClientState.cpp
../src/network/WebSocketClient.h
../src/network/WebSocketClient.cpp
../src/network/WebSocketClientBase.h
../src/network/WebSocketClientBase.cpp
../src/network/WebSocketClientEmscripten.h
../src/network/WebSocketClientEmscripten.cpp
../src/render/TextRenderer.h
../src/render/TextRenderer.cpp
)
@ -90,7 +94,7 @@ add_subdirectory("../thirdparty/libzip-1.11.4" libzip-build)
# Линковка:
# 'zip' берется из add_subdirectory
# 'z' - это системный zlib Emscripten-а (флаг -sUSE_ZLIB=1 добавим ниже)
target_link_libraries(space-game001 PRIVATE zip z)
target_link_libraries(space-game001 PRIVATE zip z websocket)
# Эмскриптен-флаги
set(EMSCRIPTEN_FLAGS
@ -102,6 +106,7 @@ set(EMSCRIPTEN_FLAGS
"-pthread"
"-sUSE_PTHREADS=1"
"-fexceptions"
"-DNETWORK"
)
target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")

View File

@ -57,6 +57,10 @@ add_executable(space-game001
../src/network/ClientState.cpp
../src/network/WebSocketClient.h
../src/network/WebSocketClient.cpp
../src/network/WebSocketClientBase.h
../src/network/WebSocketClientBase.cpp
../src/network/WebSocketClientEmscripten.h
../src/network/WebSocketClientEmscripten.cpp
../src/render/TextRenderer.h
../src/render/TextRenderer.cpp
)

View File

@ -180,7 +180,9 @@ public:
timer->expires_after(std::chrono::milliseconds(100));
timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) {
if (!ec) {
self->send_message("ID:" + std::to_string(self->id_));
int64_t serverNow = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
self->send_message("ID:" + std::to_string(self->id_) + ":" + std::to_string(serverNow));
self->do_read();
}
});
@ -297,6 +299,7 @@ private:
}
void process_message(const std::string& msg) {
std::cout << "Received from player " << id_ << ": " << msg << std::endl;
auto parts = split(msg, ':');
if (parts.empty()) return;
@ -754,11 +757,11 @@ int main() {
std::cout << "Generated " << g_serverBoxes.size() << " boxes on server\n";
}
net::io_context ioc;
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} };
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8081} };
int next_id = 1000;
std::cout << "Server started on port 8080...\n";
std::cout << "Server started on port 8081...\n";
auto do_accept = [&](auto& self_fn) -> void {
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {

View File

@ -16,7 +16,11 @@
#endif
#ifdef NETWORK
#ifdef EMSCRIPTEN
#include "network/WebSocketClientEmscripten.h"
#else
#include "network/WebSocketClient.h"
#endif
#else
#include "network/LocalClient.h"
#endif
@ -389,7 +393,8 @@ namespace ZL
std::to_string(speedToSend) + ":" +
std::to_string(shotCount);
networkClient->Send(fireMsg);
//Temporary disable to avoid de-sync
//networkClient->Send(fireMsg);
}
});
@ -486,8 +491,13 @@ namespace ZL
planetObject.init();
#ifdef NETWORK
#ifdef EMSCRIPTEN
networkClient = std::make_unique<WebSocketClientEmscripten>();
networkClient->Connect("localhost", 8081);
#else
networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext());
networkClient->Connect("127.0.0.1", 8080);
networkClient->Connect("127.0.0.1", 8081);
#endif
#else
networkClient = std::make_unique<LocalClient>();
networkClient->Connect("", 0);
@ -870,13 +880,22 @@ namespace ZL
#endif
}
int64_t Game::getSyncTimeMs() {
int64_t localNow = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
std::cout << "getSyncTimeMs localNow = " << localNow << std::endl;
std::cout << "getSyncTimeMs getTimeOffset = " << networkClient->getTimeOffset() << std::endl;
// Добавляем смещение, полученное от сервера
return localNow + networkClient->getTimeOffset(); // Нужно добавить геттер в интерфейс
}
void Game::processTickCount() {
if (lastTickCount == 0) {
//lastTickCount = SDL_GetTicks64();
lastTickCount = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
lastTickCount = getSyncTimeMs();
lastTickCount = (lastTickCount / 50) * 50;
@ -884,15 +903,13 @@ namespace ZL
}
//newTickCount = SDL_GetTicks64();
newTickCount = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
newTickCount = getSyncTimeMs();
newTickCount = (newTickCount / 50) * 50;
if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) {
size_t delta = newTickCount - lastTickCount;
int64_t delta = newTickCount - lastTickCount;
if (delta > CONST_MAX_TIME_INTERVAL)
{
//throw std::runtime_error("Synchronization is lost");
@ -900,6 +917,8 @@ namespace ZL
auto now_ms = newTickCount;
std::cout << "processTickCount = " << now_ms << std::endl;
sparkEmitter.update(static_cast<float>(delta));
planetObject.update(static_cast<float>(delta));

View File

@ -44,6 +44,7 @@ namespace ZL {
std::unique_ptr<INetworkClient> networkClient;
private:
int64_t getSyncTimeMs();
void processTickCount();
void drawScene();
void drawCubemap(float skyPercent);
@ -66,8 +67,8 @@ namespace ZL {
size_t newTickCount;
size_t lastTickCount;
int64_t newTickCount;
int64_t lastTickCount;
std::vector<BoxCoords> boxCoordsArr;
std::vector<VertexRenderStruct> boxRenderArr;

View File

@ -47,5 +47,7 @@ namespace ZL {
virtual std::vector<int> getPendingRespawns() = 0;
virtual int GetClientId() const { return -1; }
virtual std::vector<BoxDestroyedInfo> getPendingBoxDestructions() = 0;
virtual int64_t getTimeOffset() const { return 0; }
};
}

View File

@ -4,17 +4,6 @@
#include <iostream>
#include <SDL2/SDL.h>
// Вспомогательный split
std::vector<std::string> split(const std::string& s, char delimiter) {
std::vector<std::string> tokens;
std::string token;
std::istringstream tokenStream(s);
while (std::getline(tokenStream, token, delimiter)) {
tokens.push_back(token);
}
return tokens;
}
namespace ZL {
void WebSocketClient::Connect(const std::string& host, uint16_t port) {
@ -55,43 +44,15 @@ namespace ZL {
void WebSocketClient::processIncomingMessage(const std::string& msg) {
// Логика парсинга...
if (msg.rfind("ID:", 0) == 0) {
/*if (msg.rfind("ID:", 0) == 0) {
clientId = std::stoi(msg.substr(3));
}
}*/
// Безопасно кладем в очередь для главного потока
std::lock_guard<std::mutex> lock(queueMutex);
messageQueue.push(msg);
}
std::vector<ProjectileInfo> WebSocketClient::getPendingProjectiles() {
std::lock_guard<std::mutex> lock(projMutex_);
auto copy = pendingProjectiles_;
pendingProjectiles_.clear();
return copy;
}
std::vector<DeathInfo> WebSocketClient::getPendingDeaths() {
std::lock_guard<std::mutex> lock(deathsMutex_);
auto copy = pendingDeaths_;
pendingDeaths_.clear();
return copy;
}
std::vector<int> WebSocketClient::getPendingRespawns() {
std::lock_guard<std::mutex> lock(respawnMutex_);
auto copy = pendingRespawns_;
pendingRespawns_.clear();
return copy;
}
std::vector<BoxDestroyedInfo> WebSocketClient::getPendingBoxDestructions() {
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
auto copy = pendingBoxDestructions_;
pendingBoxDestructions_.clear();
return copy;
}
void WebSocketClient::Poll() {
std::lock_guard<std::mutex> lock(queueMutex);
@ -105,224 +66,15 @@ namespace ZL {
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
nowTime.time_since_epoch()
).count();
std::string msg = messageQueue.front();
messageQueue.pop();
// Обработка списка коробок от сервера
if (msg.rfind("BOXES:", 0) == 0) {
std::string payload = msg.substr(6); // после "BOXES:"
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> parsedBoxes;
if (!payload.empty()) {
auto items = split(payload, '|');
for (auto& item : items) {
if (item.empty()) continue;
auto parts = split(item, ':');
if (parts.size() < 7) continue;
try {
float px = std::stof(parts[0]);
float py = std::stof(parts[1]);
float pz = std::stof(parts[2]);
Eigen::Quaternionf q(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5]),
std::stof(parts[6])
);
Eigen::Matrix3f rot = q.toRotationMatrix();
parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot);
}
catch (...) {
// пропускаем некорректную запись
continue;
}
}
}
{
std::lock_guard<std::mutex> bLock(boxesMutex);
serverBoxes_ = std::move(parsedBoxes);
}
continue;
}
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
auto parts = split(msg, ':');
if (parts.size() >= 2) {
try {
int respawnedPlayerId = std::stoi(parts[1]);
{
std::lock_guard<std::mutex> rLock(respawnMutex_);
pendingRespawns_.push_back(respawnedPlayerId);
}
{
std::lock_guard<std::mutex> pLock(playersMutex);
remotePlayers.erase(respawnedPlayerId);
}
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
}
catch (...) {}
}
continue;
}
if (msg.rfind("BOX_DESTROYED:", 0) == 0) {
auto parts = split(msg, ':');
if (parts.size() >= 7) {
try {
BoxDestroyedInfo destruction;
destruction.boxIndex = std::stoi(parts[1]);
destruction.serverTime = std::stoull(parts[2]);
destruction.position = Eigen::Vector3f(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5])
);
destruction.destroyedBy = std::stoi(parts[6]);
{
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
pendingBoxDestructions_.push_back(destruction);
}
std::cout << "Client: Received BOX_DESTROYED for box " << destruction.boxIndex
<< " destroyed by player " << destruction.destroyedBy << std::endl;
}
catch (const std::exception& e) {
std::cerr << "Client: Error parsing BOX_DESTROYED: " << e.what() << std::endl;
}
}
continue;
}
if (msg.rfind("PROJECTILE:", 0) == 0) {
auto parts = split(msg, ':');
if (parts.size() >= 10) {
try {
ProjectileInfo pi;
pi.shooterId = std::stoi(parts[1]);
pi.clientTime = std::stoull(parts[2]);
pi.position = Eigen::Vector3f(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5])
);
Eigen::Quaternionf q(
std::stof(parts[6]),
std::stof(parts[7]),
std::stof(parts[8]),
std::stof(parts[9])
);
pi.rotation = q.toRotationMatrix();
pi.velocity = std::stof(parts[10]);
std::lock_guard<std::mutex> pl(projMutex_);
pendingProjectiles_.push_back(pi);
}
catch (...) {}
}
continue;
}
if (msg.rfind("DEAD:", 0) == 0) {
auto parts = split(msg, ':');
if (parts.size() >= 7) {
try {
DeathInfo di;
di.serverTime = std::stoull(parts[1]);
di.targetId = std::stoi(parts[2]);
di.position = Eigen::Vector3f(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5])
);
di.killerId = std::stoi(parts[6]);
std::lock_guard<std::mutex> dl(deathsMutex_);
pendingDeaths_.push_back(di);
}
catch (...) {}
}
continue;
}
if (msg.rfind("EVENT:", 0) == 0) {
auto parts = split(msg, ':');
if (parts.size() < 5) continue; // EVENT:ID:TYPE:TIME:DATA...
int remoteId = std::stoi(parts[1]);
std::string subType = parts[2];
uint64_t sentTime = std::stoull(parts[3]);
ClientState remoteState;
remoteState.id = remoteId;
std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast<std::chrono::system_clock::time_point::duration>(std::chrono::milliseconds(sentTime)) };
remoteState.lastUpdateServerTime = uptime_timepoint;
if (subType == "UPD") {
int startFrom = 4;
remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) };
Eigen::Quaternionf q(
std::stof(parts[startFrom + 3]),
std::stof(parts[startFrom + 4]),
std::stof(parts[startFrom + 5]),
std::stof(parts[startFrom + 6]));
remoteState.rotation = q.toRotationMatrix();
remoteState.currentAngularVelocity = Eigen::Vector3f{
std::stof(parts[startFrom + 7]),
std::stof(parts[startFrom + 8]),
std::stof(parts[startFrom + 9]) };
remoteState.velocity = std::stof(parts[startFrom + 10]);
remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]);
remoteState.discreteMag = std::stof(parts[startFrom + 12]);
remoteState.discreteAngle = std::stoi(parts[startFrom + 13]);
}
else
{
throw std::runtime_error("Unknown EVENT subtype: " + subType);
}
{
std::lock_guard<std::mutex> pLock(playersMutex);
auto& rp = remotePlayers[remoteId];
rp.add_state(remoteState);
HandlePollMessage(msg);
}
}
if (msg.rfind("SNAPSHOT:", 0) == 0) {
auto mainParts = split(msg.substr(9), '|'); // Отсекаем "SNAPSHOT:" и делим по игрокам
if (mainParts.empty()) continue;
uint64_t serverTimestamp = std::stoull(mainParts[0]);
std::chrono::system_clock::time_point serverTime{ std::chrono::milliseconds(serverTimestamp) };
for (size_t i = 1; i < mainParts.size(); ++i) {
auto playerParts = split(mainParts[i], ':');
if (playerParts.size() < 15) continue; // ID + 14 полей ClientState
int rId = std::stoi(playerParts[0]);
if (rId == clientId) continue; // Свое состояние игрок знает лучше всех (Client Side Prediction)
ClientState remoteState;
remoteState.id = rId;
remoteState.lastUpdateServerTime = serverTime;
// Используем твой handle_full_sync, начиная со 2-го индекса (пропускаем ID в playerParts)
remoteState.handle_full_sync(playerParts, 1);
{
std::lock_guard<std::mutex> pLock(playersMutex);
remotePlayers[rId].add_state(remoteState);
}
}
continue;
}
}
}
void WebSocketClient::Send(const std::string& message) {
if (!connected) return;

View File

@ -2,7 +2,7 @@
#ifdef NETWORK
#include "NetworkInterface.h"
#include "WebSocketClientBase.h"
#include <queue>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
@ -11,7 +11,7 @@
namespace ZL {
class WebSocketClient : public INetworkClient {
class WebSocketClient : public WebSocketClientBase {
private:
// Переиспользуем io_context из TaskManager
boost::asio::io_context& ioc_;
@ -28,26 +28,7 @@ namespace ZL {
std::mutex writeMutex_; // Отдельный мьютекс для очереди записи
bool connected = false;
int clientId = -1;
std::unordered_map<int, ClientStateInterval> remotePlayers;
std::mutex playersMutex;
// Серверные коробки
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_;
std::mutex boxesMutex;
std::vector<ProjectileInfo> pendingProjectiles_;
std::mutex projMutex_;
std::vector<DeathInfo> pendingDeaths_;
std::mutex deathsMutex_;
std::vector<int> pendingRespawns_;
std::mutex respawnMutex_;
std::vector<BoxDestroyedInfo> pendingBoxDestructions_;
std::mutex boxDestructionsMutex_;
void startAsyncRead();
void processIncomingMessage(const std::string& msg);
@ -63,22 +44,6 @@ namespace ZL {
void doWrite();
bool IsConnected() const override { return connected; }
int GetClientId() const override { return clientId; }
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override {
std::lock_guard<std::mutex> lock(playersMutex);
return remotePlayers;
}
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override {
std::lock_guard<std::mutex> lock(boxesMutex);
return serverBoxes_;
}
std::vector<ProjectileInfo> getPendingProjectiles() override;
std::vector<DeathInfo> getPendingDeaths() override;
std::vector<int> getPendingRespawns() override;
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
};
}
#endif

View File

@ -0,0 +1,283 @@
#ifdef NETWORK
#include "WebSocketClientBase.h"
#include <iostream>
#include <SDL2/SDL.h>
// Вспомогательный split
std::vector<std::string> split(const std::string& s, char delimiter) {
std::vector<std::string> tokens;
std::string token;
std::istringstream tokenStream(s);
while (std::getline(tokenStream, token, delimiter)) {
tokens.push_back(token);
}
return tokens;
}
namespace ZL {
void WebSocketClientBase::HandlePollMessage(const std::string& msg) {
auto parts = split(msg, ':');
if (parts.empty()) return;
if (parts[0] == "ID") {
std::cout << "ID Message Received:" << msg << std::endl;
clientId = std::stoi(parts[1]);
if (parts.size() >= 3) {
std::cout << "ID Message Received step 2" << std::endl;
uint64_t serverTime = std::stoull(parts[2]);
uint64_t localTime = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
std::cout << "ID Message Received localTime = " << localTime << std::endl;
std::cout << "ID Message Received serverTime = " << serverTime << std::endl;
// Вычисляем смещение
timeOffset = static_cast<int64_t>(serverTime) - static_cast<int64_t>(localTime);
std::cout << "Time synchronized. Offset: " << timeOffset << " ms" << std::endl;
}
return;
}
// Обработка списка коробок от сервера
if (msg.rfind("BOXES:", 0) == 0) {
std::string payload = msg.substr(6); // после "BOXES:"
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> parsedBoxes;
if (!payload.empty()) {
auto items = split(payload, '|');
for (auto& item : items) {
if (item.empty()) return;
auto parts = split(item, ':');
if (parts.size() < 7) return;
try {
float px = std::stof(parts[0]);
float py = std::stof(parts[1]);
float pz = std::stof(parts[2]);
Eigen::Quaternionf q(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5]),
std::stof(parts[6])
);
Eigen::Matrix3f rot = q.toRotationMatrix();
parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot);
}
catch (...) {
// пропускаем некорректную запись
return;
}
}
}
{
std::lock_guard<std::mutex> bLock(boxesMutex);
serverBoxes_ = std::move(parsedBoxes);
}
return;
}
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
//auto parts = split(msg, ':');
if (parts.size() >= 2) {
try {
int respawnedPlayerId = std::stoi(parts[1]);
{
std::lock_guard<std::mutex> rLock(respawnMutex_);
pendingRespawns_.push_back(respawnedPlayerId);
}
{
std::lock_guard<std::mutex> pLock(playersMutex);
remotePlayers.erase(respawnedPlayerId);
}
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
}
catch (...) {}
}
return;
}
if (msg.rfind("BOX_DESTROYED:", 0) == 0) {
//auto parts = split(msg, ':');
if (parts.size() >= 7) {
try {
BoxDestroyedInfo destruction;
destruction.boxIndex = std::stoi(parts[1]);
destruction.serverTime = std::stoull(parts[2]);
destruction.position = Eigen::Vector3f(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5])
);
destruction.destroyedBy = std::stoi(parts[6]);
{
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
pendingBoxDestructions_.push_back(destruction);
}
std::cout << "Client: Received BOX_DESTROYED for box " << destruction.boxIndex
<< " destroyed by player " << destruction.destroyedBy << std::endl;
}
catch (const std::exception& e) {
std::cerr << "Client: Error parsing BOX_DESTROYED: " << e.what() << std::endl;
}
}
return;
}
if (msg.rfind("PROJECTILE:", 0) == 0) {
//auto parts = split(msg, ':');
if (parts.size() >= 10) {
try {
ProjectileInfo pi;
pi.shooterId = std::stoi(parts[1]);
pi.clientTime = std::stoull(parts[2]);
pi.position = Eigen::Vector3f(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5])
);
Eigen::Quaternionf q(
std::stof(parts[6]),
std::stof(parts[7]),
std::stof(parts[8]),
std::stof(parts[9])
);
pi.rotation = q.toRotationMatrix();
pi.velocity = std::stof(parts[10]);
std::lock_guard<std::mutex> pl(projMutex_);
pendingProjectiles_.push_back(pi);
}
catch (...) {}
}
return;
}
if (msg.rfind("DEAD:", 0) == 0) {
//auto parts = split(msg, ':');
if (parts.size() >= 7) {
try {
DeathInfo di;
di.serverTime = std::stoull(parts[1]);
di.targetId = std::stoi(parts[2]);
di.position = Eigen::Vector3f(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5])
);
di.killerId = std::stoi(parts[6]);
std::lock_guard<std::mutex> dl(deathsMutex_);
pendingDeaths_.push_back(di);
}
catch (...) {}
}
return;
}
if (msg.rfind("EVENT:", 0) == 0) {
//auto parts = split(msg, ':');
if (parts.size() < 5) return; // EVENT:ID:TYPE:TIME:DATA...
int remoteId = std::stoi(parts[1]);
std::string subType = parts[2];
uint64_t sentTime = std::stoull(parts[3]);
ClientState remoteState;
remoteState.id = remoteId;
std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast<std::chrono::system_clock::time_point::duration>(std::chrono::milliseconds(sentTime)) };
remoteState.lastUpdateServerTime = uptime_timepoint;
if (subType == "UPD") {
int startFrom = 4;
remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) };
Eigen::Quaternionf q(
std::stof(parts[startFrom + 3]),
std::stof(parts[startFrom + 4]),
std::stof(parts[startFrom + 5]),
std::stof(parts[startFrom + 6]));
remoteState.rotation = q.toRotationMatrix();
remoteState.currentAngularVelocity = Eigen::Vector3f{
std::stof(parts[startFrom + 7]),
std::stof(parts[startFrom + 8]),
std::stof(parts[startFrom + 9]) };
remoteState.velocity = std::stof(parts[startFrom + 10]);
remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]);
remoteState.discreteMag = std::stof(parts[startFrom + 12]);
remoteState.discreteAngle = std::stoi(parts[startFrom + 13]);
}
else
{
throw std::runtime_error("Unknown EVENT subtype: " + subType);
}
{
std::lock_guard<std::mutex> pLock(playersMutex);
auto& rp = remotePlayers[remoteId];
rp.add_state(remoteState);
}
}
if (msg.rfind("SNAPSHOT:", 0) == 0) {
auto mainParts = split(msg.substr(9), '|'); // Отсекаем "SNAPSHOT:" и делим по игрокам
if (mainParts.empty()) return;
uint64_t serverTimestamp = std::stoull(mainParts[0]);
std::chrono::system_clock::time_point serverTime{ std::chrono::milliseconds(serverTimestamp) };
for (size_t i = 1; i < mainParts.size(); ++i) {
auto playerParts = split(mainParts[i], ':');
if (playerParts.size() < 15) return; // ID + 14 полей ClientState
int rId = std::stoi(playerParts[0]);
if (rId == clientId) return; // Свое состояние игрок знает лучше всех (Client Side Prediction)
ClientState remoteState;
remoteState.id = rId;
remoteState.lastUpdateServerTime = serverTime;
// Используем твой handle_full_sync, начиная со 2-го индекса (пропускаем ID в playerParts)
remoteState.handle_full_sync(playerParts, 1);
{
std::lock_guard<std::mutex> pLock(playersMutex);
remotePlayers[rId].add_state(remoteState);
}
}
}
}
std::vector<ProjectileInfo> WebSocketClientBase::getPendingProjectiles() {
std::lock_guard<std::mutex> lock(projMutex_);
auto copy = pendingProjectiles_;
pendingProjectiles_.clear();
return copy;
}
std::vector<DeathInfo> WebSocketClientBase::getPendingDeaths() {
std::lock_guard<std::mutex> lock(deathsMutex_);
auto copy = pendingDeaths_;
pendingDeaths_.clear();
return copy;
}
std::vector<int> WebSocketClientBase::getPendingRespawns() {
std::lock_guard<std::mutex> lock(respawnMutex_);
auto copy = pendingRespawns_;
pendingRespawns_.clear();
return copy;
}
std::vector<BoxDestroyedInfo> WebSocketClientBase::getPendingBoxDestructions() {
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
auto copy = pendingBoxDestructions_;
pendingBoxDestructions_.clear();
return copy;
}
}
#endif

View File

@ -0,0 +1,58 @@
#pragma once
#include "NetworkInterface.h"
#include <queue>
#include <mutex>
namespace ZL {
class WebSocketClientBase : public INetworkClient {
protected:
std::unordered_map<int, ClientStateInterval> remotePlayers;
std::mutex playersMutex;
// Серверные коробки
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_;
std::mutex boxesMutex;
std::vector<ProjectileInfo> pendingProjectiles_;
std::mutex projMutex_;
std::vector<DeathInfo> pendingDeaths_;
std::mutex deathsMutex_;
std::vector<int> pendingRespawns_;
std::mutex respawnMutex_;
std::vector<BoxDestroyedInfo> pendingBoxDestructions_;
std::mutex boxDestructionsMutex_;
int clientId = -1;
int64_t timeOffset = 0;
public:
int GetClientId() const override { return clientId; }
int64_t getTimeOffset() const override { return timeOffset; }
void HandlePollMessage(const std::string& msg);
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override {
std::lock_guard<std::mutex> lock(playersMutex);
return remotePlayers;
}
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override {
std::lock_guard<std::mutex> lock(boxesMutex);
return serverBoxes_;
}
std::vector<ProjectileInfo> getPendingProjectiles() override;
std::vector<DeathInfo> getPendingDeaths() override;
std::vector<int> getPendingRespawns() override;
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
};
}

View File

@ -0,0 +1,91 @@
#ifdef NETWORK
#ifdef EMSCRIPTEN
#include "WebSocketClientEmscripten.h"
#include <iostream>
#include <SDL2/SDL.h>
namespace ZL {
void WebSocketClientEmscripten::Connect(const std::string& host, uint16_t port) {
// Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost
std::string url = "ws://" + host + ":" + std::to_string(port);
EmscriptenWebSocketCreateAttributes attr = {
url.c_str(),
nullptr,
EM_TRUE // create_on_main_thread
};
socket_ = emscripten_websocket_new(&attr);
emscripten_websocket_set_onopen_callback(socket_, this, onOpen);
emscripten_websocket_set_onmessage_callback(socket_, this, onMessage);
emscripten_websocket_set_onerror_callback(socket_, this, onError);
emscripten_websocket_set_onclose_callback(socket_, this, onClose);
}
void WebSocketClientEmscripten::Send(const std::string& message) {
if (connected && socket_ > 0) {
emscripten_websocket_send_utf8_text(socket_, message.c_str());
}
}
void WebSocketClientEmscripten::Poll() {
// Локальная очередь для минимизации времени блокировки мьютекса
std::queue<std::string> localQueue;
{
std::lock_guard<std::mutex> lock(queueMutex);
if (messageQueue.empty()) return;
std::swap(localQueue, messageQueue);
}
while (!localQueue.empty()) {
const std::string& msg = localQueue.front();
std::cout << "[WebWS] Processing message: " << msg << std::endl;
// Передаем в базовый класс для парсинга игровых событий (BOXES, UPD, и т.д.)
HandlePollMessage(msg);
localQueue.pop();
}
}
// --- Колбэки ---
EM_BOOL WebSocketClientEmscripten::onOpen(int eventType, const EmscriptenWebSocketOpenEvent* e, void* userData) {
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
self->connected = true;
std::cout << "[WebWS] Connection opened" << std::endl;
return EM_TRUE;
}
EM_BOOL WebSocketClientEmscripten::onMessage(int eventType, const EmscriptenWebSocketMessageEvent* e, void* userData) {
std::cout << "[WebWS] onMessage " << std::endl;
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
if (e->isText && e->data) {
std::string msg(reinterpret_cast<const char*>(e->data), e->numBytes);
std::lock_guard<std::mutex> lock(self->queueMutex);
self->messageQueue.push(msg);
}
return EM_TRUE;
}
EM_BOOL WebSocketClientEmscripten::onError(int eventType, const EmscriptenWebSocketErrorEvent* e, void* userData) {
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
self->connected = false;
std::cerr << "[WebWS] Error detected" << std::endl;
return EM_TRUE;
}
EM_BOOL WebSocketClientEmscripten::onClose(int eventType, const EmscriptenWebSocketCloseEvent* e, void* userData) {
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
self->connected = false;
std::cout << "[WebWS] Connection closed" << std::endl;
return EM_TRUE;
}
}
#endif
#endif

View File

@ -0,0 +1,42 @@
#pragma once
#ifdef NETWORK
#ifdef EMSCRIPTEN
#include <emscripten/websocket.h>
#include "WebSocketClientBase.h"
#include <queue>
#include <mutex>
#include <string>
namespace ZL {
class WebSocketClientEmscripten : public WebSocketClientBase {
private:
EMSCRIPTEN_WEBSOCKET_T socket_ = 0;
bool connected = false;
// Очередь для хранения сырых строк от браузера
std::queue<std::string> messageQueue;
std::mutex queueMutex;
public:
WebSocketClientEmscripten() = default;
virtual ~WebSocketClientEmscripten() = default;
void Connect(const std::string& host, uint16_t port) override;
void Send(const std::string& message) override;
void Poll() override;
bool IsConnected() const override { return connected; }
// Статические методы-переходники для C-API Emscripten
static EM_BOOL onOpen(int eventType, const EmscriptenWebSocketOpenEvent* e, void* userData);
static EM_BOOL onMessage(int eventType, const EmscriptenWebSocketMessageEvent* e, void* userData);
static EM_BOOL onError(int eventType, const EmscriptenWebSocketErrorEvent* e, void* userData);
static EM_BOOL onClose(int eventType, const EmscriptenWebSocketCloseEvent* e, void* userData);
};
}
#endif
#endif

View File

@ -11,21 +11,14 @@ namespace ZL {
TextRenderer::~TextRenderer()
{
/*for (auto& kv : glyphs) {
if (kv.second.texID) glDeleteTextures(1, &kv.second.texID);
}*/
glyphs.clear();
textMesh.positionVBO.reset();
/*
if (vbo) glDeleteBuffers(1, &vbo);
// if (vao) glDeleteVertexArrays(1, &vao);
vao = 0;
vbo = 0;*/
}
bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename)
{
r = &renderer;
#ifdef EMSCRIPTEN
@ -47,15 +40,6 @@ bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixe
textMesh.data.PositionData.resize(6, Eigen::Vector3f(0, 0, 0));
textMesh.RefreshVBO();
// VAO/VBO для 6 вершин (2 треугольника) * (pos.xy + uv.xy) = 4 float
// glGenVertexArrays(1, &vao);
/* glGenBuffers(1, &vbo);
//glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, nullptr, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
*/
ZL::CheckGlError();
return true;
}
@ -142,6 +126,7 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color)
{
if (!r || text.empty()) return;
// 1. Считаем ширину для центрирования