From cacc18dc7e97f20f9d2d5086f631062bdd83a8c6 Mon Sep 17 00:00:00 2001 From: Vlad Date: Mon, 19 Jan 2026 22:04:33 +0600 Subject: [PATCH 1/4] added boxes from server --- server/server.cpp | 344 ++++++++++++++++++++++---------- src/Game.cpp | 21 ++ src/Game.h | 1 + src/network/ClientState.h | 2 +- src/network/LocalClient.h | 4 + src/network/NetworkInterface.h | 2 + src/network/WebSocketClient.cpp | 36 ++++ src/network/WebSocketClient.h | 9 + 8 files changed, 308 insertions(+), 111 deletions(-) diff --git a/server/server.cpp b/server/server.cpp index 1853f88..96350f3 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -11,16 +11,18 @@ #define _USE_MATH_DEFINES #include #include "../src/network/ClientState.h" +#include +#include // Вспомогательный 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; + 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; @@ -31,152 +33,274 @@ using tcp = net::ip::tcp; class Session; +struct ServerBox { + Eigen::Vector3f position; + Eigen::Matrix3f rotation; + float collisionRadius = 2.0f; +}; + +std::vector g_serverBoxes; +std::mutex g_boxes_mutex; + + 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_; - + websocket::stream ws_; + beast::flat_buffer buffer_; + int id_; + bool is_writing_ = false; + ClientStateInterval timedClientStates; - void process_message(const std::string& msg) { - auto now_server = std::chrono::system_clock::now(); + void process_message(const std::string& msg) { + auto now_server = std::chrono::system_clock::now(); - auto parts = split(msg, ':'); + auto parts = split(msg, ':'); - if (parts.size() < 16) - { - throw std::runtime_error("Unknown message type received, too small"); - } + if (parts.size() < 16) + { + throw std::runtime_error("Unknown message type received, too small"); + } - uint64_t clientTimestamp = std::stoull(parts[1]); + uint64_t clientTimestamp = std::stoull(parts[1]); - ClientState receivedState; + ClientState receivedState; - receivedState.id = id_; + receivedState.id = id_; - std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast(std::chrono::milliseconds(clientTimestamp)) }; - receivedState.lastUpdateServerTime = uptime_timepoint; + std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast(std::chrono::milliseconds(clientTimestamp)) }; + receivedState.lastUpdateServerTime = uptime_timepoint; - if (parts[0] == "UPD") { - receivedState.handle_full_sync(parts, 2); - retranslateMessage(msg); - } - else - { + if (parts[0] == "UPD") { + receivedState.handle_full_sync(parts, 2); + retranslateMessage(msg); + } + else + { throw std::runtime_error("Unknown message type received: " + parts[0]); - } + } timedClientStates.add_state(receivedState); - } + } - void retranslateMessage(const std::string& msg) - { - std::string event_msg = "EVENT:" + std::to_string(id_) + ":" + msg; + void retranslateMessage(const std::string& msg) + { + std::string event_msg = "EVENT:" + std::to_string(id_) + ":" + msg; + + std::lock_guard lock(g_sessions_mutex); + for (auto& session : g_sessions) { + if (session->get_id() != id_) { // Не шлем отправителю + session->send_message(event_msg); + } + } + } + + void sendBoxesToClient() { + std::lock_guard lock(g_boxes_mutex); + + std::string boxMsg = "BOXES:"; + for (const auto& box : g_serverBoxes) { + Eigen::Quaternionf q(box.rotation); + boxMsg += std::to_string(box.position.x()) + ":" + + std::to_string(box.position.y()) + ":" + + std::to_string(box.position.z()) + ":" + + std::to_string(q.w()) + ":" + + std::to_string(q.x()) + ":" + + std::to_string(q.y()) + ":" + + std::to_string(q.z()) + "|"; + } + + if (!boxMsg.empty() && boxMsg.back() == '|') { + boxMsg.pop_back(); + } + + send_message(boxMsg); + } + + void send_message(std::string msg) { + auto ss = std::make_shared(std::move(msg)); + + if (is_writing_) { + + ws_.async_write(net::buffer(*ss), + [self = shared_from_this(), ss](beast::error_code ec, std::size_t) { + if (ec) { + std::cerr << "Write error: " << ec.message() << std::endl; + } + }); + } + else { + is_writing_ = true; + ws_.async_write(net::buffer(*ss), + [self = shared_from_this(), ss](beast::error_code ec, std::size_t) { + self->is_writing_ = false; + if (ec) { + std::cerr << "Write error: " << ec.message() << std::endl; + } + }); + } + } - std::lock_guard lock(g_sessions_mutex); - for (auto& session : g_sessions) { - if (session->get_id() != id_) { // Не шлем отправителю - session->send_message(event_msg); - } - } - } - public: - explicit Session(tcp::socket&& socket, int id) - : ws_(std::move(socket)), id_(id) { - } + explicit Session(tcp::socket&& socket, int id) + : ws_(std::move(socket)), id_(id) { + } - void init() - { - } + void init() + { + sendBoxesToClient(); - void run() { + auto timer = std::make_shared(ws_.get_executor()); + 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_)); + self->do_read(); + } + }); + } - { - std::lock_guard lock(g_sessions_mutex); + 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(); - }); - } + 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 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) {}); - } + /*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 get_id() const { - return id_; + int get_id() const { + return id_; } 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(); + 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; - } + return; + } - std::string msg = beast::buffers_to_string(self->buffer_.data()); - self->process_message(msg); + std::string msg = beast::buffers_to_string(self->buffer_.data()); + self->process_message(msg); - self->buffer_.consume(self->buffer_.size()); - self->do_read(); - }); - } + self->buffer_.consume(self->buffer_.size()); + self->do_read(); + }); + } }; void update_world(net::steady_timer& timer, net::io_context& ioc) { - // TODO: Renew game state + // TODO: Renew game state - timer.expires_after(std::chrono::milliseconds(50)); - timer.async_wait([&](const boost::system::error_code& ec) { - if (!ec) update_world(timer, ioc); - }); + timer.expires_after(std::chrono::milliseconds(50)); + timer.async_wait([&](const boost::system::error_code& ec) { + if (!ec) update_world(timer, ioc); + }); } +std::vector generateServerBoxes(int count) { + std::vector boxes; + std::random_device rd; + std::mt19937 gen(rd()); + + const float MIN_COORD = -100.0f; + const float MAX_COORD = 100.0f; + const float MIN_DISTANCE = 3.0f; + const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE; + const int MAX_ATTEMPTS = 1000; + + std::uniform_real_distribution<> posDistrib(MIN_COORD, MAX_COORD); + std::uniform_real_distribution<> angleDistrib(0.0, M_PI * 2.0); + + for (int i = 0; i < count; i++) { + bool accepted = false; + int attempts = 0; + + while (!accepted && attempts < MAX_ATTEMPTS) { + ServerBox box; + box.position = Eigen::Vector3f( + (float)posDistrib(gen), + (float)posDistrib(gen), + (float)posDistrib(gen) + ); + + accepted = true; + for (const auto& existingBox : boxes) { + Eigen::Vector3f diff = box.position - existingBox.position; + if (diff.squaredNorm() < MIN_DISTANCE_SQUARED) { + accepted = false; + break; + } + } + + if (accepted) { + float randomAngle = (float)angleDistrib(gen); + Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized(); + box.rotation = Eigen::AngleAxisf(randomAngle, axis).toRotationMatrix(); + boxes.push_back(box); + } + + attempts++; + } + } + + return boxes; +} + + int main() { - try { - net::io_context ioc; - tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} }; - int next_id = 1000; + try { + { + std::lock_guard lock(g_boxes_mutex); + g_serverBoxes = generateServerBoxes(50); + std::cout << "Generated " << g_serverBoxes.size() << " boxes on server\n"; + } + net::io_context ioc; + tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} }; + int next_id = 1000; - std::cout << "Server started on port 8080...\n"; + 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); - }); - }; + 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; + 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/Game.cpp b/src/Game.cpp index 7afcadb..b0b7cb2 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -577,6 +577,27 @@ namespace ZL latestRemotePlayers = networkClient->getRemotePlayers(); + // Если сервер прислал коробки, применяем их однократно вместо локальной генерации + if (!serverBoxesApplied && networkClient) { + auto sboxes = networkClient->getServerBoxes(); + if (!sboxes.empty()) { + boxCoordsArr.clear(); + for (auto &b : sboxes) { + BoxCoords bc; + bc.pos = b.first; + bc.m = b.second; + boxCoordsArr.push_back(bc); + } + boxRenderArr.resize(boxCoordsArr.size()); + for (int i = 0; i < (int)boxCoordsArr.size(); ++i) { + boxRenderArr[i].AssignFrom(boxBase); + boxRenderArr[i].RefreshVBO(); + } + boxAlive.assign(boxCoordsArr.size(), true); + serverBoxesApplied = true; + } + } + // Итерируемся по актуальным данным из extrapolateRemotePlayers for (auto const& [id, remotePlayer] : latestRemotePlayers) { diff --git a/src/Game.h b/src/Game.h index 597c223..8c91b4b 100644 --- a/src/Game.h +++ b/src/Game.h @@ -104,6 +104,7 @@ namespace ZL { uint64_t lastExplosionTime = 0; const uint64_t explosionDurationMs = 500; + bool serverBoxesApplied = false; }; diff --git a/src/network/ClientState.h b/src/network/ClientState.h index ac28def..ca2eca2 100644 --- a/src/network/ClientState.h +++ b/src/network/ClientState.h @@ -14,7 +14,7 @@ constexpr float SHIP_ACCEL = 1.0f * 1000.0f; constexpr float ROTATION_SENSITIVITY = 0.002f; constexpr long long SERVER_DELAY = 0; //ms -constexpr long long CLIENT_DELAY = 200; //ms +constexpr long long CLIENT_DELAY = 1000; //ms constexpr long long CUTOFF_TIME = 5000; //ms struct ClientState { diff --git a/src/network/LocalClient.h b/src/network/LocalClient.h index 55994b3..8cd1cee 100644 --- a/src/network/LocalClient.h +++ b/src/network/LocalClient.h @@ -21,5 +21,9 @@ namespace ZL { std::unordered_map getRemotePlayers() override { return std::unordered_map(); } + + std::vector> getServerBoxes() override { + return {}; + } }; } \ No newline at end of file diff --git a/src/network/NetworkInterface.h b/src/network/NetworkInterface.h index 4873f07..d2c513d 100644 --- a/src/network/NetworkInterface.h +++ b/src/network/NetworkInterface.h @@ -15,5 +15,7 @@ namespace ZL { virtual bool IsConnected() const = 0; virtual void Poll() = 0; // ƒл¤ обработки вход¤щих пакетов virtual std::unordered_map getRemotePlayers() = 0; + + virtual std::vector> getServerBoxes() = 0; }; } diff --git a/src/network/WebSocketClient.cpp b/src/network/WebSocketClient.cpp index ce9dd3e..c0aa195 100644 --- a/src/network/WebSocketClient.cpp +++ b/src/network/WebSocketClient.cpp @@ -83,6 +83,42 @@ namespace ZL { std::string msg = messageQueue.front(); messageQueue.pop(); + // Обработка списка коробок от сервера + if (msg.rfind("BOXES:", 0) == 0) { + std::string payload = msg.substr(6); // после "BOXES:" + std::vector> 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 bLock(boxesMutex); + serverBoxes_ = std::move(parsedBoxes); + } + continue; + } + if (msg.rfind("EVENT:", 0) == 0) { auto parts = split(msg, ':'); if (parts.size() < 5) continue; // EVENT:ID:TYPE:TIME:DATA... diff --git a/src/network/WebSocketClient.h b/src/network/WebSocketClient.h index d8b108f..0692d67 100644 --- a/src/network/WebSocketClient.h +++ b/src/network/WebSocketClient.h @@ -34,6 +34,10 @@ namespace ZL { std::unordered_map remotePlayers; std::mutex playersMutex; + // Серверные коробки + std::vector> serverBoxes_; + std::mutex boxesMutex; + void startAsyncRead(); void processIncomingMessage(const std::string& msg); @@ -54,6 +58,11 @@ namespace ZL { std::lock_guard lock(playersMutex); return remotePlayers; } + + std::vector> getServerBoxes() override { + std::lock_guard lock(boxesMutex); + return serverBoxes_; + } }; } #endif From e8fb14b809c8a28ecbb2669a8878b725385216fb Mon Sep 17 00:00:00 2001 From: Vlad Date: Tue, 27 Jan 2026 19:38:18 +0600 Subject: [PATCH 2/4] added sync projectiles/boxes --- server/server.cpp | 79 +++++-- src/Game.cpp | 65 +++++- src/network/LocalClient.cpp | 16 +- src/network/LocalClient.h | 1 + src/network/NetworkInterface.h | 9 + src/network/WebSocketClient.cpp | 355 +++++++++++++++++--------------- src/network/WebSocketClient.h | 5 + 7 files changed, 338 insertions(+), 192 deletions(-) diff --git a/server/server.cpp b/server/server.cpp index 96350f3..1dd04fa 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -56,33 +56,76 @@ class Session : public std::enable_shared_from_this { void process_message(const std::string& msg) { auto now_server = std::chrono::system_clock::now(); - auto parts = split(msg, ':'); - if (parts.size() < 16) - { - throw std::runtime_error("Unknown message type received, too small"); + if (parts.empty()) { + std::cerr << "Empty message received\n"; + return; } - uint64_t clientTimestamp = std::stoull(parts[1]); + std::string type = parts[0]; - ClientState receivedState; - - receivedState.id = id_; - - std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast(std::chrono::milliseconds(clientTimestamp)) }; - receivedState.lastUpdateServerTime = uptime_timepoint; - - if (parts[0] == "UPD") { + if (type == "UPD") { + if (parts.size() < 16) { + std::cerr << "Invalid UPD message: too few parts (" << parts.size() << ")\n"; + return; + } + uint64_t clientTimestamp = std::stoull(parts[1]); + ClientState receivedState; + receivedState.id = id_; + std::chrono::system_clock::time_point uptime_timepoint{ + std::chrono::milliseconds(clientTimestamp) + }; + receivedState.lastUpdateServerTime = uptime_timepoint; receivedState.handle_full_sync(parts, 2); retranslateMessage(msg); + timedClientStates.add_state(receivedState); } - else - { - throw std::runtime_error("Unknown message type received: " + parts[0]); - } + else if (parts[0] == "FIRE") { + if (parts.size() < 8) { + std::cerr << "Invalid FIRE: too few parts\n"; + return; + } - timedClientStates.add_state(receivedState); + uint64_t clientTime = std::stoull(parts[1]); + Eigen::Vector3f pos{ + std::stof(parts[2]), std::stof(parts[3]), std::stof(parts[4]) + }; + Eigen::Vector3f dir{ + std::stof(parts[5]), std::stof(parts[6]), std::stof(parts[7]) + }; + + int shotCount = 1; + if (parts.size() >= 9) { + try { + shotCount = std::stoi(parts[8]); + } + catch (...) { + shotCount = 1; + } + } + + std::string broadcast = "PROJECTILE:" + + std::to_string(id_) + ":" + + std::to_string(clientTime) + ":" + + std::to_string(pos.x()) + ":" + + std::to_string(pos.y()) + ":" + + std::to_string(pos.z()) + ":" + + std::to_string(dir.x()) + ":" + + std::to_string(dir.y()) + ":" + + std::to_string(dir.z()) + ":" + + std::to_string(shotCount); + + std::lock_guard lock(g_sessions_mutex); + std::cout << "Player " << id_ << " fired " << shotCount << " shots → broadcasting\n"; + + for (auto& session : g_sessions) { + session->send_message(broadcast); + } + } + else { + std::cerr << "Unknown message type: " << type << "\n"; + } } void retranslateMessage(const std::string& msg) diff --git a/src/Game.cpp b/src/Game.cpp index b0b7cb2..4d875ce 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -232,7 +232,24 @@ namespace ZL uint64_t now = SDL_GetTicks64(); if (now - lastProjectileFireTime >= static_cast(projectileCooldownMs)) { lastProjectileFireTime = now; - fireProjectiles(); + + Eigen::Vector3f localForward = { 0, 0, -1 }; + Eigen::Vector3f worldForward = (Environment::shipState.rotation * localForward).normalized(); + + Eigen::Vector3f centerPos = Environment::shipState.position + + Environment::shipState.rotation * Vector3f{ 0, 0.9f, 5.0f }; + + std::string fireMsg = "FIRE:" + + std::to_string(now) + ":" + + std::to_string(centerPos.x()) + ":" + + std::to_string(centerPos.y()) + ":" + + std::to_string(centerPos.z()) + ":" + + std::to_string(worldForward.x()) + ":" + + std::to_string(worldForward.y()) + ":" + + std::to_string(worldForward.z()) + ":" + + "2"; + + networkClient->Send(fireMsg); } }); @@ -582,7 +599,7 @@ namespace ZL auto sboxes = networkClient->getServerBoxes(); if (!sboxes.empty()) { boxCoordsArr.clear(); - for (auto &b : sboxes) { + for (auto& b : sboxes) { BoxCoords bc; bc.pos = b.first; bc.m = b.second; @@ -607,7 +624,7 @@ namespace ZL } ClientState playerState = remotePlayer.fetchClientStateAtTime(now); - + renderer.PushMatrix(); renderer.LoadIdentity(); @@ -704,7 +721,7 @@ namespace ZL float radians = atan2f(diffy, diffx); discreteAngle = static_cast(radians * 180.0f / M_PI); if (discreteAngle < 0) discreteAngle += 360; - + } else { @@ -718,7 +735,7 @@ namespace ZL discreteMag = 0.0f; } - + if (discreteAngle != Environment::shipState.discreteAngle || discreteMag != Environment::shipState.discreteMag) { Environment::shipState.discreteAngle = discreteAngle; Environment::shipState.discreteMag = discreteMag; @@ -1097,7 +1114,7 @@ namespace ZL { if (event.key.keysym.sym == SDLK_i) { - + } }*/ #endif @@ -1105,6 +1122,42 @@ namespace ZL render(); mainThreadHandler.processMainThreadTasks(); networkClient->Poll(); + + if (networkClient) { + auto pending = networkClient->getPendingProjectiles(); + if (!pending.empty()) { + const float projectileSpeed = 60.0f; + const float lifeMs = 5000.0f; + const float size = 0.5f; + + for (const auto& pi : pending) { + Eigen::Vector3f dir = pi.direction; + float len = dir.norm(); + if (len <= 1e-6f) continue; + dir /= len; + Eigen::Vector3f baseVel = dir * projectileSpeed; + + int shotCount = 1; + shotCount = 2; + + std::vector localOffsets = { + {-1.5f, 0.9f, 5.0f}, + { 1.5f, 0.9f, 5.0f} + }; + + for (int i = 0; i < shotCount; ++i) { + Eigen::Vector3f shotPos = pi.position + localOffsets[i]; + + for (auto& p : projectiles) { + if (!p->isActive()) { + p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer); + break; + } + } + } + } + } + } } void Game::handleDown(int mx, int my) diff --git a/src/network/LocalClient.cpp b/src/network/LocalClient.cpp index 264f526..b460a6f 100644 --- a/src/network/LocalClient.cpp +++ b/src/network/LocalClient.cpp @@ -4,15 +4,17 @@ namespace ZL { - void LocalClient::Connect(const std::string& host, uint16_t port) { - } + void LocalClient::Connect(const std::string& host, uint16_t port) { + } - void LocalClient::Poll() { - } + void LocalClient::Poll() { + } - void LocalClient::Send(const std::string& message) { - - } + void LocalClient::Send(const std::string& message) { + } + std::vector LocalClient::getPendingProjectiles() { + return {}; + } } \ No newline at end of file diff --git a/src/network/LocalClient.h b/src/network/LocalClient.h index 8cd1cee..c45c0c8 100644 --- a/src/network/LocalClient.h +++ b/src/network/LocalClient.h @@ -17,6 +17,7 @@ namespace ZL { bool IsConnected() const override { return true; } int GetClientId() const { return 1; } + std::vector getPendingProjectiles(); std::unordered_map getRemotePlayers() override { return std::unordered_map(); diff --git a/src/network/NetworkInterface.h b/src/network/NetworkInterface.h index d2c513d..6bb64be 100644 --- a/src/network/NetworkInterface.h +++ b/src/network/NetworkInterface.h @@ -7,6 +7,13 @@ // NetworkInterface.h - »нтерфейс дл¤ разных типов соединений namespace ZL { + struct ProjectileInfo { + int shooterId = -1; + uint64_t clientTime = 0; + Eigen::Vector3f position = Eigen::Vector3f::Zero(); + Eigen::Vector3f direction = Eigen::Vector3f::Zero(); + }; + class INetworkClient { public: virtual ~INetworkClient() = default; @@ -17,5 +24,7 @@ namespace ZL { virtual std::unordered_map getRemotePlayers() = 0; virtual std::vector> getServerBoxes() = 0; + + virtual std::vector getPendingProjectiles() = 0; }; } diff --git a/src/network/WebSocketClient.cpp b/src/network/WebSocketClient.cpp index c0aa195..047066b 100644 --- a/src/network/WebSocketClient.cpp +++ b/src/network/WebSocketClient.cpp @@ -6,206 +6,239 @@ // Вспомогательный 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; + std::vector 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) { - try { - boost::asio::ip::tcp::resolver resolver(ioc_); - auto const results = resolver.resolve(host, std::to_string(port)); + void WebSocketClient::Connect(const std::string& host, uint16_t port) { + try { + boost::asio::ip::tcp::resolver resolver(ioc_); + auto const results = resolver.resolve(host, std::to_string(port)); - ws_ = std::make_unique>(ioc_); + ws_ = std::make_unique>(ioc_); - // Выполняем синхронный коннект и handshake для простоты старта - boost::beast::get_lowest_layer(*ws_).connect(results); - ws_->handshake(host, "/"); + // Выполняем синхронный коннект и handshake для простоты старта + boost::beast::get_lowest_layer(*ws_).connect(results); + ws_->handshake(host, "/"); - connected = true; + connected = true; - // Запускаем асинхронное чтение в пуле потоков TaskManager - startAsyncRead(); + // Запускаем асинхронное чтение в пуле потоков TaskManager + startAsyncRead(); - } - catch (std::exception& e) { - std::cerr << "Network Error: " << e.what() << std::endl; - } - } + } + catch (std::exception& e) { + std::cerr << "Network Error: " << e.what() << std::endl; + } + } - void WebSocketClient::startAsyncRead() { - ws_->async_read(buffer_, [this](boost::beast::error_code ec, std::size_t bytes) { - if (!ec) { - std::string msg = boost::beast::buffers_to_string(buffer_.data()); - buffer_.consume(bytes); - processIncomingMessage(msg); - startAsyncRead(); - } - else { - connected = false; - } - }); - } + void WebSocketClient::startAsyncRead() { + ws_->async_read(buffer_, [this](boost::beast::error_code ec, std::size_t bytes) { + if (!ec) { + std::string msg = boost::beast::buffers_to_string(buffer_.data()); + buffer_.consume(bytes); + processIncomingMessage(msg); + startAsyncRead(); + } + else { + connected = false; + } + }); + } - void WebSocketClient::processIncomingMessage(const std::string& msg) { - // Логика парсинга... - if (msg.rfind("ID:", 0) == 0) { - clientId = std::stoi(msg.substr(3)); - } + void WebSocketClient::processIncomingMessage(const std::string& msg) { + // Логика парсинга... + if (msg.rfind("ID:", 0) == 0) { + clientId = std::stoi(msg.substr(3)); + } - // Безопасно кладем в очередь для главного потока - std::lock_guard lock(queueMutex); - messageQueue.push(msg); - } + // Безопасно кладем в очередь для главного потока + std::lock_guard lock(queueMutex); + messageQueue.push(msg); + } - void WebSocketClient::Poll() { - std::lock_guard lock(queueMutex); + std::vector WebSocketClient::getPendingProjectiles() { + std::lock_guard lock(projMutex_); + auto copy = pendingProjectiles_; + pendingProjectiles_.clear(); + return copy; + } - while (!messageQueue.empty()) { + void WebSocketClient::Poll() { + std::lock_guard lock(queueMutex); - auto nowTime = std::chrono::system_clock::now(); + while (!messageQueue.empty()) { - //Apply server delay: - nowTime -= std::chrono::milliseconds(CLIENT_DELAY); + auto nowTime = std::chrono::system_clock::now(); - auto now_ms = std::chrono::duration_cast( - nowTime.time_since_epoch() - ).count(); + //Apply server delay: + nowTime -= std::chrono::milliseconds(CLIENT_DELAY); + + auto now_ms = std::chrono::duration_cast( + nowTime.time_since_epoch() + ).count(); - std::string msg = messageQueue.front(); - messageQueue.pop(); + std::string msg = messageQueue.front(); + messageQueue.pop(); - // Обработка списка коробок от сервера - if (msg.rfind("BOXES:", 0) == 0) { - std::string payload = msg.substr(6); // после "BOXES:" - std::vector> 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 bLock(boxesMutex); - serverBoxes_ = std::move(parsedBoxes); - } - continue; - } + // Обработка списка коробок от сервера + if (msg.rfind("BOXES:", 0) == 0) { + std::string payload = msg.substr(6); // после "BOXES:" + std::vector> 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 bLock(boxesMutex); + serverBoxes_ = std::move(parsedBoxes); + } + continue; + } - if (msg.rfind("EVENT:", 0) == 0) { - auto parts = split(msg, ':'); - if (parts.size() < 5) continue; // EVENT:ID:TYPE:TIME:DATA... + if (msg.rfind("PROJECTILE:", 0) == 0) { + auto parts = split(msg, ':'); + if (parts.size() >= 9) { + 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]) + ); + pi.direction = Eigen::Vector3f( + std::stof(parts[6]), + std::stof(parts[7]), + std::stof(parts[8]) + ); + std::lock_guard pl(projMutex_); + pendingProjectiles_.push_back(pi); + } + catch (...) { + } + } + continue; + } - int remoteId = std::stoi(parts[1]); - std::string subType = parts[2]; - uint64_t sentTime = std::stoull(parts[3]); + if (msg.rfind("EVENT:", 0) == 0) { + auto parts = split(msg, ':'); + if (parts.size() < 5) continue; // EVENT:ID:TYPE:TIME:DATA... - ClientState remoteState; + 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::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(); + std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast(std::chrono::milliseconds(sentTime)) }; + remoteState.lastUpdateServerTime = uptime_timepoint; - 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 - { + 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 pLock(playersMutex); + { + std::lock_guard pLock(playersMutex); auto& rp = remotePlayers[remoteId]; - rp.add_state(remoteState); - } - } + rp.add_state(remoteState); + } + } - } - } - void WebSocketClient::Send(const std::string& message) { - if (!connected) return; + } + } + void WebSocketClient::Send(const std::string& message) { + if (!connected) return; - auto ss = std::make_shared(message); + auto ss = std::make_shared(message); - std::lock_guard lock(writeMutex_); - writeQueue_.push(ss); + std::lock_guard lock(writeMutex_); + writeQueue_.push(ss); - // Если сейчас ничего не записывается, инициируем первую запись - if (!isWriting_) { - doWrite(); - } - } + // Если сейчас ничего не записывается, инициируем первую запись + if (!isWriting_) { + doWrite(); + } + } - void WebSocketClient::doWrite() { - // Эта функция всегда вызывается под мьютексом или из колбэка - if (writeQueue_.empty()) { - isWriting_ = false; - return; - } + void WebSocketClient::doWrite() { + // Эта функция всегда вызывается под мьютексом или из колбэка + if (writeQueue_.empty()) { + isWriting_ = false; + return; + } - isWriting_ = true; - auto message = writeQueue_.front(); + 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; - } + // Захватываем 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(); // Проверяем следующее - } - ); - } + std::lock_guard lock(writeMutex_); + writeQueue_.pop(); // Удаляем отправленное сообщение + doWrite(); // Проверяем следующее + } + ); + } } #endif diff --git a/src/network/WebSocketClient.h b/src/network/WebSocketClient.h index 0692d67..093e008 100644 --- a/src/network/WebSocketClient.h +++ b/src/network/WebSocketClient.h @@ -38,6 +38,9 @@ namespace ZL { std::vector> serverBoxes_; std::mutex boxesMutex; + std::vector pendingProjectiles_; + std::mutex projMutex_; + void startAsyncRead(); void processIncomingMessage(const std::string& msg); @@ -63,6 +66,8 @@ namespace ZL { std::lock_guard lock(boxesMutex); return serverBoxes_; } + + std::vector getPendingProjectiles() override; }; } #endif From 50232c081640474914d33eb00b77dceac6ec1070 Mon Sep 17 00:00:00 2001 From: Vlad Date: Sat, 31 Jan 2026 16:44:00 +0600 Subject: [PATCH 3/4] fix shooting --- server/server.cpp | 4 +++- src/Game.cpp | 25 +++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/server/server.cpp b/server/server.cpp index 1dd04fa..463d04b 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -120,7 +120,9 @@ class Session : public std::enable_shared_from_this { std::cout << "Player " << id_ << " fired " << shotCount << " shots → broadcasting\n"; for (auto& session : g_sessions) { - session->send_message(broadcast); + if (session->get_id() != id_) { + session->send_message(broadcast); + } } } else { diff --git a/src/Game.cpp b/src/Game.cpp index 4d875ce..bd0778d 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -233,6 +233,8 @@ namespace ZL if (now - lastProjectileFireTime >= static_cast(projectileCooldownMs)) { lastProjectileFireTime = now; + this->fireProjectiles(); + Eigen::Vector3f localForward = { 0, 0, -1 }; Eigen::Vector3f worldForward = (Environment::shipState.rotation * localForward).normalized(); @@ -1130,15 +1132,29 @@ namespace ZL const float lifeMs = 5000.0f; const float size = 0.5f; + auto remotePlayersSnapshot = networkClient->getRemotePlayers(); for (const auto& pi : pending) { Eigen::Vector3f dir = pi.direction; float len = dir.norm(); if (len <= 1e-6f) continue; dir /= len; - Eigen::Vector3f baseVel = dir * projectileSpeed; - int shotCount = 1; - shotCount = 2; + Eigen::Matrix3f shooterRot = Eigen::Matrix3f::Identity(); + float shooterVel = 0.0f; + auto it = remotePlayersSnapshot.find(pi.shooterId); + if (it != remotePlayersSnapshot.end()) { + std::chrono::system_clock::time_point pktTime{ std::chrono::milliseconds(pi.clientTime) }; + if (it->second.canFetchClientStateAtTime(pktTime)) { + ClientState shooterState = it->second.fetchClientStateAtTime(pktTime); + shooterRot = shooterState.rotation; + shooterVel = shooterState.velocity; + } + } + + float speedWithOwner = projectileSpeed + shooterVel; + Eigen::Vector3f baseVel = dir * speedWithOwner; + + int shotCount = 2; std::vector localOffsets = { {-1.5f, 0.9f, 5.0f}, @@ -1146,7 +1162,8 @@ namespace ZL }; for (int i = 0; i < shotCount; ++i) { - Eigen::Vector3f shotPos = pi.position + localOffsets[i]; + Eigen::Vector3f rotatedOffset = shooterRot * localOffsets[i]; + Eigen::Vector3f shotPos = pi.position + rotatedOffset; for (auto& p : projectiles) { if (!p->isActive()) { From 204b79bf06e19ab694a0dc837444c9c6fdb194c8 Mon Sep 17 00:00:00 2001 From: Vlad Date: Sat, 31 Jan 2026 19:25:40 +0600 Subject: [PATCH 4/4] add main menu --- resources/config/main_menu.json | 128 ++++++++++++++++++++ resources/main_menu/exit.png | 3 + resources/main_menu/lang.png | 3 + resources/main_menu/line.png | 3 + resources/main_menu/multi.png | 3 + resources/main_menu/single.png | 3 + resources/main_menu/subtitle.png | 3 + resources/main_menu/title.png | 3 + resources/main_menu/version.png | 3 + src/Game.cpp | 200 +++++++++++++++++-------------- 10 files changed, 261 insertions(+), 91 deletions(-) create mode 100644 resources/config/main_menu.json create mode 100644 resources/main_menu/exit.png create mode 100644 resources/main_menu/lang.png create mode 100644 resources/main_menu/line.png create mode 100644 resources/main_menu/multi.png create mode 100644 resources/main_menu/single.png create mode 100644 resources/main_menu/subtitle.png create mode 100644 resources/main_menu/title.png create mode 100644 resources/main_menu/version.png diff --git a/resources/config/main_menu.json b/resources/config/main_menu.json new file mode 100644 index 0000000..9677e48 --- /dev/null +++ b/resources/config/main_menu.json @@ -0,0 +1,128 @@ +{ + "root": { + "type": "FrameLayout", + "x": 0, + "y": 0, + "width": 1280, + "height": 720, + "children": [ + { + "type": "LinearLayout", + "name": "settingsButtons", + "orientation": "vertical", + "spacing": 10, + "x": 0, + "y": 0, + "width": 300, + "height": 300, + "children": [ + { + "type": "Button", + "name": "langButton", + "x": 1100, + "y": 580, + "width": 142, + "height": 96, + "textures": { + "normal": "resources/main_menu/lang.png", + "hover": "resources/main_menu/lang.png", + "pressed": "resources/main_menu/lang.png" + } + }, + { + "type": "Button", + "name": "titleBtn", + "x": 473, + "y": 500, + "width": 254, + "height": 35, + "textures": { + "normal": "resources/main_menu/title.png", + "hover": "resources/main_menu/title.png", + "pressed": "resources/main_menu/title.png" + } + }, + { + "type": "Button", + "name": "underlineBtn", + "x": 516, + "y": 465, + "width": 168, + "height": 44, + "textures": { + "normal": "resources/main_menu/line.png", + "hover": "resources/main_menu/line.png", + "pressed": "resources/main_menu/line.png" + } + }, + { + "type": "Button", + "name": "subtitleBtn", + "x": 528, + "y": 455, + "width": 144, + "height": 11, + "textures": { + "normal": "resources/main_menu/subtitle.png", + "hover": "resources/main_menu/subtitle.png", + "pressed": "resources/main_menu/subtitle.png" + } + }, + { + "type": "Button", + "name": "singleButton", + "x": 409, + "y": 360, + "width": 382, + "height": 56, + "textures": { + "normal": "resources/main_menu/single.png", + "hover": "resources/main_menu/single.png", + "pressed": "resources/main_menu/single.png" + } + }, + { + "type": "Button", + "name": "multiplayerButton", + "x": 409, + "y": 289, + "width": 382, + "height": 56, + "textures": { + "normal": "resources/main_menu/multi.png", + "hover": "resources/main_menu/multi.png", + "pressed": "resources/main_menu/multi.png" + } + }, + { + "type": "Button", + "name": "exitButton", + "x": 409, + "y": 218, + "width": 382, + "height": 56, + "textures": { + "normal": "resources/main_menu/exit.png", + "hover": "resources/main_menu/exit.png", + "pressed": "resources/main_menu/exit.png" + } + }, + { + "type": "Button", + "name": "versionLabel", + "x": 559.5, + "y": 170, + "width": 81, + "height": 9, + "textures": { + "normal": "resources/main_menu/version.png", + "hover": "resources/main_menu/version.png", + "pressed": "resources/main_menu/version.png" + } + } + ] + } + ] + } + } + \ No newline at end of file diff --git a/resources/main_menu/exit.png b/resources/main_menu/exit.png new file mode 100644 index 0000000..d2e10c7 --- /dev/null +++ b/resources/main_menu/exit.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd2def6829ad74a9a6f466259f56001261161e44e04d54452b20537f02100510 +size 1705 diff --git a/resources/main_menu/lang.png b/resources/main_menu/lang.png new file mode 100644 index 0000000..6ab2a9d --- /dev/null +++ b/resources/main_menu/lang.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:063b260ec102139ba2afe878ceff30343c1d3debe82cfed2bf80c4681d9f97e1 +size 9811 diff --git a/resources/main_menu/line.png b/resources/main_menu/line.png new file mode 100644 index 0000000..73e47a5 --- /dev/null +++ b/resources/main_menu/line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:268a7cb9f878a44b0e170655f6ef88ef6ba55e422458a87e05a16a121a5ed2f3 +size 5774 diff --git a/resources/main_menu/multi.png b/resources/main_menu/multi.png new file mode 100644 index 0000000..8ed569b --- /dev/null +++ b/resources/main_menu/multi.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44549cc8ac38f4ee24ebf8feedd11a5c0dfb156ad1f65c84d290980d5b0f44fb +size 1909 diff --git a/resources/main_menu/single.png b/resources/main_menu/single.png new file mode 100644 index 0000000..ee1595e --- /dev/null +++ b/resources/main_menu/single.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34926668c8f681d6fa9527343bf47169f0f6683a281591ee0d8106b65ad2e758 +size 2036 diff --git a/resources/main_menu/subtitle.png b/resources/main_menu/subtitle.png new file mode 100644 index 0000000..4955406 --- /dev/null +++ b/resources/main_menu/subtitle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef4a84b8462285ee4038b85b5474059c5dc77b114a93e35ce09dd71f74c9ed60 +size 1652 diff --git a/resources/main_menu/title.png b/resources/main_menu/title.png new file mode 100644 index 0000000..81d446a --- /dev/null +++ b/resources/main_menu/title.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efaa78ed8e564bc34e0f2d154f92b5a0e4cc18da733db729925967d1b1c4e482 +size 2023 diff --git a/resources/main_menu/version.png b/resources/main_menu/version.png new file mode 100644 index 0000000..daaa9ce --- /dev/null +++ b/resources/main_menu/version.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9570c36786c210f39fc6e44a428b7fbeecfa91d564b8982e9cc479aa8a2d545f +size 1157 diff --git a/src/Game.cpp b/src/Game.cpp index bd0778d..2e7aa41 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #ifdef __ANDROID__ #include #endif @@ -164,102 +165,119 @@ namespace ZL bool explosionCfgLoaded = explosionEmitter.loadFromJsonFile("resources/config/explosion_config.json", renderer, CONST_ZIP_FILE); explosionEmitter.setEmissionPoints(std::vector()); projectileEmitter.setEmissionPoints(std::vector()); - uiManager.loadFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE); - uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); - static bool isExitButtonAnimating = false; - uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() { - std::cerr << "Settings button animation finished -> переход в настройки" << std::endl; - if (uiManager.pushMenuFromFile("resources/config/settings.json", this->renderer, CONST_ZIP_FILE)) { - uiManager.setButtonCallback("Opt1", [this](const std::string& n) { - std::cerr << "Opt1 pressed: " << n << std::endl; - }); - uiManager.setButtonCallback("Opt2", [this](const std::string& n) { - std::cerr << "Opt2 pressed: " << n << std::endl; - }); - uiManager.setButtonCallback("backButton", [this](const std::string& n) { - uiManager.stopAllAnimations(); - uiManager.popMenu(); - }); - } - else { - std::cerr << "Failed to open settings menu after animations" << std::endl; - } - }); + uiManager.loadFromFile("resources/config/main_menu.json", renderer, CONST_ZIP_FILE); + std::function loadGameplayUI; + loadGameplayUI = [this]() { + uiManager.loadFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE); - uiManager.setAnimationCallback("exitButton", "bgScroll", []() { - std::cerr << "Exit button bgScroll animation finished" << std::endl; - g_exitBgAnimating = false; - }); - - // Set UI button callbacks - uiManager.setButtonCallback("playButton", [this](const std::string& name) { - std::cerr << "Play button pressed: " << name << std::endl; - - }); - - uiManager.setButtonCallback("settingsButton", [this](const std::string& name) { - std::cerr << "Settings button pressed: " << name << std::endl; - uiManager.startAnimationOnNode("playButton", "buttonsExit"); - uiManager.startAnimationOnNode("settingsButton", "buttonsExit"); - uiManager.startAnimationOnNode("exitButton", "buttonsExit"); - }); - - uiManager.setButtonCallback("exitButton", [this](const std::string& name) { - std::cerr << "Exit button pressed: " << name << std::endl; - - if (!g_exitBgAnimating) { - std::cerr << "start repeat anim bgScroll on exitButton" << std::endl; - g_exitBgAnimating = true; - uiManager.startAnimationOnNode("exitButton", "bgScroll"); - } - else { - std::cerr << "stop repeat anim bgScroll on exitButton" << std::endl; - g_exitBgAnimating = false; - uiManager.stopAnimationOnNode("exitButton", "bgScroll"); - - auto exitButton = uiManager.findButton("exitButton"); - if (exitButton) { - exitButton->animOffsetX = 0.0f; - exitButton->animOffsetY = 0.0f; - exitButton->animScaleX = 1.0f; - exitButton->animScaleY = 1.0f; - exitButton->buildMesh(); + uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); + static bool isExitButtonAnimating = false; + uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() { + std::cerr << "Settings button animation finished -> переход в настройки" << std::endl; + if (uiManager.pushMenuFromFile("resources/config/settings.json", this->renderer, CONST_ZIP_FILE)) { + uiManager.setButtonCallback("Opt1", [this](const std::string& n) { + std::cerr << "Opt1 pressed: " << n << std::endl; + }); + uiManager.setButtonCallback("Opt2", [this](const std::string& n) { + std::cerr << "Opt2 pressed: " << n << std::endl; + }); + uiManager.setButtonCallback("backButton", [this](const std::string& n) { + uiManager.stopAllAnimations(); + uiManager.popMenu(); + }); } - } + else { + std::cerr << "Failed to open settings menu after animations" << std::endl; + } + }); + + uiManager.setAnimationCallback("exitButton", "bgScroll", []() { + std::cerr << "Exit button bgScroll animation finished" << std::endl; + g_exitBgAnimating = false; + }); + + uiManager.setButtonCallback("playButton", [this](const std::string& name) { + std::cerr << "Play button pressed: " << name << std::endl; + }); + + uiManager.setButtonCallback("settingsButton", [this](const std::string& name) { + std::cerr << "Settings button pressed: " << name << std::endl; + uiManager.startAnimationOnNode("playButton", "buttonsExit"); + uiManager.startAnimationOnNode("settingsButton", "buttonsExit"); + uiManager.startAnimationOnNode("exitButton", "buttonsExit"); + }); + + uiManager.setButtonCallback("exitButton", [this](const std::string& name) { + std::cerr << "Exit button pressed: " << name << std::endl; + + if (!g_exitBgAnimating) { + std::cerr << "start repeat anim bgScroll on exitButton" << std::endl; + g_exitBgAnimating = true; + uiManager.startAnimationOnNode("exitButton", "bgScroll"); + } + else { + std::cerr << "stop repeat anim bgScroll on exitButton" << std::endl; + g_exitBgAnimating = false; + uiManager.stopAnimationOnNode("exitButton", "bgScroll"); + + auto exitButton = uiManager.findButton("exitButton"); + if (exitButton) { + exitButton->animOffsetX = 0.0f; + exitButton->animOffsetY = 0.0f; + exitButton->animScaleX = 1.0f; + exitButton->animScaleY = 1.0f; + exitButton->buildMesh(); + } + } + }); + + uiManager.setButtonCallback("shootButton", [this](const std::string& name) { + uint64_t now = SDL_GetTicks64(); + if (now - lastProjectileFireTime >= static_cast(projectileCooldownMs)) { + lastProjectileFireTime = now; + + this->fireProjectiles(); + + Eigen::Vector3f localForward = { 0, 0, -1 }; + Eigen::Vector3f worldForward = (Environment::shipState.rotation * localForward).normalized(); + + Eigen::Vector3f centerPos = Environment::shipState.position + + Environment::shipState.rotation * Vector3f{ 0, 0.9f, 5.0f }; + + std::string fireMsg = "FIRE:" + + std::to_string(now) + ":" + + std::to_string(centerPos.x()) + ":" + + std::to_string(centerPos.y()) + ":" + + std::to_string(centerPos.z()) + ":" + + std::to_string(worldForward.x()) + ":" + + std::to_string(worldForward.y()) + ":" + + std::to_string(worldForward.z()) + ":" + + "2"; + + networkClient->Send(fireMsg); + } + }); + + uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) { + int newVel = roundf(value * 10); + if (newVel != Environment::shipState.selectedVelocity) { + newShipVelocity = newVel; + } + }); + }; + + uiManager.setButtonCallback("singleButton", [loadGameplayUI](const std::string& name) { + std::cerr << "Single button pressed: " << name << " -> load gameplay UI\n"; + loadGameplayUI(); }); - uiManager.setButtonCallback("shootButton", [this](const std::string& name) { - uint64_t now = SDL_GetTicks64(); - if (now - lastProjectileFireTime >= static_cast(projectileCooldownMs)) { - lastProjectileFireTime = now; - - this->fireProjectiles(); - - Eigen::Vector3f localForward = { 0, 0, -1 }; - Eigen::Vector3f worldForward = (Environment::shipState.rotation * localForward).normalized(); - - Eigen::Vector3f centerPos = Environment::shipState.position + - Environment::shipState.rotation * Vector3f{ 0, 0.9f, 5.0f }; - - std::string fireMsg = "FIRE:" + - std::to_string(now) + ":" + - std::to_string(centerPos.x()) + ":" + - std::to_string(centerPos.y()) + ":" + - std::to_string(centerPos.z()) + ":" + - std::to_string(worldForward.x()) + ":" + - std::to_string(worldForward.y()) + ":" + - std::to_string(worldForward.z()) + ":" + - "2"; - - networkClient->Send(fireMsg); - } + uiManager.setButtonCallback("multiplayerButton", [loadGameplayUI](const std::string& name) { + std::cerr << "Multiplayer button pressed: " << name << " -> load gameplay UI\n"; + loadGameplayUI(); }); - - uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) { - int newVel = roundf(value * 10); - if (newVel != Environment::shipState.selectedVelocity) { - newShipVelocity = newVel; - } + uiManager.setButtonCallback("exitButton", [](const std::string& name) { + std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n"; + Environment::exitGameLoop = true; }); cubemapTexture = std::make_shared(