Working on server

This commit is contained in:
Vladislav Khorev 2026-01-15 21:54:37 +03:00
parent ea26d6fb23
commit 1348efb5fe
13 changed files with 761 additions and 193 deletions

View File

@ -53,6 +53,8 @@ add_executable(space-game001
../src/network/NetworkInterface.h ../src/network/NetworkInterface.h
../src/network/LocalClient.h ../src/network/LocalClient.h
../src/network/LocalClient.cpp ../src/network/LocalClient.cpp
../src/network/ClientState.h
../src/network/RemotePlayer.h
../src/network/WebSocketClient.h ../src/network/WebSocketClient.h
../src/network/WebSocketClient.cpp ../src/network/WebSocketClient.cpp
) )

View File

@ -25,7 +25,10 @@ add_subdirectory("${BOOST_SRC_DIR}/libs/predef" boost-predef-build EXCLUDE_FROM_
# EXCLUDE_FROM_ALL гарантирует, что мы собираем только то, что линкуем # EXCLUDE_FROM_ALL гарантирует, что мы собираем только то, что линкуем
# Исполняемый файл сервера # Исполняемый файл сервера
add_executable(Server main.cpp) add_executable(Server
server.cpp
../src/network/ClientState.h
)
target_include_directories(Server PRIVATE ${BOOST_SRC_DIR}) target_include_directories(Server PRIVATE ${BOOST_SRC_DIR})

View File

@ -1,81 +0,0 @@
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <mutex>
namespace beast = boost::beast;
namespace http = beast::http;
namespace websocket = beast::websocket;
namespace net = boost::asio;
using tcp = net::ip::tcp;
class Session : public std::enable_shared_from_this<Session> {
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
int id_;
public:
explicit Session(tcp::socket&& socket, int id)
: ws_(std::move(socket)), id_(id) {
}
void run() {
ws_.async_accept([self = shared_from_this()](beast::error_code ec) {
if (ec) return;
std::cout << "Client " << self->id_ << " connected\n";
// Ñðàçó îòïðàâëÿåì ID êëèåíòó
self->send_message("ID:" + std::to_string(self->id_));
self->do_read();
});
}
private:
void do_read() {
ws_.async_read(buffer_, [self = shared_from_this()](beast::error_code ec, std::size_t) {
if (ec) return;
std::string msg = beast::buffers_to_string(self->buffer_.data());
if (msg == "PING") {
self->send_message("PONG");
}
self->buffer_.consume(self->buffer_.size());
self->do_read();
});
}
void send_message(std::string msg) {
auto ss = std::make_shared<std::string>(std::move(msg));
ws_.async_write(net::buffer(*ss), [ss](beast::error_code, std::size_t) {});
}
};
int main() {
try {
net::io_context ioc;
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} };
int next_id = 1000;
std::cout << "Server started on port 8080...\n";
auto do_accept = [&](auto& self_fn) -> void {
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<Session>(std::move(socket), next_id++)->run();
}
self_fn(self_fn);
});
};
do_accept(do_accept);
ioc.run();
}
catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}

237
server/server.cpp Normal file
View File

