From 1348efb5fe95ecb3547199a474232c510df7b0c2 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Thu, 15 Jan 2026 21:54:37 +0300 Subject: [PATCH] Working on server --- proj-windows/CMakeLists.txt | 2 + server/CMakeLists.txt | 5 +- server/main.cpp | 81 --------- server/server.cpp | 237 ++++++++++++++++++++++++ src/Environment.cpp | 5 + src/Environment.h | 5 + src/Game.cpp | 307 +++++++++++++++++++++----------- src/Game.h | 5 +- src/network/ClientState.h | 152 ++++++++++++++++ src/network/NetworkInterface.h | 3 + src/network/RemotePlayer.h | 15 ++ src/network/WebSocketClient.cpp | 122 ++++++++++++- src/network/WebSocketClient.h | 15 ++ 13 files changed, 761 insertions(+), 193 deletions(-) delete mode 100644 server/main.cpp create mode 100644 server/server.cpp create mode 100644 src/network/ClientState.h create mode 100644 src/network/RemotePlayer.h diff --git a/proj-windows/CMakeLists.txt b/proj-windows/CMakeLists.txt index fd01681..774e1e0 100644 --- a/proj-windows/CMakeLists.txt +++ b/proj-windows/CMakeLists.txt @@ -53,6 +53,8 @@ add_executable(space-game001 ../src/network/NetworkInterface.h ../src/network/LocalClient.h ../src/network/LocalClient.cpp + ../src/network/ClientState.h + ../src/network/RemotePlayer.h ../src/network/WebSocketClient.h ../src/network/WebSocketClient.cpp ) diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index b119422..5e12ff8 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -25,7 +25,10 @@ add_subdirectory("${BOOST_SRC_DIR}/libs/predef" boost-predef-build EXCLUDE_FROM_ # 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}) diff --git a/server/main.cpp b/server/main.cpp deleted file mode 100644 index c3a3195..0000000 --- a/server/main.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -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 { - websocket::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::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(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; -} \ No newline at end of file diff --git a/server/server.cpp b/server/server.cpp new file mode 100644 index 0000000..635d3c8 --- /dev/null +++ b/server/server.cpp @@ -0,0 +1,237 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define _USE_MATH_DEFINES +#include +#include "../src/network/ClientState.h" + +// split +std::vector split(const std::string& s, char delimiter) { + std::vector 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> g_sessions; +std::mutex g_sessions_mutex; + +class Session : public std::enable_shared_from_this { + websocket::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( + 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::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 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(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::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 lock(g_sessions_mutex); + g_sessions.erase(std::remove_if(g_sessions.begin(), g_sessions.end(), + [self](const std::shared_ptr& 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 lock(g_sessions_mutex); + + // 1. + for (auto& session : g_sessions) { + session->tick_physics_global(now); + } + + static auto last_broadcast = now; + if (std::chrono::duration(now - last_broadcast).count() >= 1.0f) { + last_broadcast = now; + + auto now_ms = std::chrono::duration_cast( + 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(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; +} \ No newline at end of file diff --git a/src/Environment.cpp b/src/Environment.cpp index 5f2bf6a..7c6ab60 100644 --- a/src/Environment.cpp +++ b/src/Environment.cpp @@ -40,8 +40,13 @@ Eigen::Vector2f Environment::tapDownCurrentPos = { 0, 0 }; Eigen::Vector3f Environment::shipPosition = {0,0,45000.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_FAR = 5000.f; diff --git a/src/Environment.h b/src/Environment.h index 0e01ee1..6c52147 100644 --- a/src/Environment.h +++ b/src/Environment.h @@ -38,6 +38,11 @@ public: static Eigen::Vector3f shipPosition; 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_FAR; diff --git a/src/Game.cpp b/src/Game.cpp index 974ffe6..447f401 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -19,6 +19,8 @@ #include "network/LocalClient.h" #endif +#include "network/RemotePlayer.h" + namespace ZL { #ifdef EMSCRIPTEN @@ -139,33 +141,6 @@ namespace ZL ZL::BindOpenGlFunctions(); 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 renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE); @@ -178,37 +153,11 @@ namespace ZL #else 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); -#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); -#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); -#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); -#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); -#ifdef __ANDROID__ - __android_log_print(ANDROID_LOG_INFO, "ShaderManager", - "Step 1"); -#endif - #endif 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) { - 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::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( std::array{ 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.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.RefreshVBO(); @@ -343,16 +289,8 @@ namespace ZL boxTexture = std::make_unique(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE)); boxBase = LoadFromTextFile02("resources/box/box.txt", CONST_ZIP_FILE); - std::cout << "Init step 1 " << std::endl; - boxCoordsArr = generateRandomBoxCoords(50); - - std::cout << "Init step 2 " << std::endl; - boxRenderArr.resize(boxCoordsArr.size()); - - std::cout << "Init step 3x " << std::endl; - for (int i = 0; i < boxCoordsArr.size(); i++) { boxRenderArr[i].AssignFrom(boxBase); @@ -362,26 +300,17 @@ namespace ZL boxAlive.resize(boxCoordsArr.size(), true); - std::cout << "Init step 4 " << std::endl; - if (!cfgLoaded) { throw std::runtime_error("Failed to load spark emitter config file!"); } - std::cout << "Init step 5 " << std::endl; renderer.InitOpenGL(); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - std::cout << "Init step 6 " << std::endl; planetObject.init(); - std::cout << "Init step 7 " << std::endl; - //rockTexture = std::make_unique(CreateTextureDataFromPng("./resources/rock.png", CONST_ZIP_FILE)); - - std::cout << "Init step 8 " << std::endl; - #ifdef NETWORK networkClient = std::make_unique(taskManager.getIOContext()); networkClient->Connect("127.0.0.1", 8080); @@ -639,14 +568,182 @@ namespace ZL sparkEmitter.update(static_cast(delta)); planetObject.update(static_cast(delta)); + updateRemotePlayers(static_cast(delta)); static float pingTimer = 0.0f; pingTimer += delta; if (pingTimer >= 1000.0f) { - networkClient->Send("PING"); + + auto now_ms = std::chrono::duration_cast( + 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; } + 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(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::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(discreteAngle) * static_cast(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(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(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(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(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) { float diffx = Environment::tapDownCurrentPos(0) - Environment::tapDownStartPos(0); @@ -671,14 +768,7 @@ namespace ZL 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) { if (p && p->isActive()) { @@ -896,21 +986,7 @@ namespace ZL { 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 @@ -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 diff --git a/src/Game.h b/src/Game.h index b0aa524..d66d654 100644 --- a/src/Game.h +++ b/src/Game.h @@ -44,13 +44,15 @@ namespace ZL { void drawShip(); void drawBoxes(); void drawUI(); - + void fireProjectiles(); void handleDown(int mx, int my); void handleUp(int mx, int my); void handleMotion(int mx, int my); + void updateRemotePlayers(float deltaMs); + SDL_Window* window; SDL_GLContext glContext; @@ -62,6 +64,7 @@ namespace ZL { std::vector boxCoordsArr; std::vector boxRenderArr; + std::unordered_map latestRemotePlayers; static const size_t CONST_TIMER_INTERVAL = 10; static const size_t CONST_MAX_TIME_INTERVAL = 1000; diff --git a/src/network/ClientState.h b/src/network/ClientState.h new file mode 100644 index 0000000..edf77c2 --- /dev/null +++ b/src/network/ClientState.h @@ -0,0 +1,152 @@ +#pragma once +#include +#include +#define _USE_MATH_DEFINES +#include + + + +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(discreteAngle) * static_cast(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(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::steady_clock::now().time_since_epoch() + ).count(); + + // 2. Вычисляем задержку + double lag_ms = 0.0; + if (nowTime > lastUpdateServerTime) { + lag_ms = std::chrono::duration(nowTime - lastUpdateServerTime).count(); + } + + // 3. Защита от слишком больших скачков (Clamp) + // Если лаг более 500мс, ограничиваем его, чтобы избежать резких рывков + float final_lag_s = min(static_cast(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& 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]); + } +}; diff --git a/src/network/NetworkInterface.h b/src/network/NetworkInterface.h index dca15c0..2691be4 100644 --- a/src/network/NetworkInterface.h +++ b/src/network/NetworkInterface.h @@ -1,5 +1,7 @@ #pragma once #include +#include +#include "RemotePlayer.h" // NetworkInterface.h - namespace ZL { @@ -10,5 +12,6 @@ namespace ZL { virtual void Send(const std::string& message) = 0; virtual bool IsConnected() const = 0; virtual void Poll() = 0; // + virtual std::unordered_map getRemotePlayers() = 0; }; } diff --git a/src/network/RemotePlayer.h b/src/network/RemotePlayer.h new file mode 100644 index 0000000..ff9bb8d --- /dev/null +++ b/src/network/RemotePlayer.h @@ -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; +}; \ No newline at end of file diff --git a/src/network/WebSocketClient.cpp b/src/network/WebSocketClient.cpp index 5f0784e..276ea57 100644 --- a/src/network/WebSocketClient.cpp +++ b/src/network/WebSocketClient.cpp @@ -1,6 +1,18 @@ #include "WebSocketClient.h" #include +#include +#include "RemotePlayer.h" +// split +std::vector split(const std::string& s, char delimiter) { + std::vector tokens; + std::string token; + std::istringstream tokenStream(s); + while (std::getline(tokenStream, token, delimiter)) { + tokens.push_back(token); + } + return tokens; +} namespace ZL { @@ -55,19 +67,121 @@ namespace ZL { } void WebSocketClient::Poll() { - // std::lock_guard lock(queueMutex); while (!messageQueue.empty()) { std::string msg = messageQueue.front(); 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& vals) { + // , (TaskManager) + std::lock_guard 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::steady_clock::now().time_since_epoch() + ).count(); + rp.lastSnapshotTime = now_ms; + } + + void WebSocketClient::Send(const std::string& message) { if (!connected) return; - // , + auto ss = std::make_shared(message); - ws_->async_write(boost::asio::buffer(*ss), [ss](boost::beast::error_code, std::size_t) {}); + + std::lock_guard 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 lock(writeMutex_); + writeQueue_.pop(); // + doWrite(); // + } + ); } } \ No newline at end of file diff --git a/src/network/WebSocketClient.h b/src/network/WebSocketClient.h index ffaaecc..5f18ced 100644 --- a/src/network/WebSocketClient.h +++ b/src/network/WebSocketClient.h @@ -21,9 +21,16 @@ namespace ZL { std::queue messageQueue; std::mutex queueMutex; // messageQueue + std::queue> writeQueue_; + bool isWriting_ = false; + std::mutex writeMutex_; // + bool connected = false; int clientId = -1; + std::unordered_map remotePlayers; + std::mutex playersMutex; + void startAsyncRead(); void processIncomingMessage(const std::string& msg); @@ -33,10 +40,18 @@ namespace ZL { void Connect(const std::string& host, uint16_t port) override; void Poll() override; + void parseWorldUpdate(const std::string& msg); + void updateRemotePlayer(int id, const std::vector& vals); void Send(const std::string& message) override; + void doWrite(); bool IsConnected() const override { return connected; } int GetClientId() const { return clientId; } + + std::unordered_map getRemotePlayers() override { + std::lock_guard lock(playersMutex); + return remotePlayers; + } }; } \ No newline at end of file