@ -0,0 +1,237 @@
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <mutex>
#include <Eigen/Dense>
#define _USE_MATH_DEFINES
#include <math.h>
#include "../src/network/ClientState.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 beast = boost::beast;
namespace http = beast::http;
namespace websocket = beast::websocket;
namespace net = boost::asio;
using tcp = net::ip::tcp;
class Session;
std::vector<std::shared_ptr<Session>> g_sessions;
std::mutex g_sessions_mutex;
class Session : public std::enable_shared_from_this<Session> {
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
int id_;
ClientState state_;
void process_message(const std::string& msg) {
auto now_server = std::chrono::steady_clock::now();
// Ïðåäïîëîæèì ôîðìàò ñîîáùåíèé:
// ROT:ANGLE:MAG:TIMESTAMP
// VEL:SELECTED_VEL:TIMESTAMP
auto parts = split(msg, ':');
if (parts.empty()) return;
std::cout << msg << std::endl;
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now_server.time_since_epoch()
).count();
uint64_t clientTimestamp = std::stoull(parts[1]);
// Ñíà÷àëà ïðîãîíÿåì ñòàíäàðòíóþ ñèìóëÿöèþ "äî òåêóùåãî ìîìåíòà ñåðâåðà"
float dt_server = 0.0f;
if (state_.lastUpdateServerTime.time_since_epoch().count() > 0) {
dt_server = (clientTimestamp - now_ms) * 1000.f;
}
if (dt_server > 0) state_.simulate_physics(dt_server);
using time_point = std::chrono::steady_clock::time_point;
std::chrono::steady_clock::time_point uptime_timepoint{ std::chrono::duration_cast<std::chrono::steady_clock::time_point::duration>(std::chrono::milliseconds(clientTimestamp)) };
state_.lastUpdateServerTime = uptime_timepoint;
// Òåïåðü îáðàáàòûâàåì ñïåöèôèêó ñîîáùåíèÿ è ëàã
if (parts[0] == "ROT") {
state_.discreteAngle = std::stoi(parts[2]);
state_.discreteMag = std::stof(parts[3]);
state_.apply_lag_compensation(now_server);
}
else if (parts[0] == "VEL") {
state_.selectedVelocity = std::stoi(parts[2]);
state_.apply_lag_compensation(now_server);
}
else if (parts[0] == "PING") {
state_.handle_full_sync(parts);
state_.apply_lag_compensation(now_server);
}
}
public:
explicit Session(tcp::socket&& socket, int id)
: ws_(std::move(socket)), id_(id) {
}
void init()
{
state_.lastUpdateServerTime = std::chrono::steady_clock::now();
}
std::string get_state_string() {
return state_.get_state_string(id_);
}
void run() {
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
g_sessions.push_back(shared_from_this());
}
ws_.async_accept([self = shared_from_this()](beast::error_code ec) {
if (ec) return;
std::cout << "Client " << self->id_ << " connected\n";
self->init();
self->send_message("ID:" + std::to_string(self->id_));
self->do_read();
});
}
void tick_physics_global(std::chrono::steady_clock::time_point now) {
float dt = 0.0f;
// Åñëè ýòî ñàìûé ïåðâûé òèê, ïðîñòî çàïîìèíàåì âðåìÿ
if (state_.lastUpdateServerTime.time_since_epoch().count() > 0) {
dt = std::chrono::duration<float>(now - state_.lastUpdateServerTime).count();
}
if (dt > 0.0001f) {
state_.simulate_physics(dt);
state_.lastUpdateServerTime = now; // Îáíîâëÿåì âðåìÿ ïîñëå ñèìóëÿöèè
}
}
void tick_physics(float dt_s) {
state_.simulate_physics(dt_s);
}
void send_message(std::string msg) {
auto ss = std::make_shared<std::string>(std::move(msg));
ws_.async_write(net::buffer(*ss), [ss](beast::error_code, std::size_t) {});
}
private:
void do_read() {
ws_.async_read(buffer_, [self = shared_from_this()](beast::error_code ec, std::size_t) {
if (ec)
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
g_sessions.erase(std::remove_if(g_sessions.begin(), g_sessions.end(),
[self](const std::shared_ptr<Session>& session) {
return session.get() == self.get();
}), g_sessions.end());
return;
}
std::string msg = beast::buffers_to_string(self->buffer_.data());
self->process_message(msg);
self->buffer_.consume(self->buffer_.size());
self->do_read();
});
}
};
void update_world(net::steady_timer& timer, net::io_context& ioc) {
auto now = std::chrono::steady_clock::now();
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
// 1. Ñèìóëÿöèÿ ôèçèêè äëÿ âñåõ
for (auto& session : g_sessions) {
session->tick_physics_global(now);
}
static auto last_broadcast = now;
if (std::chrono::duration<float>(now - last_broadcast).count() >= 1.0f) {
last_broadcast = now;
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()
).count();
// Ñîáèðàåì äàííûå âñåõ èãðîêîâ â îäèí ïàêåò
std::string snapshot = "WORLD_UPDATE|";
snapshot += std::to_string(now_ms) + "|";
snapshot += std::to_string(g_sessions.size()) + "|";
for (size_t i = 0; i < g_sessions.size(); ++i) {
snapshot += g_sessions[i]->get_state_string();
if (i < g_sessions.size() - 1) snapshot += ";"; // Ðàçäåëèòåëü ìåæäó èãðîêàìè
}
// Ðàññûëàåì âñåì
for (auto& session : g_sessions) {
session->send_message(snapshot);
}
}
}
// ÂÀÆÍÎ: Òèêàåì ÷àñòî (50ìñ), à øëåì äàííûå ðåäêî (1000ìñ âûøå)
timer.expires_after(std::chrono::milliseconds(50));
timer.async_wait([&](const boost::system::error_code& ec) {
if (!ec) update_world(timer, ioc);
});
}
int main() {
try {
net::io_context ioc;
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} };
int next_id = 1000;
std::cout << "Server started on port 8080...\n";
auto do_accept = [&](auto& self_fn) -> void {
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<Session>(std::move(socket), next_id++)->run();
}
self_fn(self_fn);
});
};
net::steady_timer timer(ioc);
update_world(timer, ioc);
do_accept(do_accept);
ioc.run();
}
catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}

View File

@ -40,8 +40,13 @@ Eigen::Vector2f Environment::tapDownCurrentPos = { 0, 0 };
Eigen::Vector3f Environment::shipPosition = {0,0,45000.f}; Eigen::Vector3f Environment::shipPosition = {0,0,45000.f};
float Environment::shipVelocity = 0.f; float Environment::shipVelocity = 0.f;
int Environment::shipSelectedVelocity = 0;
Eigen::Vector3f Environment::currentAngularVelocity;
int Environment::lastSentAngle = -1;
float Environment::lastSentMagnitude = 0.0f;
const float Environment::CONST_Z_NEAR = 5.f; const float Environment::CONST_Z_NEAR = 5.f;
const float Environment::CONST_Z_FAR = 5000.f; const float Environment::CONST_Z_FAR = 5000.f;

View File

@ -38,6 +38,11 @@ public:
static Eigen::Vector3f shipPosition; static Eigen::Vector3f shipPosition;
static float shipVelocity; static float shipVelocity;
static int shipSelectedVelocity;
static Eigen::Vector3f currentAngularVelocity;
static int lastSentAngle;
static float lastSentMagnitude;
static const float CONST_Z_NEAR; static const float CONST_Z_NEAR;
static const float CONST_Z_FAR; static const float CONST_Z_FAR;

View File

@ -19,6 +19,8 @@
#include "network/LocalClient.h" #include "network/LocalClient.h"
#endif #endif
#include "network/RemotePlayer.h"
namespace ZL namespace ZL
{ {
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
@ -139,33 +141,6 @@ namespace ZL
ZL::BindOpenGlFunctions(); ZL::BindOpenGlFunctions();
ZL::CheckGlError(); ZL::CheckGlError();
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "Game", "Start for Android");
const char* testFiles[] = {
"resources/shaders/default.vertex",
"resources/shaders/default_web.fragment",
nullptr
};
for (int i = 0; testFiles[i] != nullptr; i++) {
SDL_RWops* file = SDL_RWFromFile(testFiles[i], "rb");
if (file) {
Sint64 size = SDL_RWsize(file);
__android_log_print(ANDROID_LOG_INFO, "Game", "Found: %s (size: %lld)", testFiles[i], size);
SDL_RWclose(file);
}
else {
__android_log_print(ANDROID_LOG_WARN, "Game", "Not found: %s (SDL error: %s)",
testFiles[i], SDL_GetError());
}
}
__android_log_print(ANDROID_LOG_ERROR, "ShaderManager",
"Step 1xxxxxxx");
#endif
// Initialize renderer
#ifndef SIMPLIFIED #ifndef SIMPLIFIED
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE);
@ -178,37 +153,11 @@ namespace ZL
#else #else
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_ERROR, "ShaderManager",
"Step 2xxxxxxx");
#endif
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/default_env.vertex", "resources/shaders/default_env_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/default_env.vertex", "resources/shaders/default_env_web.fragment", CONST_ZIP_FILE);
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
"Step 2");
#endif
renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
"Step 3");
#endif
renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
"Step 4");
#endif
renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
"Step 5");
#endif
renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "ShaderManager",
"Step 1");
#endif
#endif #endif
bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE); bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE);
@ -289,16 +238,17 @@ namespace ZL
}); });
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) { uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
Environment::shipVelocity = value * 1000.f; int newVel = roundf(value * 10);
if (newVel != Environment::shipSelectedVelocity) {
Environment::shipSelectedVelocity = newVel;
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()
).count();
std::string msg = "VEL:" + std::to_string(now_ms) + ":" + std::to_string(Environment::shipSelectedVelocity);
networkClient->Send(msg);
}
}); });
/*uiManager.setSliderCallback("musicVolumeSlider", [this](const std::string& name, float value) {
std::cerr << "Music volume slider changed to: " << value << std::endl;
musicVolume = value;
Environment::shipVelocity = musicVolume * 20.0f;
});
//#endif*/
cubemapTexture = std::make_shared<Texture>( cubemapTexture = std::make_shared<Texture>(
std::array<TextureDataStruct, 6>{ std::array<TextureDataStruct, 6>{
CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE), CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
@ -330,10 +280,6 @@ namespace ZL
spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix()); spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix());
spaceshipBase.Move(Vector3f{ 1.2, 0, -5 }); spaceshipBase.Move(Vector3f{ 1.2, 0, -5 });
//spaceshipBase.Move(Vector3f{ -0.52998, -13, 0 });
//spaceshipBase.Move(Vector3f{ -0.52998, -10, 10 });
//spaceshipBase.Move(Vector3f{ -0.52998, 0, 10 });
spaceship.AssignFrom(spaceshipBase); spaceship.AssignFrom(spaceshipBase);
spaceship.RefreshVBO(); spaceship.RefreshVBO();
@ -343,16 +289,8 @@ namespace ZL
boxTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE)); boxTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE));
boxBase = LoadFromTextFile02("resources/box/box.txt", CONST_ZIP_FILE); boxBase = LoadFromTextFile02("resources/box/box.txt", CONST_ZIP_FILE);
std::cout << "Init step 1 " << std::endl;
boxCoordsArr = generateRandomBoxCoords(50); boxCoordsArr = generateRandomBoxCoords(50);
std::cout << "Init step 2 " << std::endl;
boxRenderArr.resize(boxCoordsArr.size()); boxRenderArr.resize(boxCoordsArr.size());
std::cout << "Init step 3x " << std::endl;
for (int i = 0; i < boxCoordsArr.size(); i++) for (int i = 0; i < boxCoordsArr.size(); i++)
{ {
boxRenderArr[i].AssignFrom(boxBase); boxRenderArr[i].AssignFrom(boxBase);
@ -362,26 +300,17 @@ namespace ZL
boxAlive.resize(boxCoordsArr.size(), true); boxAlive.resize(boxCoordsArr.size(), true);
std::cout << "Init step 4 " << std::endl;
if (!cfgLoaded) if (!cfgLoaded)
{ {
throw std::runtime_error("Failed to load spark emitter config file!"); throw std::runtime_error("Failed to load spark emitter config file!");
} }
std::cout << "Init step 5 " << std::endl;
renderer.InitOpenGL(); renderer.InitOpenGL();
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
std::cout << "Init step 6 " << std::endl;
planetObject.init(); planetObject.init();
std::cout << "Init step 7 " << std::endl;
//rockTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/rock.png", CONST_ZIP_FILE));
std::cout << "Init step 8 " << std::endl;
#ifdef NETWORK #ifdef NETWORK
networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext()); networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext());
networkClient->Connect("127.0.0.1", 8080); networkClient->Connect("127.0.0.1", 8080);
@ -639,14 +568,182 @@ namespace ZL
sparkEmitter.update(static_cast<float>(delta)); sparkEmitter.update(static_cast<float>(delta));
planetObject.update(static_cast<float>(delta)); planetObject.update(static_cast<float>(delta));
updateRemotePlayers(static_cast<float>(delta));
static float pingTimer = 0.0f; static float pingTimer = 0.0f;
pingTimer += delta; pingTimer += delta;
if (pingTimer >= 1000.0f) { if (pingTimer >= 1000.0f) {
networkClient->Send("PING");
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()
).count();
// 1. Извлекаем кватернион из матрицы поворота
Eigen::Quaternionf q(Environment::shipMatrix);
// 2. Формируем строку PING согласно протоколу сервера
// Формат: PING:timestamp:posX:posY:posZ:qW:qX:qY:qZ:angVelX:angVelY:angVelZ:vel:selectedVel:discMag:discAngle
std::string pingMsg = "PING:" + std::to_string(now_ms) + ":"
+ std::to_string(Environment::shipPosition.x()) + ":"
+ std::to_string(Environment::shipPosition.y()) + ":"
+ std::to_string(Environment::shipPosition.z()) + ":"
+ std::to_string(q.w()) + ":"
+ std::to_string(q.x()) + ":"
+ std::to_string(q.y()) + ":"
+ std::to_string(q.z()) + ":"
+ std::to_string(Environment::currentAngularVelocity.x()) + ":"
+ std::to_string(Environment::currentAngularVelocity.y()) + ":"
+ std::to_string(Environment::currentAngularVelocity.z()) + ":"
+ std::to_string(Environment::shipVelocity) + ":"
+ std::to_string(Environment::shipSelectedVelocity) + ":"
+ std::to_string(Environment::lastSentMagnitude) + ":" // Используем те же static переменные из блока ROT
+ std::to_string(Environment::lastSentAngle);
networkClient->Send(pingMsg);
pingTimer = 0.0f; pingTimer = 0.0f;
} }
static const float CONST_ACCELERATION = 1.f;
float shipDesiredVelocity = Environment::shipSelectedVelocity * 100.f;
if (!gameOver)
{
if (Environment::shipVelocity < shipDesiredVelocity)
{
Environment::shipVelocity += delta * CONST_ACCELERATION;
if (Environment::shipVelocity > shipDesiredVelocity)
{
Environment::shipVelocity = shipDesiredVelocity;
}
}
else if (Environment::shipVelocity > shipDesiredVelocity)
{
Environment::shipVelocity -= delta * CONST_ACCELERATION;
if (Environment::shipVelocity < shipDesiredVelocity)
{
Environment::shipVelocity = shipDesiredVelocity;
}
}
}
static const float ANGULAR_ACCEL = 0.005f;
if (Environment::tapDownHold) {
float diffx = Environment::tapDownCurrentPos(0) - Environment::tapDownStartPos(0);
float diffy = Environment::tapDownCurrentPos(1) - Environment::tapDownStartPos(1);
float rawMag = sqrtf(diffx * diffx + diffy * diffy);
float maxRadius = 200.0f; // Максимальный вынос джойстика
if (rawMag > 10.0f) { // Мертвая зона
// 1. Дискретизируем отклонение (0.0 - 1.0 с шагом 0.1)
float normalizedMag = min(rawMag / maxRadius, 1.0f);
float discreteMag = std::round(normalizedMag * 10.0f) / 10.0f;
// 2. Дискретизируем угол (0-359 градусов)
// atan2 возвращает радианы, переводим в градусы
float radians = atan2f(diffy, diffx);
int discreteAngle = static_cast<int>(radians * 180.0f / M_PI);
if (discreteAngle < 0) discreteAngle += 360;
// 3. Проверяем, изменились ли параметры значимо для отправки на сервер
if (discreteAngle != Environment::lastSentAngle || discreteMag != Environment::lastSentMagnitude) {
Environment::lastSentAngle = discreteAngle;
Environment::lastSentMagnitude = discreteMag;
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()
).count();
// Формируем сетевой пакет
// Нам нужно отправить: дискретный угол, дискретную силу и текущую матрицу/позицию для синхронизации
std::string msg = "ROT:" + std::to_string(now_ms) + ":" + std::to_string(discreteAngle) + ":" + std::to_string(discreteMag);
networkClient->Send(msg);
}
// 4. Логика вращения (угловое ускорение)
// Теперь используем discreteAngle и discreteMag как "цель" для набора угловой скорости
// ... (код интерполяции скорости к этой цели)
// Вычисляем целевой вектор оси вращения из дискретного угла
// В 3D пространстве экранные X и Y преобразуются в оси вращения вокруг Y и X соответственно
float rad = static_cast<float>(discreteAngle) * static_cast<float>(M_PI) / 180.0f;
// Целевая угловая скорость (дискретная сила определяет модуль вектора)
// Вектор {cos, sin, 0} дает нам направление отклонения джойстика
Eigen::Vector3f targetAngularVelDir(sinf(rad), cosf(rad), 0.0f);
Eigen::Vector3f targetAngularVelocity = targetAngularVelDir * discreteMag;
Eigen::Vector3f diffVel = targetAngularVelocity - Environment::currentAngularVelocity;
float diffLen = diffVel.norm();
if (diffLen > 0.0001f) {
// Вычисляем, на сколько мы можем изменить скорость в этом кадре
float maxChange = ANGULAR_ACCEL * static_cast<float>(delta);
if (diffLen <= maxChange) {
// Если до цели осталось меньше, чем шаг ускорения — просто прыгаем в цель
Environment::currentAngularVelocity = targetAngularVelocity;
}
else {
// Линейно двигаемся в сторону целевого вектора
Environment::currentAngularVelocity += (diffVel / diffLen) * maxChange;
}
}
// Применяем вращение к матрице корабля
float speedScale = Environment::currentAngularVelocity.norm();
if (speedScale > 0.0001f) {
// Коэффициент чувствительности вращения
const float ROTATION_SENSITIVITY = 0.002f;
float deltaAlpha = speedScale * static_cast<float>(delta) * ROTATION_SENSITIVITY;
Eigen::Vector3f axis = Environment::currentAngularVelocity.normalized();
Eigen::Quaternionf rotateQuat(Eigen::AngleAxisf(deltaAlpha, axis));
Environment::shipMatrix = Environment::shipMatrix * rotateQuat.toRotationMatrix();
Environment::inverseShipMatrix = Environment::shipMatrix.inverse();
}
}
}
else {
// Если джойстик не зажат — сбрасываем дискретные значения и плавно замедляем вращение
Environment::lastSentAngle = -1;
Environment::lastSentMagnitude = 0.0f;
float currentSpeed = Environment::currentAngularVelocity.norm();
if (currentSpeed > 0.0001f) {
float drop = ANGULAR_ACCEL * static_cast<float>(delta);
if (currentSpeed <= drop) {
Environment::currentAngularVelocity = Eigen::Vector3f::Zero();
}
else {
// Уменьшаем модуль вектора, сохраняя направление
Environment::currentAngularVelocity -= (Environment::currentAngularVelocity / currentSpeed) * drop;
}
}
// Применяем остаточное вращение (инерция)
float speedScale = Environment::currentAngularVelocity.norm();
if (speedScale > 0.0001f) {
float deltaAlpha = speedScale * static_cast<float>(delta) * 0.002f;
Eigen::Quaternionf rotateQuat(Eigen::AngleAxisf(deltaAlpha, Environment::currentAngularVelocity.normalized()));
Environment::shipMatrix = Environment::shipMatrix * rotateQuat.toRotationMatrix();
Environment::inverseShipMatrix = Environment::shipMatrix.inverse();
}
}
// Движение вперед (существующая логика)
if (fabs(Environment::shipVelocity) > 0.01f)
{
Vector3f velocityDirection = { 0,0, -Environment::shipVelocity * delta / 1000.f };
Vector3f velocityDirectionAdjusted = Environment::shipMatrix * velocityDirection;
Environment::shipPosition = Environment::shipPosition + velocityDirectionAdjusted;
}
/*
if (Environment::tapDownHold) { if (Environment::tapDownHold) {
float diffx = Environment::tapDownCurrentPos(0) - Environment::tapDownStartPos(0); float diffx = Environment::tapDownCurrentPos(0) - Environment::tapDownStartPos(0);
@ -671,14 +768,7 @@ namespace ZL
Environment::inverseShipMatrix = Environment::shipMatrix.inverse(); Environment::inverseShipMatrix = Environment::shipMatrix.inverse();
} }
} }*/
if (fabs(Environment::shipVelocity) > 0.01f)
{
Vector3f velocityDirection = { 0,0, -Environment::shipVelocity * delta / 1000.f };
Vector3f velocityDirectionAdjusted = Environment::shipMatrix * velocityDirection;
Environment::shipPosition = Environment::shipPosition + velocityDirectionAdjusted;
}
for (auto& p : projectiles) { for (auto& p : projectiles) {
if (p && p->isActive()) { if (p && p->isActive()) {
@ -896,21 +986,7 @@ namespace ZL
{ {
if (event.key.keysym.sym == SDLK_i) if (event.key.keysym.sym == SDLK_i)
{ {
Environment::shipVelocity += 500.f;
}
if (event.key.keysym.sym == SDLK_k)
{
Environment::shipVelocity -= 500.f;
}
if (event.key.keysym.sym == SDLK_o)
{
Environment::shipVelocity += 50.f;
//x = x + 2.0;
}
if (event.key.keysym.sym == SDLK_l)
{
Environment::shipVelocity -= 50.f;
//x = x - 2.0;
} }
}*/ }*/
#endif #endif
@ -976,5 +1052,24 @@ namespace ZL
} }
} }
void Game::updateRemotePlayers(float deltaMs) {
latestRemotePlayers = networkClient->getRemotePlayers();
for (auto& [id, rp] : latestRemotePlayers) {
// Увеличиваем фактор интерполяции (базируется на частоте Snapshot = 1000мс)
rp.interpolationFactor += deltaMs / 1000.0f;
if (rp.interpolationFactor > 1.0f) rp.interpolationFactor = 1.0f;
// Линейная интерполяция позиции
rp.state.position = rp.startPosition + (rp.targetPosition - rp.startPosition) * rp.interpolationFactor;
// Сферическая интерполяция вращения (Slerp)
Eigen::Quaternionf currentQ = rp.startRotation.slerp(rp.interpolationFactor, rp.targetRotation);
rp.state.rotation = currentQ.toRotationMatrix();
}
}
} // namespace ZL } // namespace ZL

View File

@ -51,6 +51,8 @@ namespace ZL {
void handleUp(int mx, int my); void handleUp(int mx, int my);
void handleMotion(int mx, int my); void handleMotion(int mx, int my);
void updateRemotePlayers(float deltaMs);
SDL_Window* window; SDL_Window* window;
SDL_GLContext glContext; SDL_GLContext glContext;
@ -62,6 +64,7 @@ namespace ZL {
std::vector<BoxCoords> boxCoordsArr; std::vector<BoxCoords> boxCoordsArr;
std::vector<VertexRenderStruct> boxRenderArr; std::vector<VertexRenderStruct> boxRenderArr;
std::unordered_map<int, RemotePlayer> latestRemotePlayers;
static const size_t CONST_TIMER_INTERVAL = 10; static const size_t CONST_TIMER_INTERVAL = 10;
static const size_t CONST_MAX_TIME_INTERVAL = 1000; static const size_t CONST_MAX_TIME_INTERVAL = 1000;

152
src/network/ClientState.h Normal file
View File

@ -0,0 +1,152 @@
#pragma once
#include <chrono>
#include <Eigen/Dense>
#define _USE_MATH_DEFINES
#include <math.h>
using std::min;
using std::max;
struct ClientState {
int id;
Eigen::Vector3f position = { 0, 0, 45000.0f };
Eigen::Matrix3f rotation = Eigen::Matrix3f::Identity();
Eigen::Vector3f currentAngularVelocity = Eigen::Vector3f::Zero();
float velocity = 0.0f;
int selectedVelocity = 0;
float discreteMag = 0;
int discreteAngle = -1;
// Для расчета лага
uint64_t lastClientTimestamp = 0;
std::chrono::steady_clock::time_point lastUpdateServerTime;
void simulate_physics(float dt_s) {
// Константы из Game.cpp, приведенные к секундам (умножаем на 1000)
const float ANGULAR_ACCEL = 0.005f * 1000.0f;
const float ROTATION_SENSITIVITY = 0.002f * 1000.0f;
const float SHIP_ACCEL = 1.0f * 1000.0f; // CONST_ACCELERATION
// 1. Вычисляем targetAngularVelocity на лету из дискретных значений
Eigen::Vector3f targetAngularVelocity = Eigen::Vector3f::Zero();
if (discreteMag > 0.001f) {
float rad = static_cast<float>(discreteAngle) * static_cast<float>(M_PI) / 180.0f;
// Направление из угла (как в твоем обновленном клиентском коде)
Eigen::Vector3f targetDir(sinf(rad), cosf(rad), 0.0f);
targetAngularVelocity = targetDir * discreteMag;
}
// 2. Линейное изменение текущей угловой скорости к вычисленной цели
Eigen::Vector3f diffVel = targetAngularVelocity - currentAngularVelocity;
float diffLen = diffVel.norm();
if (diffLen > 0.0001f) {
float maxChange = ANGULAR_ACCEL * dt_s;
if (diffLen <= maxChange) {
currentAngularVelocity = targetAngularVelocity;
}
else {
currentAngularVelocity += (diffVel / diffLen) * maxChange;
}
}
else if (discreteMag < 0.001f && currentAngularVelocity.norm() > 0.0001f) {
// Если джойстик отпущен, используем ту же логику торможения (или ANGULAR_ACCEL)
float currentSpeed = currentAngularVelocity.norm();
float drop = ANGULAR_ACCEL * dt_s;
if (currentSpeed <= drop) {
currentAngularVelocity = Eigen::Vector3f::Zero();
}
else {
currentAngularVelocity -= (currentAngularVelocity / currentSpeed) * drop;
}
}
// 3. Применение вращения к матрице (Интеграция)
float speedScale = currentAngularVelocity.norm();
if (speedScale > 0.0001f) {
float deltaAlpha = speedScale * dt_s * ROTATION_SENSITIVITY;
Eigen::Vector3f axis = currentAngularVelocity.normalized();
Eigen::Quaternionf rotateQuat(Eigen::AngleAxisf(deltaAlpha, axis));
rotation = rotation * rotateQuat.toRotationMatrix();
}
// 4. Линейное изменение линейной скорости
float shipDesiredVelocity = static_cast<float>(selectedVelocity) * 100.f;
float speedDiff = shipDesiredVelocity - velocity;
if (std::abs(speedDiff) > 0.001f) {
float speedStep = SHIP_ACCEL * dt_s;
if (std::abs(speedDiff) <= speedStep) {
velocity = shipDesiredVelocity;
}
else {
velocity += (speedDiff > 0 ? 1.0f : -1.0f) * speedStep;
}
}
// 5. Обновление позиции
if (std::abs(velocity) > 0.01f) {
// Движение вперед по локальной оси Z (в твоем коде это {0, 0, -1})
Eigen::Vector3f localMove(0.0f, 0.0f, -velocity * dt_s);
position += rotation * localMove;
}
}
void apply_lag_compensation(std::chrono::steady_clock::time_point nowTime) {
// 1. Получаем текущее время сервера в той же шкале (мс)
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()
).count();
// 2. Вычисляем задержку
double lag_ms = 0.0;
if (nowTime > lastUpdateServerTime) {
lag_ms = std::chrono::duration<float>(nowTime - lastUpdateServerTime).count();
}
// 3. Защита от слишком больших скачков (Clamp)
// Если лаг более 500мс, ограничиваем его, чтобы избежать резких рывков
float final_lag_s = min(static_cast<float>(lag_ms), 0.5f);
if (final_lag_s > 0.001f) {
// Доматываем симуляцию на величину задержки
// Мы предполагаем, что за это время параметры управления не менялись
simulate_physics(final_lag_s);
}
}
std::string get_state_string(int id) {
// Используем кватернион для передачи вращения (4 числа вместо 9)
Eigen::Quaternionf q(rotation);
std::string s = std::to_string(id) + ","
+ std::to_string(position.x()) + "," + std::to_string(position.y()) + "," + std::to_string(position.z()) + ","
+ std::to_string(q.w()) + "," + std::to_string(q.x()) + "," + std::to_string(q.y()) + "," + std::to_string(q.z()) + ","
+ std::to_string(velocity) + ","
+ std::to_string(currentAngularVelocity.x()) + "," + std::to_string(currentAngularVelocity.y()) + "," + std::to_string(currentAngularVelocity.z()) + ","
+ std::to_string(selectedVelocity) + ","
+ std::to_string(discreteMag) + ","
+ std::to_string(discreteAngle);
return s;
}
void handle_full_sync(const std::vector<std::string>& parts) {
// Позиция
position = { std::stof(parts[2]), std::stof(parts[3]), std::stof(parts[4]) };
// Для вращения клиент должен прислать либо кватернион, либо углы Эйлера.
// Предположим, мы передаем 4 значения кватерниона для экономии:
Eigen::Quaternionf q(std::stof(parts[5]), std::stof(parts[6]), std::stof(parts[7]), std::stof(parts[8]));
rotation = q.toRotationMatrix();
currentAngularVelocity = Eigen::Vector3f{ std::stof(parts[9]), std::stof(parts[10]), std::stof(parts[11]) };
velocity = std::stof(parts[12]);
selectedVelocity = std::stoi(parts[13]);
discreteMag = std::stof(parts[14]);
discreteAngle = std::stoi(parts[15]);
}
};

View File

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <string> #include <string>
#include <unordered_map>
#include "RemotePlayer.h"
// NetworkInterface.h - Èíòåðôåéñ äëÿ ðàçíûõ òèïîâ ñîåäèíåíèé // NetworkInterface.h - Èíòåðôåéñ äëÿ ðàçíûõ òèïîâ ñîåäèíåíèé
namespace ZL { namespace ZL {
@ -10,5 +12,6 @@ namespace ZL {
virtual void Send(const std::string& message) = 0; virtual void Send(const std::string& message) = 0;
virtual bool IsConnected() const = 0; virtual bool IsConnected() const = 0;
virtual void Poll() = 0; // Äëÿ îáðàáîòêè âõîäÿùèõ ïàêåòîâ virtual void Poll() = 0; // Äëÿ îáðàáîòêè âõîäÿùèõ ïàêåòîâ
virtual std::unordered_map<int, RemotePlayer> getRemotePlayers() = 0;
}; };
} }

View File

@ -0,0 +1,15 @@
#pragma once
#include "ClientState.h"
struct RemotePlayer {
ClientState state;
// Данные для интерполяции
Eigen::Vector3f startPosition;
Eigen::Vector3f targetPosition;
Eigen::Quaternionf startRotation;
Eigen::Quaternionf targetRotation;
float interpolationFactor = 0.0f;
uint64_t lastSnapshotTime = 0;
};

View File

@ -1,6 +1,18 @@
#include "WebSocketClient.h" #include "WebSocketClient.h"
#include <iostream> #include <iostream>
#include <SDL2/SDL.h>
#include "RemotePlayer.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 { namespace ZL {
@ -55,19 +67,121 @@ namespace ZL {
} }
void WebSocketClient::Poll() { void WebSocketClient::Poll() {
// Забираем сообщения из очереди и обрабатываем их в главном потоке
std::lock_guard<std::mutex> lock(queueMutex); std::lock_guard<std::mutex> lock(queueMutex);
while (!messageQueue.empty()) { while (!messageQueue.empty()) {
std::string msg = messageQueue.front(); std::string msg = messageQueue.front();
messageQueue.pop(); messageQueue.pop();
// Тут можно вызвать колбэки игры или обновить состояние
if (msg.rfind("WORLD_UPDATE|", 0) == 0) {
parseWorldUpdate(msg);
std::cout << msg << std::endl;
}
} }
} }
void WebSocketClient::parseWorldUpdate(const std::string& msg) {
// Ôîðìàò: WORLD_UPDATE|server_now_ms|count|id,x,y,z,w,qx,qy,qz,v;...
auto parts = split(msg, '|');
if (parts.size() < 4) return;
uint64_t serverTime = std::stoull(parts[1]);
int count = std::stoi(parts[2]);
auto playersData = split(parts[3], ';');
for (const auto& pData : playersData) {
auto vals = split(pData, ',');
if (vals.size() < 9) continue;
int id = std::stoi(vals[0]);
if (id == this->clientId) continue; // Ïðîïóñêàåì ñåáÿ
// Ëîãèêà îáíîâëåíèÿ èëè ñîçäàíèÿ RemotePlayer
updateRemotePlayer(id, vals);
}
}
void WebSocketClient::updateRemotePlayer(int id, const std::vector<std::string>& vals) {
// Èñïîëüçóåì ìüþòåêñ, òàê êàê ýòîò ìåòîä âûçûâàåòñÿ èç ñåòåâîãî ïîòîêà (TaskManager)
std::lock_guard<std::mutex> lock(playersMutex);
auto& rp = remotePlayers[id];
rp.state.id = id;
// 1. Ñîõðàíÿåì òåêóùèå âèçóàëüíûå êîîðäèíàòû êàê íà÷àëüíóþ òî÷êó äëÿ íîâîé èíòåðïîëÿöèè
// Ýòî ïðåäîòâðàòèò "òåëåïîðòàöèþ", åñëè ïðåäûäóùàÿ èíòåðïîëÿöèÿ íå óñïåëà äîéòè äî 1.0
if (rp.lastSnapshotTime == 0) {
// Ïåðâûé ïàêåò äëÿ ýòîãî èãðîêà
rp.startPosition = { std::stof(vals[1]), std::stof(vals[2]), std::stof(vals[3]) };
rp.startRotation = Eigen::Quaternionf(std::stof(vals[4]), std::stof(vals[5]), std::stof(vals[6]), std::stof(vals[7]));
}
else {
// Âû÷èñëÿåì òåêóùåå ïîëîæåíèå, íà êîòîðîì îñòàíîâèëèñü, ÷òîáû íà÷àòü îòòóäà
rp.startPosition = rp.state.position;
rp.startRotation = Eigen::Quaternionf(rp.state.rotation);
}
// 2. Óñòàíàâëèâàåì íîâûå öåëåâûå çíà÷åíèÿ îò ñåðâåðà
rp.targetPosition = { std::stof(vals[1]), std::stof(vals[2]), std::stof(vals[3]) };
rp.targetRotation = Eigen::Quaternionf(
std::stof(vals[4]), // w
std::stof(vals[5]), // x
std::stof(vals[6]), // y
std::stof(vals[7]) // z
);
// 3. Ñîõðàíÿåì îñòàëüíûå ôèçè÷åñêèå ïàðàìåòðû (äëÿ ýêñòðàïîëÿöèè èëè îòëàäêè)
rp.state.velocity = std::stof(vals[8]);
rp.state.currentAngularVelocity = { std::stof(vals[9]), std::stof(vals[10]), std::stof(vals[11]) };
rp.state.selectedVelocity = std::stoi(vals[12]);
rp.state.discreteMag = std::stoi(vals[13]);
rp.state.discreteAngle = std::stoi(vals[14]);
// 4. Ñáðàñûâàåì òàéìåð èíòåðïîëÿöèè
rp.interpolationFactor = 0.0f;
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()
).count();
rp.lastSnapshotTime = now_ms;
}
void WebSocketClient::Send(const std::string& message) { void WebSocketClient::Send(const std::string& message) {
if (!connected) return; if (!connected) return;
// Отправку тоже можно сделать асинхронной, чтобы не ждать в главном потоке
auto ss = std::make_shared<std::string>(message); auto ss = std::make_shared<std::string>(message);
ws_->async_write(boost::asio::buffer(*ss), [ss](boost::beast::error_code, std::size_t) {});
std::lock_guard<std::mutex> lock(writeMutex_);
writeQueue_.push(ss);
// Åñëè ñåé÷àñ íè÷åãî íå çàïèñûâàåòñÿ, èíèöèèðóåì ïåðâóþ çàïèñü
if (!isWriting_) {
doWrite();
}
}
void WebSocketClient::doWrite() {
// Ýòà ôóíêöèÿ âñåãäà âûçûâàåòñÿ ïîä ìüþòåêñîì èëè èç êîëáýêà
if (writeQueue_.empty()) {
isWriting_ = false;
return;
}
isWriting_ = true;
auto message = writeQueue_.front();
// Çàõâàòûâàåì self (shared_from_this), ÷òîáû îáúåêò íå óäàëèëñÿ âî âðåìÿ çàïèñè
ws_->async_write(
boost::asio::buffer(*message),
[this, message](boost::beast::error_code ec, std::size_t) {
if (ec) {
connected = false;
return;
}
std::lock_guard<std::mutex> lock(writeMutex_);
writeQueue_.pop(); // Óäàëÿåì îòïðàâëåííîå ñîîáùåíèå
doWrite(); // Ïðîâåðÿåì ñëåäóþùåå
}
);
} }
} }

View File

@ -21,9 +21,16 @@ namespace ZL {
std::queue<std::string> messageQueue; std::queue<std::string> messageQueue;
std::mutex queueMutex; // Çàùèòà äëÿ messageQueue std::mutex queueMutex; // Çàùèòà äëÿ messageQueue
std::queue<std::shared_ptr<std::string>> writeQueue_;
bool isWriting_ = false;
std::mutex writeMutex_; // Îòäåëüíûé ìüþòåêñ äëÿ î÷åðåäè çàïèñè
bool connected = false; bool connected = false;
int clientId = -1; int clientId = -1;
std::unordered_map<int, RemotePlayer> remotePlayers;
std::mutex playersMutex;
void startAsyncRead(); void startAsyncRead();
void processIncomingMessage(const std::string& msg); void processIncomingMessage(const std::string& msg);
@ -33,10 +40,18 @@ namespace ZL {
void Connect(const std::string& host, uint16_t port) override; void Connect(const std::string& host, uint16_t port) override;
void Poll() override; void Poll() override;
void parseWorldUpdate(const std::string& msg);
void updateRemotePlayer(int id, const std::vector<std::string>& vals);
void Send(const std::string& message) override; void Send(const std::string& message) override;
void doWrite();
bool IsConnected() const override { return connected; } bool IsConnected() const override { return connected; }
int GetClientId() const { return clientId; } int GetClientId() const { return clientId; }
std::unordered_map<int, RemotePlayer> getRemotePlayers() override {
std::lock_guard<std::mutex> lock(playersMutex);
return remotePlayers;
}
}; };
} }