From fa3a4c66c9f1aac36120b4495f2ee286c4c03f1d Mon Sep 17 00:00:00 2001 From: vottozi Date: Mon, 23 Feb 2026 14:43:23 +0600 Subject: [PATCH 01/20] add lead indicator --- src/Space.cpp | 170 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/Space.h | 3 + 2 files changed, 169 insertions(+), 4 deletions(-) diff --git a/src/Space.cpp b/src/Space.cpp index f69e7d0..acff0dd 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -696,13 +696,15 @@ namespace ZL int Space::pickTargetId() const { int bestId = -1; - constexpr float INF_F = 1e30f; - float bestDistSq = INF_F; + float bestDistSq = 1e30f; for (auto const& [id, st] : remotePlayerStates) { if (deadRemotePlayers.count(id)) continue; float d2 = (Environment::shipState.position - st.position).squaredNorm(); + + if (d2 > TARGET_MAX_DIST_SQ) continue; // слишком далеко + if (d2 < bestDistSq) { bestDistSq = d2; bestId = id; @@ -711,6 +713,95 @@ namespace ZL return bestId; } + static Vector3f ForwardFromRotation(const Matrix3f& rot) + { + Vector3f localForward(0, 0, -1); + Vector3f worldForward = rot * localForward; + float len = worldForward.norm(); + if (len > 1e-6f) worldForward /= len; + return worldForward; + } + + static bool SolveLeadInterceptTime( + const Vector3f& shooterPos, + const Vector3f& shooterVel, + const Vector3f& targetPos, + const Vector3f& targetVel, + float projectileSpeed, // muzzle speed (например 60) + float& outT) + { + Vector3f r = targetPos - shooterPos; + Vector3f v = targetVel - shooterVel; + float S = projectileSpeed; + + float a = v.dot(v) - S * S; + float b = 2.0f * r.dot(v); + float c = r.dot(r); + + // Если a почти 0 -> линейный случай + if (std::abs(a) < 1e-6f) { + if (std::abs(b) < 1e-6f) return false; // нет решения + float t = -c / b; + if (t > 0.0f) { outT = t; return true; } + return false; + } + + float disc = b * b - 4.0f * a * c; + if (disc < 0.0f) return false; + + float sqrtDisc = std::sqrt(disc); + float t1 = (-b - sqrtDisc) / (2.0f * a); + float t2 = (-b + sqrtDisc) / (2.0f * a); + + float t = 1e30f; + if (t1 > 0.0f) t = min(t, t1); + if (t2 > 0.0f) t = min(t, t2); + + if (t >= 1e29f) return false; + outT = t; + return true; + } + + static VertexDataStruct MakeRing2D( + float cx, float cy, + float innerR, float outerR, + float z, + int segments, + const Eigen::Vector4f& rgba) + { + VertexDataStruct v; + v.PositionData.reserve(segments * 6); + v.ColorData.reserve(segments * 6); + + Vector3f rgb(rgba.x(), rgba.y(), rgba.z()); + + const float twoPi = 6.28318530718f; + for (int i = 0; i < segments; ++i) { + float a0 = twoPi * (float)i / (float)segments; + float a1 = twoPi * (float)(i + 1) / (float)segments; + + float c0 = std::cos(a0), s0 = std::sin(a0); + float c1 = std::cos(a1), s1 = std::sin(a1); + + Vector3f p0i(cx + innerR * c0, cy + innerR * s0, z); + Vector3f p0o(cx + outerR * c0, cy + outerR * s0, z); + Vector3f p1i(cx + innerR * c1, cy + innerR * s1, z); + Vector3f p1o(cx + outerR * c1, cy + outerR * s1, z); + + // два треугольника (p0i,p0o,p1o) и (p0i,p1o,p1i) + v.PositionData.push_back(p0i); + v.PositionData.push_back(p0o); + v.PositionData.push_back(p1o); + + v.PositionData.push_back(p0i); + v.PositionData.push_back(p1o); + v.PositionData.push_back(p1i); + + for (int k = 0; k < 6; ++k) v.ColorData.push_back(rgb); + } + return v; + } + static VertexDataStruct MakeColoredRect2D(float cx, float cy, float hw, float hh, float z, const Eigen::Vector4f& rgba) { @@ -756,6 +847,54 @@ namespace ZL const ClientState& st = remotePlayerStates.at(trackedTargetId); Vector3f shipWorld = st.position; + // Lead Indicator + // скорость пули (как в fireProjectiles) + const float projectileSpeed = 60.0f; + + // позиция вылета + Vector3f shooterPos = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0f, 0.9f - 6.0f, 5.0f }; + + // скорость цели в мире (вектор) + Vector3f shooterVel = ForwardFromRotation(Environment::shipState.rotation) * Environment::shipState.velocity; + Vector3f targetVel = ForwardFromRotation(st.rotation) * st.velocity; + + // условие "если враг не движется — круг не рисуем" + const float minTargetSpeed = 0.5f; // подобрать (в твоих единицах) + bool targetMoving = (targetVel.norm() > minTargetSpeed); + + Vector3f leadWorld = shipWorld; + bool haveLead = false; + + //if (targetMoving) { + // float tLead = 0.0f; + // if (SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead)) { + // // ограничим случаи, чтобы круг не улетал далеко + // if (tLead > 0.0f && tLead < 8.0f) { + // // подобрать максимум (сек) + // leadWorld = shipWorld + targetVel * tLead; + // haveLead = true; + // } + // } + //} + + if (targetMoving) { + float tLead = 0.0f; + float distToTarget = (Environment::shipState.position - shipWorld).norm(); + + const float leadMaxDist = 2500.0f; // максимум + float allowedDist = min(distToTarget, leadMaxDist); + + // + небольшой запас 10–20% чтобы не моргало на границе + const float maxLeadTime = (allowedDist / projectileSpeed) * 1.2f; + + if (SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead)) { + if (tLead > 0.0f && tLead < maxLeadTime) { + leadWorld = shipWorld + targetVel * tLead; + haveLead = true; + } + } + } + // 2) проекция float ndcX, ndcY, ndcZ, clipW; if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return; @@ -774,7 +913,7 @@ namespace ZL // time for arrow bob float t = static_cast(SDL_GetTicks64()) * 0.001f; - // 4) Настройки стиля (как X3) + // 4) Настройки стиля Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный float thickness = 2.0f; // толщина линий (px) float z = 0.0f; // 2D слой @@ -826,6 +965,29 @@ namespace ZL renderer.PushMatrix(); renderer.LoadIdentity(); + // рисуем кружок упреждения (только если есть решение) + if (haveLead) { + float leadNdcX, leadNdcY, leadNdcZ, leadClipW; + if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) { + if (leadNdcX >= -1 && leadNdcX <= 1 && leadNdcY >= -1 && leadNdcY <= 1) { + float lx = (leadNdcX * 0.5f + 0.5f) * Environment::width; + float ly = (leadNdcY * 0.5f + 0.5f) * Environment::height; + + float distLead = (Environment::shipState.position - leadWorld).norm(); + float r = 30.0f / (distLead * 0.01f + 1.0f); + r = std::clamp(r, 6.0f, 18.0f); + + float thicknessPx = 2.5f; + float innerR = max(1.0f, r - thicknessPx); + float outerR = r + thicknessPx; + + VertexDataStruct ring = MakeRing2D(lx, ly, innerR, outerR, 0.0f, 32, enemyColor); + hudTempMesh.AssignFrom(ring); + renderer.DrawVertexRenderStruct(hudTempMesh); + } + } + } + // верх-лево: горизонт + вертикаль drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness); drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen); @@ -1255,7 +1417,7 @@ namespace ZL }; const float projectileSpeed = 60.0f; - const float lifeMs = 5000.0f; + const float lifeMs = 50000.0f; const float size = 0.5f; Vector3f localForward = { 0,0,-1 }; diff --git a/src/Space.h b/src/Space.h index b1f8fed..9e97eeb 100644 --- a/src/Space.h +++ b/src/Space.h @@ -120,6 +120,9 @@ namespace ZL { std::unordered_set deadRemotePlayers; + static constexpr float TARGET_MAX_DIST = 50000.0f; + static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST; + // --- Target HUD (brackets + offscreen arrow) --- int trackedTargetId = -1; bool targetWasVisible = false; From db4254e2058a6805e9565cfc0fc05dce1192a0f1 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Mon, 23 Feb 2026 11:45:55 +0300 Subject: [PATCH 02/20] working on web --- src/MenuManager.cpp | 4 +-- src/Space.cpp | 27 ++++++++++++++++--- src/network/WebSocketClientEmscripten.cpp | 4 +-- src/render/Renderer.cpp | 32 ++++++++++++++++++++--- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index e14bd56..752552c 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -103,10 +103,10 @@ namespace ZL { }); uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) { int newVel = roundf(value * 10); - /*if (newVel > 2) + if (newVel > 2) { newVel = 2; - }*/ + } if (newVel != Environment::shipState.selectedVelocity) { onVelocityChanged(newVel); diff --git a/src/Space.cpp b/src/Space.cpp index acff0dd..7dd4e16 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -816,11 +816,16 @@ namespace ZL // defaultColor shader likely uses vColor (vec3), но нам нужен alpha. // У тебя в Renderer есть RenderUniform4fv, но шейдер может брать vColor. - // Поэтому: сделаем ColorData vec3, а alpha дадим через uniform uColor, если есть. - // Если в defaultColor нет uniform uColor — тогда alpha будет 1.0. - // Для совместимости: кладём RGB, alpha будем задавать uniform'ом отдельно. + // Поэтому: сделаем ColorData vec3, а alpha будем задавать uniform'ом отдельно. Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() }; v.ColorData = { rgb, rgb, rgb, rgb, rgb, rgb }; + + // defaultColor vertex shader expects vNormal and vTexCoord; provide valid values + // so WebGL/GLSL doesn't get NaN from normalize(vec3(0,0,0)). + const Vector3f n{ 0.f, 0.f, 1.f }; + v.NormalData = { n, n, n, n, n, n }; + const Vector2f uv{ 0.f, 0.f }; + v.TexCoordData = { uv, uv, uv, uv, uv, uv }; return v; } @@ -915,7 +920,7 @@ namespace ZL // 4) Настройки стиля Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный - float thickness = 2.0f; // толщина линий (px) + float thickness = 10.0f; // толщина линий (px) float z = 0.0f; // 2D слой // 5) Если цель в кадре: рисуем скобки @@ -959,12 +964,14 @@ namespace ZL glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glClear(GL_DEPTH_BUFFER_BIT); renderer.shaderManager.PushShader("defaultColor"); renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); renderer.PushMatrix(); renderer.LoadIdentity(); +<<<<<<< Updated upstream // рисуем кружок упреждения (только если есть решение) if (haveLead) { float leadNdcX, leadNdcY, leadNdcZ, leadClipW; @@ -987,6 +994,10 @@ namespace ZL } } } +======= + renderer.EnableVertexAttribArray("vPosition"); + +>>>>>>> Stashed changes // верх-лево: горизонт + вертикаль drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness); @@ -1004,6 +1015,9 @@ namespace ZL drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness); drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen); + renderer.DisableVertexAttribArray("vPosition"); + + renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); @@ -1069,6 +1083,11 @@ namespace ZL v.PositionData = { a, b, c }; Vector3f rgb{ enemyColor.x(), enemyColor.y(), enemyColor.z() }; v.ColorData = { rgb, rgb, rgb }; + // defaultColor vertex shader expects vNormal and vTexCoord (avoids NaN on WebGL). + const Vector3f n{ 0.f, 0.f, 1.f }; + v.NormalData = { n, n, n }; + const Vector2f uv{ 0.f, 0.f }; + v.TexCoordData = { uv, uv, uv }; hudTempMesh.AssignFrom(v); renderer.DrawVertexRenderStruct(hudTempMesh); }; diff --git a/src/network/WebSocketClientEmscripten.cpp b/src/network/WebSocketClientEmscripten.cpp index 9534cf0..98044e9 100644 --- a/src/network/WebSocketClientEmscripten.cpp +++ b/src/network/WebSocketClientEmscripten.cpp @@ -7,8 +7,8 @@ namespace ZL { void WebSocketClientEmscripten::Connect(const std::string& host, uint16_t port) { // Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost - std::string url = "ws://" + host + ":" + std::to_string(port); - //std::string url = "wss://api.spacegame.fishrungames.com"; + //std::string url = "ws://" + host + ":" + std::to_string(port); + std::string url = "wss://api.spacegame.fishrungames.com"; EmscriptenWebSocketCreateAttributes attr = { url.c_str(), diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index a11ed19..721ae54 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -895,41 +895,65 @@ namespace ZL { static const std::string vColor("vColor"); static const std::string vTexCoord("vTexCoord"); static const std::string vPosition("vPosition"); - - //glBindVertexArray(VertexRenderStruct.vao->getBuffer()); - //Check if main thread, check if data is not empty... + // On WebGL (and when not using VAO), vertex attribute arrays must be explicitly + // enabled before drawing. Desktop with VAO can rely on stored state; WebGL cannot. if (VertexRenderStruct.data.NormalData.size() > 0) { glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.normalVBO->getBuffer()); VertexAttribPointer3fv(vNormal, 0, NULL); + EnableVertexAttribArray(vNormal); + } + else + { + DisableVertexAttribArray(vNormal); } if (VertexRenderStruct.data.TangentData.size() > 0) { glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.tangentVBO->getBuffer()); VertexAttribPointer3fv(vTangent, 0, NULL); + EnableVertexAttribArray(vTangent); + } + else + { + DisableVertexAttribArray(vTangent); } if (VertexRenderStruct.data.BinormalData.size() > 0) { glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.binormalVBO->getBuffer()); VertexAttribPointer3fv(vBinormal, 0, NULL); + EnableVertexAttribArray(vBinormal); + } + else + { + DisableVertexAttribArray(vBinormal); } if (VertexRenderStruct.data.ColorData.size() > 0) { glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.colorVBO->getBuffer()); VertexAttribPointer3fv(vColor, 0, NULL); + EnableVertexAttribArray(vColor); + } + else + { + DisableVertexAttribArray(vColor); } if (VertexRenderStruct.data.TexCoordData.size() > 0) { glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.texCoordVBO->getBuffer()); VertexAttribPointer2fv(vTexCoord, 0, NULL); + EnableVertexAttribArray(vTexCoord); + } + else + { + DisableVertexAttribArray(vTexCoord); } glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer()); VertexAttribPointer3fv(vPosition, 0, NULL); + EnableVertexAttribArray(vPosition); glDrawArrays(GL_TRIANGLES, 0, static_cast(VertexRenderStruct.data.PositionData.size())); - } void worldToScreenCoordinates(Vector3f objectPos, From 5cca2413fe52f12993c4681f5f1e1bb650e182b7 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Mon, 23 Feb 2026 11:46:57 +0300 Subject: [PATCH 03/20] changes --- src/Space.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Space.cpp b/src/Space.cpp index 7dd4e16..8483030 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -971,7 +971,8 @@ namespace ZL renderer.PushMatrix(); renderer.LoadIdentity(); -<<<<<<< Updated upstream + renderer.EnableVertexAttribArray("vPosition"); + // рисуем кружок упреждения (только если есть решение) if (haveLead) { float leadNdcX, leadNdcY, leadNdcZ, leadClipW; @@ -994,11 +995,8 @@ namespace ZL } } } -======= - renderer.EnableVertexAttribArray("vPosition"); - ->>>>>>> Stashed changes + // верх-лево: горизонт + вертикаль drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness); drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen); From cb2e8318e730958a9b24869379fc12215f326937 Mon Sep 17 00:00:00 2001 From: Vlad Date: Mon, 23 Feb 2026 15:29:05 +0600 Subject: [PATCH 04/20] added multi_menu --- resources/config/multiplayer_menu.json | 217 +++++++++++------- .../multiplayer_menu/AvailableServers.png | 3 + resources/multiplayer_menu/Backbutton.png | 3 + resources/multiplayer_menu/Button.png | 3 + resources/multiplayer_menu/Button2.png | 3 + resources/multiplayer_menu/Button3.png | 3 + resources/multiplayer_menu/Filledbuttons.png | 3 + resources/multiplayer_menu/JoinServer.png | 3 + resources/multiplayer_menu/ServerName.png | 3 + resources/multiplayer_menu/title.png | 3 + src/Game.cpp | 1 - src/UiManager.cpp | 18 +- 12 files changed, 174 insertions(+), 89 deletions(-) create mode 100644 resources/multiplayer_menu/AvailableServers.png create mode 100644 resources/multiplayer_menu/Backbutton.png create mode 100644 resources/multiplayer_menu/Button.png create mode 100644 resources/multiplayer_menu/Button2.png create mode 100644 resources/multiplayer_menu/Button3.png create mode 100644 resources/multiplayer_menu/Filledbuttons.png create mode 100644 resources/multiplayer_menu/JoinServer.png create mode 100644 resources/multiplayer_menu/ServerName.png create mode 100644 resources/multiplayer_menu/title.png diff --git a/resources/config/multiplayer_menu.json b/resources/config/multiplayer_menu.json index 17bf917..acbbe09 100644 --- a/resources/config/multiplayer_menu.json +++ b/resources/config/multiplayer_menu.json @@ -3,103 +3,156 @@ "type": "LinearLayout", "x": 0, "y": 0, - "width": 1920, - "height": 1080, - "orientation": "vertical", - "spacing": 20, + "width": 1280, + "height": 720, "children": [ { - "type": "TextView", - "name": "titleText", - "x": 300, - "y": 100, - "width": 1320, - "height": 100, - "text": "Multiplayer", - "fontPath": "resources/fonts/DroidSans.ttf", - "fontSize": 72, - "color": [1, 1, 1, 1], - "centered": true - }, - { - "type": "TextView", - "name": "serverLabel", - "x": 400, - "y": 250, - "width": 1120, - "height": 50, - "text": "Enter server name or IP:", - "fontPath": "resources/fonts/DroidSans.ttf", - "fontSize": 32, - "color": [1, 1, 1, 1], - "centered": false - }, + "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": 512, + "y": 500, + "width": 254, + "height": 35, + "textures": { + "normal": "resources/multiplayer_menu/title.png", + "hover": "resources/multiplayer_menu/title.png", + "pressed": "resources/multiplayer_menu/title.png" + } + }, + { + "type": "Button", + "name": "subtitle", + "x": 596.5, + "y": 470, + "width": 87, + "height": 11, + "textures": { + "normal": "resources/multiplayer_menu/JoinServer.png", + "hover": "resources/multiplayer_menu/JoinServer.png", + "pressed": "resources/multiplayer_menu/JoinServer.png" + } + }, + { + "type": "Button", + "name": "subtitleBtn", + "x": 450, + "y": 445, + "width": 94, + "height": 9, + "textures": { + "normal": "resources/multiplayer_menu/ServerName.png", + "hover": "resources/multiplayer_menu/ServerName.png", + "pressed": "resources/multiplayer_menu/ServerName.png" + } + }, { "type": "TextField", "name": "serverInputField", - "x": 400, - "y": 320, - "width": 1120, - "height": 60, + "x": 449, + "y": 390, + "width": 382, + "height": 56, "placeholder": "Enter server name or IP", "fontPath": "resources/fonts/DroidSans.ttf", - "fontSize": 28, + "fontSize": 16, "maxLength": 256, - "color": [1, 1, 1, 1], - "placeholderColor": [0.6, 0.6, 0.6, 1], - "backgroundColor": [0.15, 0.15, 0.15, 1], - "borderColor": [0.7, 0.7, 0.7, 1] + "color": [122, 156, 198, 1], + "placeholderColor": [122, 156, 198, 1], + "backgroundColor": [15, 29, 51, 1], + "borderColor": [15, 29, 51, 1] }, { - "type": "LinearLayout", - "x": 400, - "y": 450, - "width": 1120, - "height": 80, - "orientation": "horizontal", - "spacing": 30, - "children": [ - { - "type": "Button", - "name": "connectButton", - "x": 0, - "y": 0, - "width": 530, - "height": 80, - "textures": { - "normal": "resources/main_menu/single.png", - "hover": "resources/main_menu/single.png", - "pressed": "resources/main_menu/single.png" - } - }, + "type": "Button", + "name": "connectButton", + "x": 449, + "y": 350, + "width": 382, + "height": 56, + "textures": { + "normal": "resources/multiplayer_menu/Filledbuttons.png", + "hover": "resources/multiplayer_menu/Filledbuttons.png", + "pressed": "resources/multiplayer_menu/Filledbuttons.png" + } + }, + { "type": "Button", "name": "backButton", - "x": 590, - "y": 0, - "width": 530, - "height": 80, + "x": 449, + "y": 280, + "width": 382, + "height": 56, "textures": { - "normal": "resources/main_menu/exit.png", - "hover": "resources/main_menu/exit.png", - "pressed": "resources/main_menu/exit.png" + "normal": "resources/multiplayer_menu/Backbutton.png", + "hover": "resources/multiplayer_menu/Backbutton.png", + "pressed": "resources/multiplayer_menu/Backbutton.png" + } + }, + { + "type": "Button", + "name": "AvailableServers", + "x": 450, + "y": 240, + "width": 139, + "height": 9, + "textures": { + "normal": "resources/multiplayer_menu/AvailableServers.png", + "hover": "resources/multiplayer_menu/AvailableServers.png", + "pressed": "resources/multiplayer_menu/AvailableServers.png" + } + }, + { + "type": "Button", + "name": "SerButton", + "x": 436.5, + "y": 170, + "width": 407, + "height": 62, + "textures": { + "normal": "resources/multiplayer_menu/Button.png", + "hover": "resources/multiplayer_menu/Button.png", + "pressed": "resources/multiplayer_menu/Button.png" + } + }, + { + "type": "Button", + "name": "SerButton2", + "x": 436.5, + "y": 88, + "width": 407, + "height": 62, + "textures": { + "normal": "resources/multiplayer_menu/Button2.png", + "hover": "resources/multiplayer_menu/Button2.png", + "pressed": "resources/multiplayer_menu/Button2.png" + } + }, + { + "type": "Button", + "name": "SerButton3", + "x": 436.5, + "y": 6, + "width": 407, + "height": 62, + "textures": { + "normal": "resources/multiplayer_menu/Button3.png", + "hover": "resources/multiplayer_menu/Button3.png", + "pressed": "resources/multiplayer_menu/Button3.png" + } } - } ] - }, - { - "type": "TextView", - "name": "statusText", - "x": 400, - "y": 580, - "width": 1120, - "height": 50, - "text": "Ready to connect", - "fontPath": "resources/fonts/DroidSans.ttf", - "fontSize": 24, - "color": [0.8, 0.8, 0.8, 1], - "centered": false - } - ] } } \ No newline at end of file diff --git a/resources/multiplayer_menu/AvailableServers.png b/resources/multiplayer_menu/AvailableServers.png new file mode 100644 index 0000000..8a5759d --- /dev/null +++ b/resources/multiplayer_menu/AvailableServers.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6c6555671ec968eff10211ef2efd8ac097c52bf602b314d101f78c57f4a7059 +size 1823 diff --git a/resources/multiplayer_menu/Backbutton.png b/resources/multiplayer_menu/Backbutton.png new file mode 100644 index 0000000..9ee46af --- /dev/null +++ b/resources/multiplayer_menu/Backbutton.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0bcfe59a76449bd26a8558bbcf59a2bb06227d4ad99f5686a41ff567ca4b2bb +size 1814 diff --git a/resources/multiplayer_menu/Button.png b/resources/multiplayer_menu/Button.png new file mode 100644 index 0000000..a08abec --- /dev/null +++ b/resources/multiplayer_menu/Button.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e9ef4f2c9e8b45419915f4630e9ec3228388e6eff9c1f6fffd8a7762f0437c7 +size 3508 diff --git a/resources/multiplayer_menu/Button2.png b/resources/multiplayer_menu/Button2.png new file mode 100644 index 0000000..49b0468 --- /dev/null +++ b/resources/multiplayer_menu/Button2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b9dc0b621e0125c32d21cf2a941638fc758ababd52b933b3aa806d8fba2d2bb +size 3575 diff --git a/resources/multiplayer_menu/Button3.png b/resources/multiplayer_menu/Button3.png new file mode 100644 index 0000000..b7f3804 --- /dev/null +++ b/resources/multiplayer_menu/Button3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fcb8c3d841ab6e449adf66263e4bd27d80240f591e090e6047bc1206dd4d9b10 +size 3742 diff --git a/resources/multiplayer_menu/Filledbuttons.png b/resources/multiplayer_menu/Filledbuttons.png new file mode 100644 index 0000000..ae03c85 --- /dev/null +++ b/resources/multiplayer_menu/Filledbuttons.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb124591fa4995b4f958f082976d87cf3bc22b560dc790d0dac6d89c158c9a78 +size 2315 diff --git a/resources/multiplayer_menu/JoinServer.png b/resources/multiplayer_menu/JoinServer.png new file mode 100644 index 0000000..aec320d --- /dev/null +++ b/resources/multiplayer_menu/JoinServer.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66448d4bc67ad1cd0ed4ddd4e3671b17ffe97d11e59185d123b1b621badb6449 +size 1282 diff --git a/resources/multiplayer_menu/ServerName.png b/resources/multiplayer_menu/ServerName.png new file mode 100644 index 0000000..b8b0d63 --- /dev/null +++ b/resources/multiplayer_menu/ServerName.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8bd3c9e3fa4fff2c9f5cba2858e8a964be237406162ed7dd7046690a8543eb4 +size 1367 diff --git a/resources/multiplayer_menu/title.png b/resources/multiplayer_menu/title.png new file mode 100644 index 0000000..4702a8a --- /dev/null +++ b/resources/multiplayer_menu/title.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffcfc5e1d00f135259a94122d60c9524524b6f40d9cf7d26d17dd25b793ade40 +size 1830 diff --git a/src/Game.cpp b/src/Game.cpp index d6c28c0..a6981b9 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -109,7 +109,6 @@ namespace ZL spaceGameStarted = 1; }; - menuManager.onMultiplayerPressed = [this]() { #ifdef NETWORK #ifdef EMSCRIPTEN diff --git a/src/UiManager.cpp b/src/UiManager.cpp index d051f8c..647d72f 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -906,22 +906,28 @@ namespace ZL { } void UiManager::onMouseUp(int x, int y) { + std::vector> clicked; + for (auto& b : buttons) { + if (!b) continue; bool contains = b->rect.contains((float)x, (float)y); + if (b->state == ButtonState::Pressed) { if (contains && pressedButton == b) { - if (b->onClick) { - b->onClick(b->name); - } + clicked.push_back(b); } b->state = contains ? ButtonState::Hover : ButtonState::Normal; } } - pressedButton.reset(); - if (pressedSlider) { - pressedSlider.reset(); + for (auto& b : clicked) { + if (b->onClick) { + b->onClick(b->name); + } } + + pressedButton.reset(); + if (pressedSlider) pressedSlider.reset(); } void UiManager::onKeyPress(unsigned char key) { From ad7294ceea45ba990fe4ba2e73001a22bead86e2 Mon Sep 17 00:00:00 2001 From: Vlad Date: Tue, 24 Feb 2026 19:51:33 +0600 Subject: [PATCH 05/20] added random spaceship type --- src/Space.cpp | 28 +++++++++++++++++++++++--- src/Space.h | 5 ++++- src/network/ClientState.h | 39 +++++++++++++++++++------------------ src/network/LocalClient.cpp | 9 +++++++++ src/network/LocalClient.h | 1 + 5 files changed, 59 insertions(+), 23 deletions(-) diff --git a/src/Space.cpp b/src/Space.cpp index 8483030..f96e170 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -310,6 +310,21 @@ namespace ZL spaceship.AssignFrom(spaceshipBase); spaceship.RefreshVBO(); + // Load cargo + cargoTexture = std::make_shared(CreateTextureDataFromPng("resources/Cargo_Base_color_sRGB.png", CONST_ZIP_FILE)); + cargoBase = LoadFromTextFile02("resources/cargoship001.txt", CONST_ZIP_FILE); + auto quat = Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ())); + auto rotMatrix = quat.toRotationMatrix(); + cargoBase.RotateByMatrix(rotMatrix); + + auto quat2 = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI * 0.5, Eigen::Vector3f::UnitY())); + auto rotMatrix2 = quat2.toRotationMatrix(); + cargoBase.RotateByMatrix(rotMatrix2); + //cargoBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix()); + cargoBase.Move(Vector3f{ 1.2, 0, -5 }); + cargo.AssignFrom(cargoBase); + cargo.RefreshVBO(); + //Boxes boxTexture = std::make_unique(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE)); @@ -588,8 +603,8 @@ namespace ZL static_cast(Environment::width) / static_cast(Environment::height), Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); - // Биндим текстуру корабля один раз для всех удаленных игроков (оптимизация батчинга) - glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID()); + // Биндим текстуру корабля один раз для ?сех правильных игроков + // ?????????: ?????? ???????? ?????????? ?????? ????? ? ??????????? ?? ClientState.shipType // Если сервер прислал коробки, применяем их однократно вместо локальной генерации if (!serverBoxesApplied && networkClient) { @@ -633,7 +648,14 @@ namespace ZL // 3. Поворот врага renderer.RotateMatrix(playerState.rotation); - renderer.DrawVertexRenderStruct(spaceship); + if (playerState.shipType == 1 && cargoTexture) { + glBindTexture(GL_TEXTURE_2D, cargoTexture->getTexID()); + renderer.DrawVertexRenderStruct(cargo); + } + else { + glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID()); + renderer.DrawVertexRenderStruct(spaceship); + } renderer.PopMatrix(); } diff --git a/src/Space.h b/src/Space.h index 9e97eeb..36aed51 100644 --- a/src/Space.h +++ b/src/Space.h @@ -78,6 +78,9 @@ namespace ZL { VertexDataStruct spaceshipBase; VertexRenderStruct spaceship; + std::shared_ptr cargoTexture; + VertexDataStruct cargoBase; + VertexRenderStruct cargo; VertexRenderStruct cubemap; @@ -133,7 +136,7 @@ namespace ZL { // helpers void drawTargetHud(); // рисует рамку или стрелку - int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок) + int pickTargetId() const; // ???????? ???? (????: ????????? ????? ????????? ?????) void clearTextRendererCache(); }; diff --git a/src/network/ClientState.h b/src/network/ClientState.h index 7e48953..11c868c 100644 --- a/src/network/ClientState.h +++ b/src/network/ClientState.h @@ -13,12 +13,12 @@ constexpr auto NET_SECRET = "880b3713b9ff3e7a94b2712d54679e1f"; #define ENABLE_NETWORK_CHECKSUM constexpr float ANGULAR_ACCEL = 0.005f * 1000.0f; -constexpr float SHIP_ACCEL = 1.0f * 1000.0f; +constexpr float SHIP_ACCEL = 1.0f * 1000.0f; constexpr float ROTATION_SENSITIVITY = 0.002f; constexpr float PLANET_RADIUS = 20000.f; constexpr float PLANET_ALIGN_ZONE = 1.05f; -constexpr float PLANET_ANGULAR_ACCEL = 0.01f; // Подбери под динамику +constexpr float PLANET_ANGULAR_ACCEL = 0.01f; // ??????? ??? ???????? constexpr float PLANET_MAX_ANGULAR_VELOCITY = 10.f; constexpr float PITCH_LIMIT = static_cast(M_PI) / 9.f;//18.0f; @@ -29,35 +29,36 @@ constexpr long long CUTOFF_TIME = 5000; //ms uint32_t fnv1a_hash(const std::string& data); struct ClientState { - int id = 0; - 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; + int id = 0; + 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; - // Для расчета лага - std::chrono::system_clock::time_point lastUpdateServerTime; + int shipType = 0; + // ??? ??????? ???? + std::chrono::system_clock::time_point lastUpdateServerTime; - void simulate_physics(size_t delta); + void simulate_physics(size_t delta); - void apply_lag_compensation(std::chrono::system_clock::time_point nowTime); + void apply_lag_compensation(std::chrono::system_clock::time_point nowTime); - void handle_full_sync(const std::vector& parts, int startFrom); + void handle_full_sync(const std::vector& parts, int startFrom); - std::string formPingMessageContent(); + std::string formPingMessageContent(); }; struct ClientStateInterval { std::vector timedStates; - void add_state(const ClientState& state); + void add_state(const ClientState& state); - bool canFetchClientStateAtTime(std::chrono::system_clock::time_point targetTime) const; + bool canFetchClientStateAtTime(std::chrono::system_clock::time_point targetTime) const; - ClientState fetchClientStateAtTime(std::chrono::system_clock::time_point targetTime) const; + ClientState fetchClientStateAtTime(std::chrono::system_clock::time_point targetTime) const; }; diff --git a/src/network/LocalClient.cpp b/src/network/LocalClient.cpp index 5336b43..d7c08af 100644 --- a/src/network/LocalClient.cpp +++ b/src/network/LocalClient.cpp @@ -79,6 +79,10 @@ namespace ZL { void LocalClient::initializeNPCs() { npcs.clear(); + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution typeDistrib(0, 1); // 0 = default ship, 1 = cargo + for (int i = 0; i < 3; ++i) { LocalNPC npc; npc.id = 100 + i; @@ -91,6 +95,11 @@ namespace ZL { npc.currentState.discreteAngle = -1; npc.currentState.currentAngularVelocity = Eigen::Vector3f::Zero(); +// random + int shipType = typeDistrib(gen); + npc.shipType = shipType; + npc.currentState.shipType = shipType; + npc.targetPosition = generateRandomPosition(); npc.lastStateUpdateMs = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); diff --git a/src/network/LocalClient.h b/src/network/LocalClient.h index c8707a4..5227bf5 100644 --- a/src/network/LocalClient.h +++ b/src/network/LocalClient.h @@ -31,6 +31,7 @@ namespace ZL { Eigen::Vector3f targetPosition; uint64_t lastStateUpdateMs = 0; bool destroyed = false; + int shipType = 0; }; class LocalClient : public INetworkClient { From 5216965496be3e6b726d97d9ecba8252b15680df Mon Sep 17 00:00:00 2001 From: Vlad Date: Tue, 24 Feb 2026 20:34:01 +0600 Subject: [PATCH 06/20] added choosing spaceship in singleplay --- resources/config/ship_selection_menu.json | 64 +++++++++++++++++++++++ src/Game.cpp | 15 +++++- src/MenuManager.cpp | 37 +++++++++++-- src/MenuManager.h | 2 +- src/Space.cpp | 10 +++- src/network/ClientState.h | 38 +++++++------- 6 files changed, 140 insertions(+), 26 deletions(-) create mode 100644 resources/config/ship_selection_menu.json diff --git a/resources/config/ship_selection_menu.json b/resources/config/ship_selection_menu.json new file mode 100644 index 0000000..7d953fe --- /dev/null +++ b/resources/config/ship_selection_menu.json @@ -0,0 +1,64 @@ +{ + "root": { + "name": "shipSelectionRoot", + "type": "node", + "children": [ + + { + "type": "TextField", + "name": "nicknameInput", + "x": 400, + "y": 150, + "width": 400, + "height": 50, + "placeholder": "Enter your nickname", + "fontPath": "resources/fonts/DroidSans.ttf", + "fontSize": 16, + "maxLength": 256, + "color": [122, 156, 198, 1], + "placeholderColor": [122, 156, 198, 1], + "backgroundColor": [15, 29, 51, 1], + "borderColor": [15, 29, 51, 1] + }, + { + "type": "Button", + "name": "spaceshipButton", + "x": 300, + "y": 320, + "width": 200, + "height": 80, + "textures": { + "normal": "resources/multiplayer_menu/JoinServer.png", + "hover": "resources/multiplayer_menu/JoinServer.png", + "pressed": "resources/multiplayer_menu/JoinServer.png" + } + }, + { + "type": "Button", + "name": "cargoshipButton", + "x": 700, + "y": 320, + "width": 200, + "height": 80, + "textures": { + "normal": "resources/multiplayer_menu/JoinServer.png", + "hover": "resources/multiplayer_menu/JoinServer.png", + "pressed": "resources/multiplayer_menu/JoinServer.png" + } + }, + { + "type": "Button", + "name": "backButton", + "x": 449, + "y": 280, + "width": 382, + "height": 56, + "textures": { + "normal": "resources/multiplayer_menu/Backbutton.png", + "hover": "resources/multiplayer_menu/Backbutton.png", + "pressed": "resources/multiplayer_menu/Backbutton.png" + } + } + ] + } + } \ No newline at end of file diff --git a/src/Game.cpp b/src/Game.cpp index a6981b9..96a09a5 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -103,9 +103,22 @@ namespace ZL menuManager.setupMenu(); - menuManager.onSingleplayerPressed = [this]() { + menuManager.onSingleplayerPressed = [this](const std::string& nickname, int shipType) { + Environment::shipState.nickname = nickname; + Environment::shipState.shipType = shipType; + networkClient = std::make_unique(); networkClient->Connect("", 0); + +#ifndef NETWORK + auto localClient = dynamic_cast(networkClient.get()); + if (localClient) { + ZL::ClientState st = Environment::shipState; + st.id = localClient->GetClientId(); + localClient->setLocalPlayerState(st); + } +#endif + lastTickCount = 0; spaceGameStarted = 1; }; diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index 752552c..2dca211 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -21,6 +21,7 @@ namespace ZL { gameOverSavedRoot = loadUiFromFile("resources/config/game_over.json", renderer, CONST_ZIP_FILE); + auto shipSelectionRoot = loadUiFromFile("resources/config/ship_selection_menu.json", renderer, CONST_ZIP_FILE); std::function loadGameplayUI; loadGameplayUI = [this]() { uiManager.replaceRoot(uiSavedRoot); @@ -114,10 +115,38 @@ namespace ZL { }); }; - uiManager.setButtonCallback("singleButton", [loadGameplayUI, this](const std::string& name) { - std::cerr << "Single button pressed: " << name << " -> load gameplay UI\n"; - loadGameplayUI(); - onSingleplayerPressed(); + uiManager.setButtonCallback("singleButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) { + std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n"; + if (!shipSelectionRoot) { + std::cerr << "Failed to load ship selection UI\n"; + return; + } + if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) { + uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) { + std::string nick = uiManager.getTextFieldValue("nicknameInput"); + if (nick.empty()) nick = "Player"; + int shipType = 0; + uiManager.popMenu(); + loadGameplayUI(); + if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType); + }); + + uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) { + std::string nick = uiManager.getTextFieldValue("nicknameInput"); + if (nick.empty()) nick = "Player"; + int shipType = 1; + uiManager.popMenu(); + loadGameplayUI(); + if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType); + }); + + uiManager.setButtonCallback("backButton", [this](const std::string& btnName) { + uiManager.popMenu(); + }); + } + else { + std::cerr << "Failed to push ship selection menu\n"; + } }); uiManager.setButtonCallback("multiplayerButton", [loadGameplayUI, this](const std::string& name) { std::cerr << "Multiplayer button pressed: " << name << " -> load gameplay UI\n"; diff --git a/src/MenuManager.h b/src/MenuManager.h index 7ad6d87..18b935f 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -34,7 +34,7 @@ namespace ZL { std::function onVelocityChanged; std::function onFirePressed; - std::function onSingleplayerPressed; + std::function onSingleplayerPressed; std::function onMultiplayerPressed; }; diff --git a/src/Space.cpp b/src/Space.cpp index f96e170..32a5e9f 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -454,8 +454,14 @@ namespace ZL renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset if (shipAlive) { - glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID()); - renderer.DrawVertexRenderStruct(spaceship); + if (Environment::shipState.shipType == 1 && cargoTexture) { + glBindTexture(GL_TEXTURE_2D, cargoTexture->getTexID()); + renderer.DrawVertexRenderStruct(cargo); + } + else { + glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID()); + renderer.DrawVertexRenderStruct(spaceship); + } } renderer.PopMatrix(); glEnable(GL_BLEND); diff --git a/src/network/ClientState.h b/src/network/ClientState.h index 11c868c..c9fb48f 100644 --- a/src/network/ClientState.h +++ b/src/network/ClientState.h @@ -4,6 +4,7 @@ #define _USE_MATH_DEFINES #include #include +#include using std::min; @@ -29,36 +30,37 @@ constexpr long long CUTOFF_TIME = 5000; //ms uint32_t fnv1a_hash(const std::string& data); struct ClientState { - int id = 0; - 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; + int id = 0; + 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; - int shipType = 0; - // ??? ??????? ???? - std::chrono::system_clock::time_point lastUpdateServerTime; + std::string nickname = "Player"; + int shipType = 0; + // ??? ??????? ???? + std::chrono::system_clock::time_point lastUpdateServerTime; - void simulate_physics(size_t delta); + void simulate_physics(size_t delta); - void apply_lag_compensation(std::chrono::system_clock::time_point nowTime); + void apply_lag_compensation(std::chrono::system_clock::time_point nowTime); - void handle_full_sync(const std::vector& parts, int startFrom); + void handle_full_sync(const std::vector& parts, int startFrom); - std::string formPingMessageContent(); + std::string formPingMessageContent(); }; struct ClientStateInterval { std::vector timedStates; - void add_state(const ClientState& state); + void add_state(const ClientState& state); - bool canFetchClientStateAtTime(std::chrono::system_clock::time_point targetTime) const; + bool canFetchClientStateAtTime(std::chrono::system_clock::time_point targetTime) const; - ClientState fetchClientStateAtTime(std::chrono::system_clock::time_point targetTime) const; + ClientState fetchClientStateAtTime(std::chrono::system_clock::time_point targetTime) const; }; From 74c2f786a197acd3bb311eb11123ca6ea4792c43 Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 25 Feb 2026 19:17:42 +0600 Subject: [PATCH 07/20] added choising spaceship type for multiplayer and some fix in respawn --- server/server.cpp | 81 +++++++++++++++++++++++++---- src/Game.cpp | 26 ++++++++- src/MenuManager.cpp | 42 +++++++++++---- src/MenuManager.h | 2 +- src/Space.cpp | 42 +++++++++------ src/network/WebSocketClientBase.cpp | 42 ++++++++++++++- 6 files changed, 196 insertions(+), 39 deletions(-) diff --git a/server/server.cpp b/server/server.cpp index 687658e..18d08c0 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -93,6 +93,9 @@ class Session : public std::enable_shared_from_this { public: ClientStateInterval timedClientStates; + std::string nickname = "Player"; + int shipType = 0; + explicit Session(tcp::socket&& socket, int id) : ws_(std::move(socket)), id_(id) { } @@ -205,7 +208,7 @@ public: return latest; } - + void doWrite() { std::lock_guard lock(writeMutex_); if (is_writing_ || writeQueue_.empty()) { @@ -253,7 +256,6 @@ private: void process_message(const std::string& msg) { if (!IsMessageValid(msg)) { - // Логируем попытку подмены и просто выходим из обработки std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl; return; } @@ -266,7 +268,40 @@ private: std::string type = parts[0]; - if (type == "UPD") { + if (type == "JOIN") { + std::string nick = "Player"; + int sType = 0; + if (parts.size() >= 2) nick = parts[1]; + if (parts.size() >= 3) { + try { sType = std::stoi(parts[2]); } + catch (...) { sType = 0; } + } + + this->nickname = nick; + this->shipType = sType; + + std::cout << "Server: Player " << id_ << " joined as [" << nick << "] shipType=" << sType << std::endl; + + std::string info = "PLAYERINFO:" + std::to_string(id_) + ":" + nick + ":" + std::to_string(sType); + + { + std::lock_guard lock(g_sessions_mutex); + for (auto& session : g_sessions) { + if (session->get_id() == this->id_) continue; + session->send_message(info); + } + } + { + std::lock_guard lock(g_sessions_mutex); + for (auto& session : g_sessions) { + if (session->get_id() == this->id_) continue; + std::string otherInfo = "PLAYERINFO:" + std::to_string(session->get_id()) + ":" + session->nickname + ":" + std::to_string(session->shipType); + // Отправляем именно новому клиенту + this->send_message(otherInfo); + } + } + } + else if (type == "UPD") { { std::lock_guard gd(g_dead_mutex); if (g_dead_players.find(id_) != g_dead_players.end()) { @@ -284,20 +319,48 @@ private: }; receivedState.lastUpdateServerTime = uptime_timepoint; receivedState.handle_full_sync(parts, 2); + receivedState.nickname = this->nickname; + receivedState.shipType = this->shipType; timedClientStates.add_state(receivedState); retranslateMessage(cleanMessage); } - else if (parts[0] == "RESPAWN") { + else if (type == "RESPAWN") { { std::lock_guard gd(g_dead_mutex); g_dead_players.erase(id_); } - std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_); - broadcastToAll(respawnMsg); + { + auto now_tp = std::chrono::system_clock::now(); + uint64_t now_ms = static_cast(std::chrono::duration_cast(now_tp.time_since_epoch()).count()); - std::cout << "Server: Player " << id_ << " respawned\n"; + ClientState st; + st.id = id_; + st.position = Eigen::Vector3f(0.0f, 0.0f, 45000.0f); + st.rotation = Eigen::Matrix3f::Identity(); + st.currentAngularVelocity = Eigen::Vector3f::Zero(); + st.velocity = 0.0f; + st.selectedVelocity = 0; + st.discreteMag = 0.0f; + st.discreteAngle = -1; + st.lastUpdateServerTime = now_tp; + st.nickname = this->nickname; + st.shipType = this->shipType; + + timedClientStates.add_state(st); + + std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_); + broadcastToAll(respawnMsg); + + std::string playerInfo = "PLAYERINFO:" + std::to_string(id_) + ":" + st.nickname + ":" + std::to_string(st.shipType); + broadcastToAll(playerInfo); + + std::string eventMsg = "EVENT:" + std::to_string(id_) + ":UPD:" + std::to_string(now_ms) + ":" + st.formPingMessageContent(); + broadcastToAll(eventMsg); + + std::cout << "Server: Player " << id_ << " respawned, broadcasted RESPAWN_ACK, PLAYERINFO and initial UPD\n"; + } } else if (parts[0] == "FIRE") { if (parts.size() < 10) return; @@ -344,8 +407,8 @@ private: Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f) }; - uint64_t now_ms = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count(); + uint64_t now_ms = std::chrono::duration_cast(( + std::chrono::system_clock::now().time_since_epoch())).count(); std::lock_guard pl(g_projectiles_mutex); for (int i = 0; i < std::min(shotCount, (int)localOffsets.size()); ++i) { diff --git a/src/Game.cpp b/src/Game.cpp index 96a09a5..de0dd61 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -24,6 +24,7 @@ #endif #include "network/LocalClient.h" +#include "network/ClientState.h" namespace ZL @@ -122,7 +123,11 @@ namespace ZL spaceGameStarted = 1; }; - menuManager.onMultiplayerPressed = [this]() { + menuManager.onMultiplayerPressed = [this](const std::string& nickname, int shipType) { + Environment::shipState.nickname = nickname; + Environment::shipState.shipType = shipType; + + networkClient = std::make_unique(); #ifdef NETWORK #ifdef EMSCRIPTEN networkClient = std::make_unique(); @@ -131,7 +136,26 @@ namespace ZL networkClient = std::make_unique(taskManager.getIOContext()); networkClient->Connect("localhost", 8081); #endif +#else + networkClient->Connect("", 0); #endif + +#ifndef NETWORK + auto localClient = dynamic_cast(networkClient.get()); + if (localClient) { + ZL::ClientState st = Environment::shipState; + st.id = localClient->GetClientId(); + localClient->setLocalPlayerState(st); + } +#endif + + + if (networkClient) { + std::string joinMsg = std::string("JOIN:") + nickname + ":" + std::to_string(shipType); + networkClient->Send(joinMsg); + std::cerr << "Sent JOIN: " << joinMsg << std::endl; + } + lastTickCount = 0; spaceGameStarted = 1; }; diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index 2dca211..c153cf0 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -148,10 +148,39 @@ namespace ZL { std::cerr << "Failed to push ship selection menu\n"; } }); - uiManager.setButtonCallback("multiplayerButton", [loadGameplayUI, this](const std::string& name) { - std::cerr << "Multiplayer button pressed: " << name << " -> load gameplay UI\n"; - loadGameplayUI(); - onMultiplayerPressed(); + + uiManager.setButtonCallback("multiplayerButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) { + std::cerr << "Multiplayer button pressed: " << name << " -> open ship selection UI\n"; + if (!shipSelectionRoot) { + std::cerr << "Failed to load ship selection UI\n"; + return; + } + if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) { + uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) { + std::string nick = uiManager.getTextFieldValue("nicknameInput"); + if (nick.empty()) nick = "Player"; + int shipType = 0; + uiManager.popMenu(); + loadGameplayUI(); + if (onMultiplayerPressed) onMultiplayerPressed(nick, shipType); + }); + + uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) { + std::string nick = uiManager.getTextFieldValue("nicknameInput"); + if (nick.empty()) nick = "Player"; + int shipType = 1; + uiManager.popMenu(); + loadGameplayUI(); + if (onMultiplayerPressed) onMultiplayerPressed(nick, shipType); + }); + + uiManager.setButtonCallback("backButton", [this](const std::string& btnName) { + uiManager.popMenu(); + }); + } + else { + std::cerr << "Failed to push ship selection menu\n"; + } }); uiManager.setButtonCallback("multiplayerButton2", [this](const std::string& name) { @@ -164,7 +193,6 @@ namespace ZL { if (uiManager.pushMenuFromSavedRoot(multiplayerSavedRoot)) { - // Callback для кнопки подключения uiManager.setButtonCallback("connectButton", [this](const std::string& buttonName) { std::string serverAddress = uiManager.getTextFieldValue("serverInputField"); @@ -176,16 +204,12 @@ namespace ZL { uiManager.setText("statusText", "Connecting to " + serverAddress + "..."); std::cerr << "Connecting to server: " << serverAddress << std::endl; - // Здесь добавить вашу логику подключения к серверу - // connectToServer(serverAddress); }); - // Callback для кнопки назад uiManager.setButtonCallback("backButton", [this](const std::string& buttonName) { uiManager.popMenu(); }); - // Callback для отслеживания ввода текста uiManager.setTextFieldCallback("serverInputField", [this](const std::string& fieldName, const std::string& newText) { std::cout << "Server input field changed to: " << newText << std::endl; diff --git a/src/MenuManager.h b/src/MenuManager.h index 18b935f..2bb4819 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -35,7 +35,7 @@ namespace ZL { std::function onFirePressed; std::function onSingleplayerPressed; - std::function onMultiplayerPressed; + std::function onMultiplayerPressed; }; }; diff --git a/src/Space.cpp b/src/Space.cpp index 32a5e9f..d5eb509 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -267,6 +267,16 @@ namespace ZL Environment::zoom = DEFAULT_ZOOM; Environment::tapDownHold = false; + if (networkClient) { + try { + networkClient->Send(std::string("RESPAWN")); + std::cout << "Client: Sent RESPAWN to server\n"; + } + catch (...) { + std::cerr << "Client: Failed to send RESPAWN\n"; + } + } + std::cerr << "Game restarted\n"; }; @@ -677,8 +687,6 @@ namespace ZL { if (!textRenderer) return; - //#ifdef NETWORK - // 2D поверх 3D glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -688,18 +696,12 @@ namespace ZL if (deadRemotePlayers.count(id)) continue; const ClientState& st = remotePlayer; - // Позиция корабля в мире Vector3f shipWorld = st.position; float distSq = (Environment::shipState.position - shipWorld).squaredNorm(); - /*if (distSq > MAX_DIST_SQ) // дальность прорисовки никнейма - continue;*/ float dist = sqrt(distSq); - float alpha = 1.0f; // постоянная видимость - /*float alpha = std::clamp(1.f - (dist - FADE_START) / FADE_RANGE, 0.f, 1.f); // дальность прорисовки никнейма - if (alpha < 0.01f) - continue; */ - Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f }; // регулировка высоты + float alpha = 1.0f; + Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f }; float sx, sy, depth; if (!worldToScreen(labelWorld, sx, sy, depth)) continue; @@ -707,18 +709,21 @@ namespace ZL float uiX = sx, uiY = sy; float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE); - // Дефолтный лейбл - std::string label = "Player (" + std::to_string(st.id) + ") " + std::to_string((int)dist) + "m"; + std::string displayName; + if (!st.nickname.empty() && st.nickname != "Player") { + displayName = st.nickname; + } + else { + displayName = "Player (" + std::to_string(st.id) + ")"; + } + std::string label = displayName + " " + std::to_string((int)dist) + "m"; - // TODO: nickname sync - - textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, { 0.f, 0.f, 0.f, alpha }); // color param + textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, { 0.f, 0.f, 0.f, alpha }); textRenderer->drawText(label, uiX, uiY, scale, true, { 1.f, 1.f, 1.f, alpha }); } glDisable(GL_BLEND); glEnable(GL_DEPTH_TEST); - //#endif } int Space::pickTargetId() const @@ -1288,6 +1293,10 @@ namespace ZL for (auto const& [id, remotePlayer] : latestRemotePlayers) { + if (networkClient && id == networkClient->GetClientId()) { + continue; + } + if (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay)) { continue; @@ -1296,7 +1305,6 @@ namespace ZL ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay); remotePlayerStates[id] = playerState; - } for (auto& p : projectiles) { diff --git a/src/network/WebSocketClientBase.cpp b/src/network/WebSocketClientBase.cpp index 48b99f2..52c82a4 100644 --- a/src/network/WebSocketClientBase.cpp +++ b/src/network/WebSocketClientBase.cpp @@ -79,7 +79,6 @@ namespace ZL { try { int respawnedPlayerId = std::stoi(parts[1]); pendingRespawns_.push_back(respawnedPlayerId); - remotePlayers.erase(respawnedPlayerId); std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl; } catch (...) {} @@ -202,6 +201,11 @@ namespace ZL { { auto& rp = remotePlayers[remoteId]; + if (!rp.timedStates.empty()) { + const ClientState& last = rp.timedStates.back(); + remoteState.nickname = last.nickname; + remoteState.shipType = last.shipType; + } rp.add_state(remoteState); } } @@ -218,7 +222,7 @@ namespace ZL { if (playerParts.size() < 15) return; // ID + 14 полей ClientState int rId = std::stoi(playerParts[0]); - if (rId == clientId) return; // Свое состояние игрок знает лучше всех (Client Side Prediction) + if (rId == clientId) return; // Свое состояние игрок знает лучше всех, (Client Side Prediction) ClientState remoteState; remoteState.id = rId; @@ -230,6 +234,40 @@ namespace ZL { remotePlayers[rId].add_state(remoteState); } } + + if (msg.rfind("PLAYERINFO:", 0) == 0) { + if (parts.size() >= 4) { + try { + int pid = std::stoi(parts[1]); + if (pid == clientId) { + return; + } + + std::string nick = parts[2]; + int st = std::stoi(parts[3]); + + auto it = remotePlayers.find(pid); + if (it != remotePlayers.end() && !it->second.timedStates.empty()) { + auto& states = it->second.timedStates; + states.back().nickname = nick; + states.back().shipType = st; + } + else { + ClientState cs; + cs.id = pid; + cs.nickname = nick; + cs.shipType = st; + cs.lastUpdateServerTime = std::chrono::system_clock::now(); + remotePlayers[pid].add_state(cs); + } + + std::cout << "Client: PLAYERINFO received. id=" << pid << " nick=" << nick << " shipType=" << st << std::endl; + } + catch (...) { + } + } + return; + } } std::string WebSocketClientBase::SignMessage(const std::string& msg) { From fb1b0a1c2e2649f442ceed6099f8749e2b968b09 Mon Sep 17 00:00:00 2001 From: vottozi Date: Wed, 25 Feb 2026 22:18:47 +0600 Subject: [PATCH 08/20] Lead Indicator fix --- src/Space.cpp | 56 +++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/Space.cpp b/src/Space.cpp index acff0dd..1336b35 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -858,41 +858,38 @@ namespace ZL Vector3f shooterVel = ForwardFromRotation(Environment::shipState.rotation) * Environment::shipState.velocity; Vector3f targetVel = ForwardFromRotation(st.rotation) * st.velocity; - // условие "если враг не движется — круг не рисуем" const float minTargetSpeed = 0.5f; // подобрать (в твоих единицах) bool targetMoving = (targetVel.norm() > minTargetSpeed); + + // альфа круга + float leadAlpha = targetMoving ? 1.0f : 0.5f; Vector3f leadWorld = shipWorld; bool haveLead = false; - //if (targetMoving) { - // float tLead = 0.0f; - // if (SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead)) { - // // ограничим случаи, чтобы круг не улетал далеко - // if (tLead > 0.0f && tLead < 8.0f) { - // // подобрать максимум (сек) - // leadWorld = shipWorld + targetVel * tLead; - // haveLead = true; - // } - // } - //} + // чтобы круг не улетал далеко : максимум 4 секунды(подстрой под игру) + float distToTarget = (Environment::shipState.position - shipWorld).norm(); + float maxLeadTime = std::clamp((distToTarget / projectileSpeed) * 1.2f, 0.05f, 4.0f); - if (targetMoving) { + if (!targetMoving) { + // Цель стоит: рисуем lead прямо на ней, но полупрозрачный + leadWorld = shipWorld; + haveLead = true; + } + else { float tLead = 0.0f; - float distToTarget = (Environment::shipState.position - shipWorld).norm(); - const float leadMaxDist = 2500.0f; // максимум - float allowedDist = min(distToTarget, leadMaxDist); + // 1) Пытаемся “правильное” решение перехвата + bool ok = SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead); - // + небольшой запас 10–20% чтобы не моргало на границе - const float maxLeadTime = (allowedDist / projectileSpeed) * 1.2f; - - if (SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead)) { - if (tLead > 0.0f && tLead < maxLeadTime) { - leadWorld = shipWorld + targetVel * tLead; - haveLead = true; - } + // 2) Если решения нет / оно плохое — fallback (чтобы круг не пропадал при пролёте "вбок") + // Это ключевое изменение: lead всегда будет. + if (!ok || !(tLead > 0.0f) || tLead > maxLeadTime) { + tLead = std::clamp(distToTarget / projectileSpeed, 0.05f, maxLeadTime); } + + leadWorld = shipWorld + targetVel * tLead; + haveLead = true; } // 2) проекция @@ -965,6 +962,10 @@ namespace ZL renderer.PushMatrix(); renderer.LoadIdentity(); + // базовый цвет для HUD (скобки/стрелка) — непрозрачный + Eigen::Vector4f hudColor = enemyColor; + renderer.RenderUniform4fv("uColor", hudColor.data()); + // рисуем кружок упреждения (только если есть решение) if (haveLead) { float leadNdcX, leadNdcY, leadNdcZ, leadClipW; @@ -981,9 +982,16 @@ namespace ZL float innerR = max(1.0f, r - thicknessPx); float outerR = r + thicknessPx; + // для lead indicator alpha: 1.0 если движется, 0.5 если стоит + Eigen::Vector4f leadColor = enemyColor; + leadColor.w() = leadAlpha; + renderer.RenderUniform4fv("uColor", leadColor.data()); + VertexDataStruct ring = MakeRing2D(lx, ly, innerR, outerR, 0.0f, 32, enemyColor); hudTempMesh.AssignFrom(ring); renderer.DrawVertexRenderStruct(hudTempMesh); + + renderer.RenderUniform4fv("uColor", hudColor.data()); } } } From 5da5a754fe88b925b17eeafd15e1b19b6b267fa9 Mon Sep 17 00:00:00 2001 From: vottozi Date: Thu, 26 Feb 2026 01:49:49 +0600 Subject: [PATCH 09/20] add configurable HUD crosshair --- resources/config/crosshair_config.json | 21 +++ src/Space.cpp | 240 ++++++++++++++++++++++++- src/Space.h | 36 ++++ 3 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 resources/config/crosshair_config.json diff --git a/resources/config/crosshair_config.json b/resources/config/crosshair_config.json new file mode 100644 index 0000000..d4b2452 --- /dev/null +++ b/resources/config/crosshair_config.json @@ -0,0 +1,21 @@ +{ + "enabled": true, + + "referenceResolution": [1280, 720], + + "color": [1.0, 1.0, 1.0], + "cl_crosshairalpha": 1.0, + "cl_crosshairthickness": 2.0, + + "centerGapPx": 10.0, + + "top": { + "lengthPx": 14.0, + "angleDeg": 90.0 + }, + + "arms": [ + { "lengthPx": 20.0, "angleDeg": 210.0 }, + { "lengthPx": 20.0, "angleDeg": 330.0 } + ] +} \ No newline at end of file diff --git a/src/Space.cpp b/src/Space.cpp index 1336b35..6f2a77b 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -336,6 +336,19 @@ namespace ZL throw std::runtime_error("Failed to load spark emitter config file!"); } + crosshairCfgLoaded = loadCrosshairConfig("resources/config/crosshair_config.json"); + std::cerr << "[Crosshair] loaded=" << crosshairCfgLoaded + << " enabled=" << crosshairCfg.enabled + << " w=" << Environment::width << " h=" << Environment::height + << " alpha=" << crosshairCfg.alpha + << " thickness=" << crosshairCfg.thicknessPx + << " gap=" << crosshairCfg.gapPx << "\n"; + if (!crosshairCfgLoaded) { + std::cerr << "Failed to load crosshair_config.json, using defaults\n"; + } + + + textRenderer = std::make_unique(); if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) { std::cerr << "Failed to init TextRenderer\n"; @@ -566,6 +579,7 @@ namespace ZL drawBoxesLabels(); drawShip(); + drawCrosshair(); drawTargetHud(); CheckGlError(); } @@ -693,6 +707,230 @@ namespace ZL //#endif } + // хелпер прицела: добавляет повернутую 2D-линию в меш прицела + static void AppendRotatedRect2D( + VertexDataStruct& out, + float cx, float cy, + float length, float thickness, + float angleRad, + float z, + const Eigen::Vector3f& rgb) + { + // прямоугольник вдоль локальной оси +X: [-L/2..+L/2] и [-T/2..+T/2] + float hl = length * 0.5f; + float ht = thickness * 0.5f; + + Eigen::Vector2f p0(-hl, -ht); + Eigen::Vector2f p1(-hl, +ht); + Eigen::Vector2f p2(+hl, +ht); + Eigen::Vector2f p3(+hl, -ht); + + float c = std::cos(angleRad); + float s = std::sin(angleRad); + + auto rot = [&](const Eigen::Vector2f& p) -> Vector3f { + float rx = p.x() * c - p.y() * s; + float ry = p.x() * s + p.y() * c; + return Vector3f(cx + rx, cy + ry, z); + }; + + Vector3f v0 = rot(p0); + Vector3f v1 = rot(p1); + Vector3f v2 = rot(p2); + Vector3f v3 = rot(p3); + + // 2 треугольника + out.PositionData.push_back(v0); + out.PositionData.push_back(v1); + out.PositionData.push_back(v2); + out.PositionData.push_back(v2); + out.PositionData.push_back(v3); + out.PositionData.push_back(v0); + + for (int i = 0; i < 6; ++i) out.ColorData.push_back(rgb); + } + + bool Space::loadCrosshairConfig(const std::string& path) + { + using json = nlohmann::json; + + std::string content; + try { + if (std::string(CONST_ZIP_FILE).empty()) content = readTextFile(path); + else { + auto buf = readFileFromZIP(path, CONST_ZIP_FILE); + if (buf.empty()) return false; + content.assign(buf.begin(), buf.end()); + } + json j = json::parse(content); + + if (j.contains("enabled")) crosshairCfg.enabled = j["enabled"].get(); + + if (j.contains("referenceResolution") && j["referenceResolution"].is_array() && j["referenceResolution"].size() == 2) { + crosshairCfg.refW = j["referenceResolution"][0].get(); + crosshairCfg.refH = j["referenceResolution"][1].get(); + } + + if (j.contains("scale")) crosshairCfg.scaleMul = j["scale"].get(); + crosshairCfg.scaleMul = std::clamp(crosshairCfg.scaleMul, 0.1f, 3.0f); + + if (j.contains("color") && j["color"].is_array() && j["color"].size() == 3) { + crosshairCfg.color = Eigen::Vector3f( + j["color"][0].get(), + j["color"][1].get(), + j["color"][2].get() + ); + } + + if (j.contains("cl_crosshairalpha")) crosshairCfg.alpha = j["cl_crosshairalpha"].get(); + if (j.contains("cl_crosshairthickness")) crosshairCfg.thicknessPx = j["cl_crosshairthickness"].get(); + if (j.contains("centerGapPx")) crosshairCfg.gapPx = j["centerGapPx"].get(); + + if (j.contains("top") && j["top"].is_object()) { + auto t = j["top"]; + if (t.contains("lengthPx")) crosshairCfg.topLenPx = t["lengthPx"].get(); + if (t.contains("angleDeg")) crosshairCfg.topAngleDeg = t["angleDeg"].get(); + } + + crosshairCfg.arms.clear(); + if (j.contains("arms") && j["arms"].is_array()) { + for (auto& a : j["arms"]) { + CrosshairConfig::Arm arm; + arm.lenPx = a.value("lengthPx", 20.0f); + arm.angleDeg = a.value("angleDeg", 210.0f); + crosshairCfg.arms.push_back(arm); + } + } + else { + // дефолт + crosshairCfg.arms.push_back({ 20.0f, 210.0f }); + crosshairCfg.arms.push_back({ 20.0f, 330.0f }); + } + + // clamp + crosshairCfg.alpha = std::clamp(crosshairCfg.alpha, 0.0f, 1.0f); + crosshairCfg.thicknessPx = max(0.5f, crosshairCfg.thicknessPx); + crosshairCfg.gapPx = max(0.0f, crosshairCfg.gapPx); + + crosshairMeshValid = false; // пересобрать + return true; + } + catch (...) { + return false; + } + } + + // пересобирает mesh прицела при изменениях/ресайзе + void Space::rebuildCrosshairMeshIfNeeded() + { + if (!crosshairCfg.enabled) return; + + // если ничего не изменилось — не трогаем VBO + if (crosshairMeshValid && + crosshairLastW == Environment::width && + crosshairLastH == Environment::height && + std::abs(crosshairLastAlpha - crosshairCfg.alpha) < 1e-6f && + std::abs(crosshairLastThickness - crosshairCfg.thicknessPx) < 1e-6f && + std::abs(crosshairLastGap - crosshairCfg.gapPx) < 1e-6f && + std::abs(crosshairLastScaleMul - crosshairCfg.scaleMul) < 1e-6f) + { + return; + } + + crosshairLastW = Environment::width; + crosshairLastH = Environment::height; + crosshairLastAlpha = crosshairCfg.alpha; + crosshairLastThickness = crosshairCfg.thicknessPx; + crosshairLastGap = crosshairCfg.gapPx; + crosshairLastScaleMul = crosshairCfg.scaleMul; + + float cx = Environment::width * 0.5f; + float cy = Environment::height * 0.5f; + + // масштаб от reference (стандартно: по высоте) + float scale = (crosshairCfg.refH > 0) ? (Environment::height / (float)crosshairCfg.refH) : 1.0f; + scale *= crosshairCfg.scaleMul; + + float thickness = crosshairCfg.thicknessPx * scale; + float gap = crosshairCfg.gapPx * scale; + + VertexDataStruct v; + v.PositionData.reserve(6 * (1 + (int)crosshairCfg.arms.size())); + v.ColorData.reserve(6 * (1 + (int)crosshairCfg.arms.size())); + + const float z = 0.0f; + const Eigen::Vector3f rgb = crosshairCfg.color; + + auto deg2rad = [](float d) { return d * 3.1415926535f / 180.0f; }; + + // TOP (короткая палочка сверху) + { + float len = crosshairCfg.topLenPx * scale; + float ang = deg2rad(crosshairCfg.topAngleDeg); + + // сдвигаем сегмент от центра на gap + len/2 по направлению + float dx = std::cos(ang); + float dy = std::sin(ang); + float mx = cx + dx * (gap + len * 0.5f); + float my = cy + dy * (gap + len * 0.5f); + + AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb); + } + + // ARMS (2 луча вниз-влево и вниз-вправо) + for (auto& a : crosshairCfg.arms) + { + float len = a.lenPx * scale; + float ang = deg2rad(a.angleDeg); + + float dx = std::cos(ang); + float dy = std::sin(ang); + float mx = cx + dx * (gap + len * 0.5f); + float my = cy + dy * (gap + len * 0.5f); + + AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb); + } + + crosshairMesh.AssignFrom(v); + crosshairMesh.RefreshVBO(); + crosshairMeshValid = true; + } + + void Space::drawCrosshair() + { + if (!crosshairCfg.enabled) return; + + rebuildCrosshairMeshIfNeeded(); + if (!crosshairMeshValid) return; + + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + renderer.shaderManager.PushShader("defaultColor"); + renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); + renderer.PushMatrix(); + renderer.LoadIdentity(); + + renderer.EnableVertexAttribArray("vPosition"); + renderer.EnableVertexAttribArray("vColor"); + + Eigen::Vector4f uColor(crosshairCfg.color.x(), crosshairCfg.color.y(), crosshairCfg.color.z(), crosshairCfg.alpha); + renderer.RenderUniform4fv("uColor", uColor.data()); + + renderer.DrawVertexRenderStruct(crosshairMesh); + + renderer.DisableVertexAttribArray("vPosition"); + renderer.DisableVertexAttribArray("vColor"); + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); + + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + } + int Space::pickTargetId() const { int bestId = -1; @@ -867,7 +1105,7 @@ namespace ZL Vector3f leadWorld = shipWorld; bool haveLead = false; - // чтобы круг не улетал далеко : максимум 4 секунды(подстрой под игру) + // чтобы круг не улетал далеко: максимум 4 секунды (подстроить под игру) float distToTarget = (Environment::shipState.position - shipWorld).norm(); float maxLeadTime = std::clamp((distToTarget / projectileSpeed) * 1.2f, 0.05f, 4.0f); diff --git a/src/Space.h b/src/Space.h index 9e97eeb..d5d8a7c 100644 --- a/src/Space.h +++ b/src/Space.h @@ -136,6 +136,42 @@ namespace ZL { int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок) void clearTextRendererCache(); + + // Crosshair HUD + struct CrosshairConfig { + bool enabled = true; + int refW = 1280; + int refH = 720; + + float scaleMul = 1.0f; + + Eigen::Vector3f color = { 1.f, 1.f, 1.f }; + float alpha = 1.0f; // cl_crosshairalpha + float thicknessPx = 2.0f; // cl_crosshairthickness + float gapPx = 10.0f; + + float topLenPx = 14.0f; + float topAngleDeg = 90.0f; + + struct Arm { float lenPx; float angleDeg; }; + std::vector arms; + }; + + CrosshairConfig crosshairCfg; + bool crosshairCfgLoaded = false; + + // кеш геометрии + VertexRenderStruct crosshairMesh; + bool crosshairMeshValid = false; + int crosshairLastW = 0, crosshairLastH = 0; + float crosshairLastAlpha = -1.0f; + float crosshairLastThickness = -1.0f; + float crosshairLastGap = -1.0f; + float crosshairLastScaleMul = -1.0f; + + bool loadCrosshairConfig(const std::string& path); + void rebuildCrosshairMeshIfNeeded(); + void drawCrosshair(); }; From f8642efa2366b5d42c8f5b9a93b361a17b4da067 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Thu, 26 Feb 2026 21:02:16 +0300 Subject: [PATCH 10/20] merge --- resources/multiplayer_menu/AvailableServers.png | 4 ++-- resources/multiplayer_menu/Backbutton.png | 4 ++-- resources/multiplayer_menu/Button.png | 4 ++-- resources/multiplayer_menu/Button2.png | 4 ++-- resources/multiplayer_menu/Button3.png | 4 ++-- resources/multiplayer_menu/Filledbuttons.png | 4 ++-- resources/multiplayer_menu/JoinServer.png | 4 ++-- resources/multiplayer_menu/ServerName.png | 4 ++-- resources/multiplayer_menu/title.png | 4 ++-- src/network/WebSocketClientEmscripten.cpp | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/resources/multiplayer_menu/AvailableServers.png b/resources/multiplayer_menu/AvailableServers.png index 8a5759d..d4c055e 100644 --- a/resources/multiplayer_menu/AvailableServers.png +++ b/resources/multiplayer_menu/AvailableServers.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6c6555671ec968eff10211ef2efd8ac097c52bf602b314d101f78c57f4a7059 -size 1823 +oid sha256:69624bbb22ebac925c3eb9299c862f07fa0815a6d90ed14e7e91a14be588e4d0 +size 8167 diff --git a/resources/multiplayer_menu/Backbutton.png b/resources/multiplayer_menu/Backbutton.png index 9ee46af..37cdae0 100644 --- a/resources/multiplayer_menu/Backbutton.png +++ b/resources/multiplayer_menu/Backbutton.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0bcfe59a76449bd26a8558bbcf59a2bb06227d4ad99f5686a41ff567ca4b2bb -size 1814 +oid sha256:113c524330190fbdf36f0f7b4ebfc03032170aff5011f8c17201533b52db872f +size 5387 diff --git a/resources/multiplayer_menu/Button.png b/resources/multiplayer_menu/Button.png index a08abec..620425a 100644 --- a/resources/multiplayer_menu/Button.png +++ b/resources/multiplayer_menu/Button.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e9ef4f2c9e8b45419915f4630e9ec3228388e6eff9c1f6fffd8a7762f0437c7 -size 3508 +oid sha256:2e0267f247715722ee0881e96dc83d1c83a999c718af1bac64b5964c1a2c7335 +size 8074 diff --git a/resources/multiplayer_menu/Button2.png b/resources/multiplayer_menu/Button2.png index 49b0468..884b2fa 100644 --- a/resources/multiplayer_menu/Button2.png +++ b/resources/multiplayer_menu/Button2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b9dc0b621e0125c32d21cf2a941638fc758ababd52b933b3aa806d8fba2d2bb -size 3575 +oid sha256:629aa9499976abeeb83194b4cc67153383ab4ebdfc46dd98e4c3733ee7b92e4e +size 8612 diff --git a/resources/multiplayer_menu/Button3.png b/resources/multiplayer_menu/Button3.png index b7f3804..892fcca 100644 --- a/resources/multiplayer_menu/Button3.png +++ b/resources/multiplayer_menu/Button3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcb8c3d841ab6e449adf66263e4bd27d80240f591e090e6047bc1206dd4d9b10 -size 3742 +oid sha256:a74a8b4c954ceb45d582b46d0e5bf2f544f5d933d1b264b4d80fb2720c3a6e68 +size 9129 diff --git a/resources/multiplayer_menu/Filledbuttons.png b/resources/multiplayer_menu/Filledbuttons.png index ae03c85..26f29f7 100644 --- a/resources/multiplayer_menu/Filledbuttons.png +++ b/resources/multiplayer_menu/Filledbuttons.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb124591fa4995b4f958f082976d87cf3bc22b560dc790d0dac6d89c158c9a78 -size 2315 +oid sha256:8977b432e9b190f568456026e05fc9ae3a9eec7a90b285095578071c03e839ea +size 8375 diff --git a/resources/multiplayer_menu/JoinServer.png b/resources/multiplayer_menu/JoinServer.png index aec320d..7db6904 100644 --- a/resources/multiplayer_menu/JoinServer.png +++ b/resources/multiplayer_menu/JoinServer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66448d4bc67ad1cd0ed4ddd4e3671b17ffe97d11e59185d123b1b621badb6449 -size 1282 +oid sha256:2f01d975f7e6bec5e796b8b4272462b0b4d3d99076af3873c4d819a50b9dbe82 +size 4177 diff --git a/resources/multiplayer_menu/ServerName.png b/resources/multiplayer_menu/ServerName.png index b8b0d63..d8df64c 100644 --- a/resources/multiplayer_menu/ServerName.png +++ b/resources/multiplayer_menu/ServerName.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8bd3c9e3fa4fff2c9f5cba2858e8a964be237406162ed7dd7046690a8543eb4 -size 1367 +oid sha256:7b197ead1094f297c06083c909d8a910812d0981386578a75fd74af67d54819e +size 4304 diff --git a/resources/multiplayer_menu/title.png b/resources/multiplayer_menu/title.png index 4702a8a..30e6644 100644 --- a/resources/multiplayer_menu/title.png +++ b/resources/multiplayer_menu/title.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffcfc5e1d00f135259a94122d60c9524524b6f40d9cf7d26d17dd25b793ade40 -size 1830 +oid sha256:d52df5a072c43ef1a113cdc6fcb0af32fe5fcf353775bb0ad6907921ced30840 +size 7477 diff --git a/src/network/WebSocketClientEmscripten.cpp b/src/network/WebSocketClientEmscripten.cpp index 98044e9..9534cf0 100644 --- a/src/network/WebSocketClientEmscripten.cpp +++ b/src/network/WebSocketClientEmscripten.cpp @@ -7,8 +7,8 @@ namespace ZL { void WebSocketClientEmscripten::Connect(const std::string& host, uint16_t port) { // Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost - //std::string url = "ws://" + host + ":" + std::to_string(port); - std::string url = "wss://api.spacegame.fishrungames.com"; + std::string url = "ws://" + host + ":" + std::to_string(port); + //std::string url = "wss://api.spacegame.fishrungames.com"; EmscriptenWebSocketCreateAttributes attr = { url.c_str(), From be2aa76f8b075bfb8ffd6d4f12940f53750a0533 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Thu, 26 Feb 2026 22:26:43 +0300 Subject: [PATCH 11/20] Working on server optimization --- server/server.cpp | 429 +++++++++++++++++------------------- src/network/ClientState.cpp | 4 +- src/network/ClientState.h | 2 +- 3 files changed, 204 insertions(+), 231 deletions(-) diff --git a/server/server.cpp b/server/server.cpp index 18d08c0..1bcd2a6 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -323,7 +323,6 @@ private: receivedState.shipType = this->shipType; timedClientStates.add_state(receivedState); - retranslateMessage(cleanMessage); } else if (type == "RESPAWN") { { @@ -433,16 +432,6 @@ private: } } - 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 broadcastToAll(const std::string& message) { @@ -454,273 +443,257 @@ void broadcastToAll(const std::string& message) { void update_world(net::steady_timer& timer, net::io_context& ioc) { - static auto last_snapshot_time = std::chrono::steady_clock::now(); - auto now = std::chrono::steady_clock::now(); - /*static uint64_t lastTickCount = 0; + static auto last_snapshot_time = std::chrono::system_clock::now(); - if (lastTickCount == 0) { - //lastTickCount = SDL_GetTicks64(); - lastTickCount = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch() - ).count(); + auto now = std::chrono::system_clock::now(); + uint64_t now_ms = static_cast( + std::chrono::duration_cast(now.time_since_epoch()).count()); - lastTickCount = (lastTickCount / 50) * 50; - - return; - } - - - auto newTickCount = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch() - ).count(); - - newTickCount = (newTickCount / 50) * 50; - - int64_t deltaMs = static_cast(newTickCount - lastTickCount); - - std::chrono::system_clock::time_point nowRounded = std::chrono::system_clock::time_point(std::chrono::milliseconds(newTickCount)); - */ - // For each player - // Get letest state + add time (until newTickCount) - // Calculate if collisions with boxes - - - - - // Рассылка Snapshot раз в 1000мс - /* - if (std::chrono::duration_cast(now - last_snapshot_time).count() >= 1000) { + // --- Snapshot every 500ms --- + /*if (std::chrono::duration_cast(now - last_snapshot_time).count() >= 500) { last_snapshot_time = now; - auto system_now = std::chrono::system_clock::now(); - - std::string snapshot_msg = "SNAPSHOT:" + std::to_string( - std::chrono::duration_cast( - system_now.time_since_epoch()).count() - ); + std::string snapshot_msg = "SNAPSHOT:" + std::to_string(now_ms); std::lock_guard lock(g_sessions_mutex); - - // Формируем общую строку состояний всех игроков for (auto& session : g_sessions) { - ClientState st = session->get_latest_state(system_now); + ClientState st = session->get_latest_state(now); snapshot_msg += "|" + std::to_string(session->get_id()) + ":" + st.formPingMessageContent(); } - for (auto& session : g_sessions) { session->send_message(snapshot_msg); } }*/ - const std::chrono::milliseconds interval(50); - timer.expires_after(interval); + // --- Tick: broadcast each player's latest state to all others (20Hz) --- + // Send the raw last-known state with its original timestamp, NOT an extrapolated one. + // Extrapolating here causes snap-back: if a player stops rotating, the server would + // keep sending over-rotated positions until the new state arrives, then B snaps back. + { + std::lock_guard lock(g_sessions_mutex); + for (auto& sender : g_sessions) { + if (sender->timedClientStates.timedStates.empty()) continue; - timer.async_wait([&](const boost::system::error_code& ec) { - if (ec) return; + const ClientState& st = sender->timedClientStates.timedStates.back(); + uint64_t stateTime = static_cast( + std::chrono::duration_cast( + st.lastUpdateServerTime.time_since_epoch()).count()); - auto now = std::chrono::system_clock::now(); - uint64_t now_ms = static_cast(std::chrono::duration_cast(now.time_since_epoch()).count()); + std::string event_msg = "EVENT:" + std::to_string(sender->get_id()) + + ":UPD:" + std::to_string(stateTime) + ":" + st.formPingMessageContent(); - std::vector deathEvents; - - { - std::lock_guard pl(g_projectiles_mutex); - std::vector indicesToRemove; - - float dt = 50.0f / 1000.0f; - - for (size_t i = 0; i < g_projectiles.size(); ++i) { - auto& pr = g_projectiles[i]; - - pr.pos += pr.vel * dt; - - if (now_ms > pr.spawnMs + static_cast(pr.lifeMs)) { - indicesToRemove.push_back(static_cast(i)); - continue; - } - - bool hitDetected = false; - - { - std::lock_guard lm(g_sessions_mutex); - std::lock_guard gd(g_dead_mutex); - - for (auto& session : g_sessions) { - int targetId = session->get_id(); - - if (targetId == pr.shooterId) continue; - if (g_dead_players.find(targetId) != g_dead_players.end()) continue; - - ClientState targetState; - if (!session->fetchStateAtTime(now, targetState)) continue; - - Eigen::Vector3f diff = pr.pos - targetState.position; - const float shipRadius = 15.0f; - const float projectileRadius = 1.5f; - float combinedRadius = shipRadius + projectileRadius; - - if (diff.squaredNorm() <= combinedRadius * combinedRadius) { - DeathInfo death; - death.targetId = targetId; - death.serverTime = now_ms; - death.position = pr.pos; - death.killerId = pr.shooterId; - - deathEvents.push_back(death); - g_dead_players.insert(targetId); - indicesToRemove.push_back(static_cast(i)); - hitDetected = true; - - std::cout << "Server: *** HIT DETECTED! ***" << std::endl; - std::cout << "Server: Projectile at (" - << pr.pos.x() << ", " << pr.pos.y() << ", " << pr.pos.z() - << ") hit player " << targetId << std::endl; - break; - } - } - } - - if (hitDetected) continue; - } - - if (!indicesToRemove.empty()) { - std::sort(indicesToRemove.rbegin(), indicesToRemove.rend()); - for (int idx : indicesToRemove) { - if (idx >= 0 && idx < (int)g_projectiles.size()) { - g_projectiles.erase(g_projectiles.begin() + idx); - } + for (auto& receiver : g_sessions) { + if (receiver->get_id() != sender->get_id()) { + receiver->send_message(event_msg); } } } + } - { - std::lock_guard bm(g_boxes_mutex); - //const float projectileHitRadius = 1.5f; - const float projectileHitRadius = 5.0f; - const float boxCollisionRadius = 2.0f; + // --- Tick: projectile movement and hit detection --- + const float dt = 50.0f / 1000.0f; + std::vector deathEvents; - std::vector> boxProjectileCollisions; + { + std::lock_guard pl(g_projectiles_mutex); + std::vector indicesToRemove; - for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) { - if (g_serverBoxes[bi].destroyed) continue; + for (size_t i = 0; i < g_projectiles.size(); ++i) { + auto& pr = g_projectiles[i]; - Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f); + pr.pos += pr.vel * dt; - for (size_t pi = 0; pi < g_projectiles.size(); ++pi) { - const auto& pr = g_projectiles[pi]; - Eigen::Vector3f diff = pr.pos - boxWorld; - //std::cout << "diff norm is " << diff.norm() << std::endl; - float thresh = boxCollisionRadius + projectileHitRadius; - - if (diff.squaredNorm() <= thresh * thresh) { - boxProjectileCollisions.push_back({ bi, pi }); - } - } + if (now_ms > pr.spawnMs + static_cast(pr.lifeMs)) { + indicesToRemove.push_back(static_cast(i)); + continue; } - for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) { - g_serverBoxes[boxIdx].destroyed = true; + bool hitDetected = false; - Eigen::Vector3f boxWorld = g_serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f); - - BoxDestroyedInfo destruction; - destruction.boxIndex = static_cast(boxIdx); - destruction.serverTime = now_ms; - destruction.position = boxWorld; - destruction.destroyedBy = g_projectiles[projIdx].shooterId; - - { - std::lock_guard dm(g_boxDestructions_mutex); - g_boxDestructions.push_back(destruction); - } - - std::cout << "Server: Box " << boxIdx << " destroyed by projectile from player " - << g_projectiles[projIdx].shooterId << std::endl; - } - } - - { - std::lock_guard bm(g_boxes_mutex); - std::lock_guard lm(g_sessions_mutex); - - const float shipCollisionRadius = 15.0f; - const float boxCollisionRadius = 2.0f; - - for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) { - if (g_serverBoxes[bi].destroyed) continue; - - Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f); + { + std::lock_guard lm(g_sessions_mutex); + std::lock_guard gd(g_dead_mutex); for (auto& session : g_sessions) { - { - std::lock_guard gd(g_dead_mutex); - if (g_dead_players.find(session->get_id()) != g_dead_players.end()) { - continue; - } - } + int targetId = session->get_id(); - ClientState shipState; - if (!session->fetchStateAtTime(now, shipState)) continue; + if (targetId == pr.shooterId) continue; + if (g_dead_players.find(targetId) != g_dead_players.end()) continue; - Eigen::Vector3f diff = shipState.position - boxWorld; - float thresh = shipCollisionRadius + boxCollisionRadius; + ClientState targetState; + if (!session->fetchStateAtTime(now, targetState)) continue; - if (diff.squaredNorm() <= thresh * thresh) { - g_serverBoxes[bi].destroyed = true; + Eigen::Vector3f diff = pr.pos - targetState.position; + const float shipRadius = 15.0f; + const float projectileRadius = 1.5f; + float combinedRadius = shipRadius + projectileRadius; - BoxDestroyedInfo destruction; - destruction.boxIndex = static_cast(bi); - destruction.serverTime = now_ms; - destruction.position = boxWorld; - destruction.destroyedBy = session->get_id(); + if (diff.squaredNorm() <= combinedRadius * combinedRadius) { + DeathInfo death; + death.targetId = targetId; + death.serverTime = now_ms; + death.position = pr.pos; + death.killerId = pr.shooterId; - { - std::lock_guard dm(g_boxDestructions_mutex); - g_boxDestructions.push_back(destruction); - } + deathEvents.push_back(death); + g_dead_players.insert(targetId); + indicesToRemove.push_back(static_cast(i)); + hitDetected = true; - std::cout << "Server: Box " << bi << " destroyed by ship collision with player " - << session->get_id() << std::endl; + std::cout << "Server: *** HIT DETECTED! ***" << std::endl; + std::cout << "Server: Projectile at (" + << pr.pos.x() << ", " << pr.pos.y() << ", " << pr.pos.z() + << ") hit player " << targetId << std::endl; break; } } } + + if (hitDetected) continue; } - if (!deathEvents.empty()) { - for (const auto& death : deathEvents) { - std::string deadMsg = "DEAD:" + - std::to_string(death.serverTime) + ":" + - std::to_string(death.targetId) + ":" + - std::to_string(death.position.x()) + ":" + - std::to_string(death.position.y()) + ":" + - std::to_string(death.position.z()) + ":" + - std::to_string(death.killerId); + if (!indicesToRemove.empty()) { + std::sort(indicesToRemove.rbegin(), indicesToRemove.rend()); + for (int idx : indicesToRemove) { + if (idx >= 0 && idx < (int)g_projectiles.size()) { + g_projectiles.erase(g_projectiles.begin() + idx); + } + } + } + } - broadcastToAll(deadMsg); + // --- Tick: box-projectile collisions --- + { + std::lock_guard bm(g_boxes_mutex); + const float projectileHitRadius = 5.0f; + const float boxCollisionRadius = 2.0f; - std::cout << "Server: Sent DEAD event - Player " << death.targetId - << " killed by " << death.killerId << std::endl; + std::vector> boxProjectileCollisions; + + for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) { + if (g_serverBoxes[bi].destroyed) continue; + + Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f); + + for (size_t pi = 0; pi < g_projectiles.size(); ++pi) { + const auto& pr = g_projectiles[pi]; + Eigen::Vector3f diff = pr.pos - boxWorld; + float thresh = boxCollisionRadius + projectileHitRadius; + + if (diff.squaredNorm() <= thresh * thresh) { + boxProjectileCollisions.push_back({ bi, pi }); + } } } - { - std::lock_guard dm(g_boxDestructions_mutex); - for (const auto& destruction : g_boxDestructions) { - std::string boxMsg = "BOX_DESTROYED:" + - std::to_string(destruction.boxIndex) + ":" + - std::to_string(destruction.serverTime) + ":" + - std::to_string(destruction.position.x()) + ":" + - std::to_string(destruction.position.y()) + ":" + - std::to_string(destruction.position.z()) + ":" + - std::to_string(destruction.destroyedBy); + for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) { + g_serverBoxes[boxIdx].destroyed = true; - broadcastToAll(boxMsg); - std::cout << "Server: Broadcasted BOX_DESTROYED for box " << destruction.boxIndex << std::endl; + Eigen::Vector3f boxWorld = g_serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f); + + BoxDestroyedInfo destruction; + destruction.boxIndex = static_cast(boxIdx); + destruction.serverTime = now_ms; + destruction.position = boxWorld; + destruction.destroyedBy = g_projectiles[projIdx].shooterId; + + { + std::lock_guard dm(g_boxDestructions_mutex); + g_boxDestructions.push_back(destruction); } - g_boxDestructions.clear(); - } + std::cout << "Server: Box " << boxIdx << " destroyed by projectile from player " + << g_projectiles[projIdx].shooterId << std::endl; + } + } + + // --- Tick: box-ship collisions --- + { + std::lock_guard bm(g_boxes_mutex); + std::lock_guard lm(g_sessions_mutex); + + const float shipCollisionRadius = 15.0f; + const float boxCollisionRadius = 2.0f; + + for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) { + if (g_serverBoxes[bi].destroyed) continue; + + Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f); + + for (auto& session : g_sessions) { + { + std::lock_guard gd(g_dead_mutex); + if (g_dead_players.find(session->get_id()) != g_dead_players.end()) { + continue; + } + } + + ClientState shipState; + if (!session->fetchStateAtTime(now, shipState)) continue; + + Eigen::Vector3f diff = shipState.position - boxWorld; + float thresh = shipCollisionRadius + boxCollisionRadius; + + if (diff.squaredNorm() <= thresh * thresh) { + g_serverBoxes[bi].destroyed = true; + + BoxDestroyedInfo destruction; + destruction.boxIndex = static_cast(bi); + destruction.serverTime = now_ms; + destruction.position = boxWorld; + destruction.destroyedBy = session->get_id(); + + { + std::lock_guard dm(g_boxDestructions_mutex); + g_boxDestructions.push_back(destruction); + } + + std::cout << "Server: Box " << bi << " destroyed by ship collision with player " + << session->get_id() << std::endl; + break; + } + } + } + } + + // --- Broadcast deaths --- + for (const auto& death : deathEvents) { + std::string deadMsg = "DEAD:" + + std::to_string(death.serverTime) + ":" + + std::to_string(death.targetId) + ":" + + std::to_string(death.position.x()) + ":" + + std::to_string(death.position.y()) + ":" + + std::to_string(death.position.z()) + ":" + + std::to_string(death.killerId); + + broadcastToAll(deadMsg); + + std::cout << "Server: Sent DEAD event - Player " << death.targetId + << " killed by " << death.killerId << std::endl; + } + + // --- Broadcast box destructions --- + { + std::lock_guard dm(g_boxDestructions_mutex); + for (const auto& destruction : g_boxDestructions) { + std::string boxMsg = "BOX_DESTROYED:" + + std::to_string(destruction.boxIndex) + ":" + + std::to_string(destruction.serverTime) + ":" + + std::to_string(destruction.position.x()) + ":" + + std::to_string(destruction.position.y()) + ":" + + std::to_string(destruction.position.z()) + ":" + + std::to_string(destruction.destroyedBy); + + broadcastToAll(boxMsg); + std::cout << "Server: Broadcasted BOX_DESTROYED for box " << destruction.boxIndex << std::endl; + } + g_boxDestructions.clear(); + } + + // --- Schedule next tick in 50ms --- + timer.expires_after(std::chrono::milliseconds(50)); + timer.async_wait([&timer, &ioc](const boost::system::error_code& ec) { + if (ec) return; update_world(timer, ioc); }); } diff --git a/src/network/ClientState.cpp b/src/network/ClientState.cpp index 578f536..fb6b822 100644 --- a/src/network/ClientState.cpp +++ b/src/network/ClientState.cpp @@ -169,7 +169,7 @@ void ClientState::apply_lag_compensation(std::chrono::system_clock::time_point n while (deltaMsLeftover > 0) { - long long miniDelta = 50; + long long miniDelta = std::min(50LL, deltaMsLeftover); simulate_physics(miniDelta); deltaMsLeftover -= miniDelta; } @@ -207,7 +207,7 @@ void ClientState::handle_full_sync(const std::vector& parts, int st discreteAngle = std::stoi(parts[startFrom + 13]); } -std::string ClientState::formPingMessageContent() +std::string ClientState::formPingMessageContent() const { Eigen::Quaternionf q(rotation); diff --git a/src/network/ClientState.h b/src/network/ClientState.h index c9fb48f..02ab5f5 100644 --- a/src/network/ClientState.h +++ b/src/network/ClientState.h @@ -50,7 +50,7 @@ struct ClientState { void handle_full_sync(const std::vector& parts, int startFrom); - std::string formPingMessageContent(); + std::string formPingMessageContent() const; }; struct ClientStateInterval From 7418bbbe27e37ab7066ad492ea477a3622e9da7c Mon Sep 17 00:00:00 2001 From: Vlad Date: Fri, 27 Feb 2026 17:37:00 +0600 Subject: [PATCH 12/20] fix spaceship rotation fix sync boxes --- server/server.cpp | 10 +++--- src/Space.cpp | 17 ++++++--- src/network/NetworkInterface.h | 2 ++ src/network/WebSocketClientBase.cpp | 56 ++++++++++++++++++++++------- src/network/WebSocketClientBase.h | 6 +++- 5 files changed, 68 insertions(+), 23 deletions(-) diff --git a/server/server.cpp b/server/server.cpp index 1bcd2a6..b981e90 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -160,17 +160,19 @@ private: std::lock_guard lock(g_boxes_mutex); std::string boxMsg = "BOXES:"; - for (const auto& box : g_serverBoxes) { + for (size_t i = 0; i < g_serverBoxes.size(); ++i) { + const auto& box = g_serverBoxes[i]; Eigen::Quaternionf q(box.rotation); - boxMsg += std::to_string(box.position.x()) + ":" + + boxMsg += std::to_string(i) + ":" + + 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()) + "|"; + std::to_string(q.z()) + ":" + + (std::to_string(box.destroyed ? 1 : 0)) + "|"; } - if (!boxMsg.empty() && boxMsg.back() == '|') boxMsg.pop_back(); send_message(boxMsg); diff --git a/src/Space.cpp b/src/Space.cpp index c0944eb..3aafd38 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -331,7 +331,7 @@ namespace ZL auto rotMatrix2 = quat2.toRotationMatrix(); cargoBase.RotateByMatrix(rotMatrix2); //cargoBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix()); - cargoBase.Move(Vector3f{ 1.2, 0, -5 }); + cargoBase.Move(Vector3f{ 0, 0, -5 }); cargo.AssignFrom(cargoBase); cargo.RefreshVBO(); @@ -639,13 +639,15 @@ namespace ZL // Если сервер прислал коробки, применяем их однократно вместо локальной генерации if (!serverBoxesApplied && networkClient) { auto sboxes = networkClient->getServerBoxes(); + auto destroyedFlags = networkClient->getServerBoxDestroyedFlags(); if (!sboxes.empty()) { boxCoordsArr.clear(); - for (auto& b : sboxes) { + boxCoordsArr.resize(sboxes.size()); + for (size_t i = 0; i < sboxes.size(); ++i) { BoxCoords bc; - bc.pos = b.first; - bc.m = b.second; - boxCoordsArr.push_back(bc); + bc.pos = sboxes[i].first; + bc.m = sboxes[i].second; + boxCoordsArr[i] = bc; } boxRenderArr.resize(boxCoordsArr.size()); for (int i = 0; i < (int)boxCoordsArr.size(); ++i) { @@ -653,6 +655,11 @@ namespace ZL boxRenderArr[i].RefreshVBO(); } boxAlive.assign(boxCoordsArr.size(), true); + if (destroyedFlags.size() == boxAlive.size()) { + for (size_t i = 0; i < boxAlive.size(); ++i) { + if (destroyedFlags[i]) boxAlive[i] = false; + } + } serverBoxesApplied = true; } } diff --git a/src/network/NetworkInterface.h b/src/network/NetworkInterface.h index 74dbdad..2c7e704 100644 --- a/src/network/NetworkInterface.h +++ b/src/network/NetworkInterface.h @@ -41,6 +41,8 @@ namespace ZL { virtual std::vector> getServerBoxes() = 0; + virtual std::vector getServerBoxDestroyedFlags() { return {}; } + virtual std::vector getPendingProjectiles() = 0; virtual std::vector getPendingDeaths() = 0; diff --git a/src/network/WebSocketClientBase.cpp b/src/network/WebSocketClientBase.cpp index 52c82a4..c4f74b3 100644 --- a/src/network/WebSocketClientBase.cpp +++ b/src/network/WebSocketClientBase.cpp @@ -41,36 +41,66 @@ namespace ZL { return; } - // Обработка списка коробок от сервера if (msg.rfind("BOXES:", 0) == 0) { - std::string payload = msg.substr(6); // после "BOXES:" - std::vector> parsedBoxes; + std::string payload = msg.substr(6); + std::vector> parsed; if (!payload.empty()) { auto items = split(payload, '|'); for (auto& item : items) { - if (item.empty()) return; + if (item.empty()) continue; auto parts = split(item, ':'); - if (parts.size() < 7) return; + if (parts.size() < 9) { + return; + } try { - float px = std::stof(parts[0]); - float py = std::stof(parts[1]); - float pz = std::stof(parts[2]); + int idx = std::stoi(parts[0]); + float px = std::stof(parts[1]); + float py = std::stof(parts[2]); + float pz = std::stof(parts[3]); Eigen::Quaternionf q( - std::stof(parts[3]), std::stof(parts[4]), std::stof(parts[5]), - std::stof(parts[6]) + std::stof(parts[6]), + std::stof(parts[7]) ); + bool destroyed = (std::stoi(parts[8]) != 0); + Eigen::Matrix3f rot = q.toRotationMatrix(); - parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot); + parsed.emplace_back(idx, Eigen::Vector3f{ px, py, pz }, rot, destroyed); } catch (...) { - // пропускаем некорректную запись return; } } } - serverBoxes_ = std::move(parsedBoxes); + + int maxIdx = -1; + for (auto& t : parsed) { + int idx = std::get<0>(t); + if (idx > maxIdx) maxIdx = idx; + } + if (maxIdx < 0) { + serverBoxes_.clear(); + serverBoxesDestroyed_.clear(); + return; + } + + serverBoxes_.clear(); + serverBoxes_.resize((size_t)maxIdx + 1); + serverBoxesDestroyed_.clear(); + serverBoxesDestroyed_.resize((size_t)maxIdx + 1, true); + + for (auto& t : parsed) { + int idx = std::get<0>(t); + const Eigen::Vector3f& pos = std::get<1>(t); + const Eigen::Matrix3f& rot = std::get<2>(t); + bool destroyed = std::get<3>(t); + if (idx >= 0 && idx < serverBoxes_.size()) { + serverBoxes_[idx] = { pos, rot }; + serverBoxesDestroyed_[idx] = destroyed; + } + } + return; } if (msg.rfind("RESPAWN_ACK:", 0) == 0) { diff --git a/src/network/WebSocketClientBase.h b/src/network/WebSocketClientBase.h index 1ca30cb..8d3f97e 100644 --- a/src/network/WebSocketClientBase.h +++ b/src/network/WebSocketClientBase.h @@ -13,8 +13,8 @@ namespace ZL { protected: std::unordered_map remotePlayers; - // Серверные коробки std::vector> serverBoxes_; + std::vector serverBoxesDestroyed_; std::vector pendingProjectiles_; std::vector pendingDeaths_; @@ -40,6 +40,10 @@ namespace ZL { return serverBoxes_; } + std::vector getServerBoxDestroyedFlags() { + return serverBoxesDestroyed_; + } + std::vector getPendingProjectiles() override; std::vector getPendingDeaths() override; std::vector getPendingRespawns() override; From 9f82e7a2e6d10d3782a22014ad348bebe3b004f7 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Fri, 27 Feb 2026 22:31:07 +0300 Subject: [PATCH 13/20] Adapt for web --- proj-web/CMakeLists.txt | 9 ++- src/Game.cpp | 45 ++++++++++-- src/Game.h | 6 ++ src/MenuManager.cpp | 35 +++++++++- src/Space.cpp | 10 +-- src/main.cpp | 104 +++++++++++++++++++++++++--- src/network/ClientState.cpp | 2 +- src/network/ClientState.h | 6 +- src/network/LocalClient.cpp | 2 +- src/network/LocalClient.h | 2 +- src/network/WebSocketClient.cpp | 2 +- src/network/WebSocketClient.h | 2 +- src/network/WebSocketClientBase.cpp | 2 +- src/network/WebSocketClientBase.h | 2 +- 14 files changed, 189 insertions(+), 40 deletions(-) diff --git a/proj-web/CMakeLists.txt b/proj-web/CMakeLists.txt index e41f118..8b02c39 100644 --- a/proj-web/CMakeLists.txt +++ b/proj-web/CMakeLists.txt @@ -115,12 +115,15 @@ set(EMSCRIPTEN_FLAGS target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2") +# Only loading.png and the shaders used before resources.zip is ready are preloaded. +# resources.zip is downloaded asynchronously at runtime and served as a separate file. set(EMSCRIPTEN_LINK_FLAGS ${EMSCRIPTEN_FLAGS} "-O2" "-sPTHREAD_POOL_SIZE=4" "-sALLOW_MEMORY_GROWTH=1" - "--preload-file resources.zip" + "--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/loading.png@resources/loading.png" + "--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/shaders@resources/shaders" ) # Применяем настройки линковки @@ -170,8 +173,8 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/space-game001plain.html" DESTINATION . ) -# Если вам все еще нужен сам resources.zip отдельно в папке public: -#install(FILES "${RESOURCES_ZIP}" DESTINATION .) +# resources.zip is served separately and downloaded asynchronously at runtime +install(FILES "${RESOURCES_ZIP}" DESTINATION .) add_custom_command(TARGET space-game001 POST_BUILD COMMAND ${CMAKE_COMMAND} --install . diff --git a/src/Game.cpp b/src/Game.cpp index de0dd61..e653d94 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -23,6 +23,10 @@ #endif #endif +#ifdef EMSCRIPTEN +#include +#endif + #include "network/LocalClient.h" #include "network/ClientState.h" @@ -36,6 +40,22 @@ namespace ZL const char* CONST_ZIP_FILE = ""; #endif +#ifdef EMSCRIPTEN + Game* Game::s_instance = nullptr; + + void Game::onResourcesZipLoaded(const char* /*filename*/) { + if (s_instance) { + s_instance->mainThreadHandler.EnqueueMainThreadTask([&]() { + s_instance->setupPart2(); + }); + } + } + + void Game::onResourcesZipError(const char* /*filename*/) { + std::cerr << "Failed to download resources.zip" << std::endl; + } +#endif + Game::Game() : window(nullptr) , glContext(nullptr) @@ -53,7 +73,11 @@ namespace ZL if (window) { SDL_DestroyWindow(window); } +#ifndef EMSCRIPTEN + // In Emscripten, SDL must stay alive across context loss/restore cycles + // so the window remains valid when the game object is re-created. SDL_Quit(); +#endif } void Game::setup() { @@ -64,21 +88,30 @@ namespace ZL renderer.InitOpenGL(); #ifdef EMSCRIPTEN - renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE); - renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE); - + // These shaders and loading.png are preloaded separately (not from zip), + // so they are available immediately without waiting for resources.zip. + renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", ""); + renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", ""); + loadingTexture = std::make_unique(CreateTextureDataFromPng("resources/loading.png", "")); #else renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_desktop.fragment", CONST_ZIP_FILE); + loadingTexture = std::make_unique(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE)); #endif - loadingTexture = std::make_unique(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE)); loadingMesh.data = CreateRect2D({ Environment::width * 0.5, Environment::height * 0.5 }, { Environment::width * 0.5, Environment::height*0.5 }, 3); loadingMesh.RefreshVBO(); +#ifdef EMSCRIPTEN + // Asynchronously download resources.zip; setupPart2() is called on completion. + // The loading screen stays visible until the download finishes. + s_instance = this; + emscripten_async_wget("resources.zip", "resources.zip", onResourcesZipLoaded, onResourcesZipError); +#else mainThreadHandler.EnqueueMainThreadTask([this]() { this->setupPart2(); - }); + }); +#endif } @@ -322,6 +355,8 @@ namespace ZL // Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях Environment::width = event.window.data1; Environment::height = event.window.data2; + std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl; + space.clearTextRendererCache(); } #endif diff --git a/src/Game.h b/src/Game.h index 6e0da91..ab4ab9f 100644 --- a/src/Game.h +++ b/src/Game.h @@ -54,6 +54,12 @@ namespace ZL { void handleUp(int mx, int my); void handleMotion(int mx, int my); +#ifdef EMSCRIPTEN + static Game* s_instance; + static void onResourcesZipLoaded(const char* filename); + static void onResourcesZipError(const char* filename); +#endif + SDL_Window* window; SDL_GLContext glContext; diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index c153cf0..459f33c 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -183,8 +183,8 @@ namespace ZL { } }); - uiManager.setButtonCallback("multiplayerButton2", [this](const std::string& name) { - std::cerr << "Multiplayer button pressed → opening multiplayer menu\n"; + uiManager.setButtonCallback("multiplayerButton2", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) { + /*std::cerr << "Multiplayer button pressed → opening multiplayer menu\n"; uiManager.startAnimationOnNode("playButton", "buttonsExit"); uiManager.startAnimationOnNode("settingsButton", "buttonsExit"); @@ -219,6 +219,37 @@ namespace ZL { } else { std::cerr << "Failed to load multiplayer menu\n"; + }*/ + std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n"; + if (!shipSelectionRoot) { + std::cerr << "Failed to load ship selection UI\n"; + return; + } + if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) { + uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) { + std::string nick = uiManager.getTextFieldValue("nicknameInput"); + if (nick.empty()) nick = "Player"; + int shipType = 0; + uiManager.popMenu(); + loadGameplayUI(); + if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType); + }); + + uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) { + std::string nick = uiManager.getTextFieldValue("nicknameInput"); + if (nick.empty()) nick = "Player"; + int shipType = 1; + uiManager.popMenu(); + loadGameplayUI(); + if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType); + }); + + uiManager.setButtonCallback("backButton", [this](const std::string& btnName) { + uiManager.popMenu(); + }); + } + else { + std::cerr << "Failed to push ship selection menu\n"; } }); uiManager.setButtonCallback("exitButton", [](const std::string& name) { diff --git a/src/Space.cpp b/src/Space.cpp index 3aafd38..fef7a82 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -616,13 +616,11 @@ namespace ZL } void Space::drawRemoteShips() { - // Используем те же константы имен для шейдеров, что и в drawShip static const std::string defaultShaderName = "default"; static const std::string vPositionName = "vPosition"; static const std::string vTexCoordName = "vTexCoord"; static const std::string textureUniformName = "Texture"; - // Активируем шейдер и текстуру (предполагаем, что меш у всех одинаковый) renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); @@ -633,10 +631,6 @@ namespace ZL static_cast(Environment::width) / static_cast(Environment::height), Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); - // Биндим текстуру корабля один раз для ?сех правильных игроков - // ?????????: ?????? ???????? ?????????? ?????? ????? ? ??????????? ?? ClientState.shipType - - // Если сервер прислал коробки, применяем их однократно вместо локальной генерации if (!serverBoxesApplied && networkClient) { auto sboxes = networkClient->getServerBoxes(); auto destroyedFlags = networkClient->getServerBoxDestroyedFlags(); @@ -664,7 +658,6 @@ namespace ZL } } - // Итерируемся по актуальным данным из extrapolateRemotePlayers for (auto const& [id, remotePlayer] : remotePlayerStates) { const ClientState& playerState = remotePlayer; @@ -674,7 +667,7 @@ namespace ZL renderer.LoadIdentity(); renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); - renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset + //renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset renderer.RotateMatrix(Environment::inverseShipMatrix); renderer.TranslateMatrix(-Environment::shipState.position); @@ -682,7 +675,6 @@ namespace ZL Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition; renderer.TranslateMatrix(relativePos); - // 3. Поворот врага renderer.RotateMatrix(playerState.rotation); if (playerState.shipType == 1 && cargoTexture) { diff --git a/src/main.cpp b/src/main.cpp index 57e0b5e..d1bdc1e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,15 +5,87 @@ #include #endif +#ifdef EMSCRIPTEN +#include +#include +#endif + +// For Emscripten the game is heap-allocated so it can be destroyed and +// re-created when the WebGL context is lost and restored (e.g. fullscreen). +// For Android and Desktop a plain global value is used (no context loss). +#ifdef EMSCRIPTEN +ZL::Game* g_game = nullptr; +#else ZL::Game game; +#endif void MainLoop() { +#ifdef EMSCRIPTEN + if (g_game) g_game->update(); +#else game.update(); +#endif } +#ifdef EMSCRIPTEN + +EM_BOOL onWebGLContextLost(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) { + delete g_game; + g_game = nullptr; + return EM_TRUE; +} + +EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) { + g_game = new ZL::Game(); + g_game->setup(); + return EM_TRUE; +} + +// Resize the canvas, notify SDL, and push a synthetic SDL_WINDOWEVENT_RESIZED +// so Game::update()'s existing handler updates Environment::width/height and clears caches. +static void applyResize(int w, int h) { + if (w <= 0 || h <= 0) return; + // Resize the actual WebGL canvas — without this the rendered pixels stay at + // the original size no matter what Environment::width/height say. + emscripten_set_canvas_element_size("#canvas", w, h); + if (ZL::Environment::window) + SDL_SetWindowSize(ZL::Environment::window, w, h); + SDL_Event e = {}; + e.type = SDL_WINDOWEVENT; + e.window.event = SDL_WINDOWEVENT_RESIZED; + e.window.data1 = w; + e.window.data2 = h; + SDL_PushEvent(&e); +} + +EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) { + // Use the event's window dimensions — querying the canvas element would + // return its old fixed size (e.g. 1280x720) before it has been resized. + applyResize(e->windowInnerWidth, e->windowInnerHeight); + return EM_FALSE; +} + +EM_BOOL onFullscreenChanged(int /*eventType*/, const EmscriptenFullscreenChangeEvent* e, void* /*userData*/) { + if (e->isFullscreen) { + // e->screenWidth/screenHeight comes from screen.width/screen.height in JS, + // which on mobile browsers returns physical pixels (e.g. 2340x1080), + // causing the canvas to extend far off-screen. window.innerWidth/innerHeight + // always gives CSS logical pixels and is correct on both desktop and mobile. + int w = EM_ASM_INT({ return window.innerWidth; }); + int h = EM_ASM_INT({ return window.innerHeight; }); + applyResize(w, h); + } + // Exiting fullscreen: the browser fires a window resize event next, + // which onWindowResized handles automatically. + return EM_FALSE; +} + +#endif + + #ifdef __ANDROID__ extern "C" int SDL_main(int argc, char* argv[]) { @@ -34,7 +106,7 @@ extern "C" int SDL_main(int argc, char* argv[]) { __android_log_print(ANDROID_LOG_INFO, "Game", "Display resolution: %dx%d", ZL::Environment::width, ZL::Environment::height); - + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); @@ -52,13 +124,13 @@ extern "C" int SDL_main(int argc, char* argv[]) { ZL::Environment::width, ZL::Environment::height, SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN ); - + if (!ZL::Environment::window) { __android_log_print(ANDROID_LOG_ERROR, "Game", "Failed to create window: %s", SDL_GetError()); SDL_Quit(); return 1; } - + SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window); if (!ctx) { __android_log_print(ANDROID_LOG_ERROR, "Game", "SDL_GL_CreateContext failed: %s", SDL_GetError()); @@ -94,7 +166,7 @@ extern "C" int SDL_main(int argc, char* argv[]) { } return 0; - + } @@ -103,8 +175,8 @@ extern "C" int SDL_main(int argc, char* argv[]) { int main(int argc, char *argv[]) { try { - - + + constexpr int CONST_WIDTH = 1280; constexpr int CONST_HEIGHT = 720; @@ -142,6 +214,20 @@ int main(int argc, char *argv[]) { SDL_GL_MakeCurrent(win, glContext); ZL::Environment::window = win; + + g_game = new ZL::Game(); + g_game->setup(); + + // Re-create the game object when the WebGL context is lost and restored + // (this happens e.g. when the user toggles fullscreen in the browser). + emscripten_set_webglcontextlost_callback("#canvas", nullptr, EM_TRUE, onWebGLContextLost); + emscripten_set_webglcontextrestored_callback("#canvas", nullptr, EM_TRUE, onWebGLContextRestored); + + // Keep Environment::width/height in sync when the canvas is resized. + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized); + emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_FALSE, onFullscreenChanged); + + emscripten_set_main_loop(MainLoop, 0, 1); #else if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { SDL_Log("SDL init failed: %s", SDL_GetError()); @@ -161,13 +247,9 @@ int main(int argc, char *argv[]) { SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window); SDL_GL_MakeCurrent(ZL::Environment::window, ctx); -#endif game.setup(); -#ifdef EMSCRIPTEN - emscripten_set_main_loop(MainLoop, 0, 1); -#else while (!game.shouldExit()) { game.update(); SDL_Delay(2); @@ -182,4 +264,4 @@ int main(int argc, char *argv[]) { return 0; } -#endif \ No newline at end of file +#endif diff --git a/src/network/ClientState.cpp b/src/network/ClientState.cpp index fb6b822..e33c965 100644 --- a/src/network/ClientState.cpp +++ b/src/network/ClientState.cpp @@ -1,4 +1,4 @@ -#include "ClientState.h" +#include "ClientState.h" uint32_t fnv1a_hash(const std::string& data) { uint32_t hash = 0x811c9dc5; diff --git a/src/network/ClientState.h b/src/network/ClientState.h index 02ab5f5..2624679 100644 --- a/src/network/ClientState.h +++ b/src/network/ClientState.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include #define _USE_MATH_DEFINES @@ -19,7 +19,7 @@ constexpr float ROTATION_SENSITIVITY = 0.002f; constexpr float PLANET_RADIUS = 20000.f; constexpr float PLANET_ALIGN_ZONE = 1.05f; -constexpr float PLANET_ANGULAR_ACCEL = 0.01f; // ??????? ??? ???????? +constexpr float PLANET_ANGULAR_ACCEL = 0.01f; constexpr float PLANET_MAX_ANGULAR_VELOCITY = 10.f; constexpr float PITCH_LIMIT = static_cast(M_PI) / 9.f;//18.0f; @@ -41,7 +41,7 @@ struct ClientState { std::string nickname = "Player"; int shipType = 0; - // ??? ??????? ???? + std::chrono::system_clock::time_point lastUpdateServerTime; void simulate_physics(size_t delta); diff --git a/src/network/LocalClient.cpp b/src/network/LocalClient.cpp index d7c08af..0321249 100644 --- a/src/network/LocalClient.cpp +++ b/src/network/LocalClient.cpp @@ -1,4 +1,4 @@ -#include "LocalClient.h" +#include "LocalClient.h" #include #include #include diff --git a/src/network/LocalClient.h b/src/network/LocalClient.h index 5227bf5..438dede 100644 --- a/src/network/LocalClient.h +++ b/src/network/LocalClient.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "NetworkInterface.h" #include diff --git a/src/network/WebSocketClient.cpp b/src/network/WebSocketClient.cpp index bdf0232..d19463a 100644 --- a/src/network/WebSocketClient.cpp +++ b/src/network/WebSocketClient.cpp @@ -1,4 +1,4 @@ -#ifdef NETWORK +#ifdef NETWORK #include "WebSocketClient.h" #include diff --git a/src/network/WebSocketClient.h b/src/network/WebSocketClient.h index 5ccb45c..bd01ba2 100644 --- a/src/network/WebSocketClient.h +++ b/src/network/WebSocketClient.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #ifdef NETWORK diff --git a/src/network/WebSocketClientBase.cpp b/src/network/WebSocketClientBase.cpp index c4f74b3..6253f94 100644 --- a/src/network/WebSocketClientBase.cpp +++ b/src/network/WebSocketClientBase.cpp @@ -1,4 +1,4 @@ -#ifdef NETWORK +#ifdef NETWORK #include "WebSocketClientBase.h" #include diff --git a/src/network/WebSocketClientBase.h b/src/network/WebSocketClientBase.h index 8d3f97e..3784700 100644 --- a/src/network/WebSocketClientBase.h +++ b/src/network/WebSocketClientBase.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "NetworkInterface.h" #include From c20e82634a667f8c8c6acd8e971c8aa81e557b92 Mon Sep 17 00:00:00 2001 From: Vlad Date: Sat, 28 Feb 2026 12:07:40 +0600 Subject: [PATCH 14/20] fix game over --- resources/config/game_over.json | 82 ++++++++++++++++++------- resources/game_over/Container.png | 3 + resources/game_over/Filledbuttons.png | 3 + resources/game_over/FinalScore.png | 3 + resources/game_over/MissionFailed.png | 3 + resources/game_over/Secondarybutton.png | 3 + src/MenuManager.cpp | 11 ++-- src/MenuManager.h | 3 +- src/Space.cpp | 13 ++-- src/Space.h | 1 + 10 files changed, 94 insertions(+), 31 deletions(-) create mode 100644 resources/game_over/Container.png create mode 100644 resources/game_over/Filledbuttons.png create mode 100644 resources/game_over/FinalScore.png create mode 100644 resources/game_over/MissionFailed.png create mode 100644 resources/game_over/Secondarybutton.png diff --git a/resources/config/game_over.json b/resources/config/game_over.json index fe448c6..4fcac9d 100644 --- a/resources/config/game_over.json +++ b/resources/config/game_over.json @@ -14,40 +14,78 @@ { "type": "Button", "name": "gameOverText", - "x": 350, - "y": 400, - "width": 600, - "height": 150, + "x": 476.5, + "y": 500, + "width": 327, + "height": 26, "textures": { - "normal": "resources/gameover.png", - "hover": "resources/gameover.png", - "pressed": "resources/gameover.png" + "normal": "resources/game_over/MissionFailed.png", + "hover": "resources/game_over/MissionFailed.png", + "pressed": "resources/game_over/MissionFailed.png" } }, { "type": "Button", - "name": "restartButton", - "x": 350, - "y": 300, - "width": 300, - "height": 80, + "name": "underlineBtn", + "x": 556, + "y": 465, + "width": 168, + "height": 44, "textures": { - "normal": "resources/shoot_normal.png", - "hover": "resources/shoot_normal.png", - "pressed": "resources/shoot_normal.png" + "normal": "resources/game_over/Container.png", + "hover": "resources/game_over/Container.png", + "pressed": "resources/game_over/Container.png" + } + }, + { + "type": "Button", + "name": "finalscore", + "x": 596.5, + "y": 436, + "width": 87, + "height": 9, + "textures": { + "normal": "resources/game_over/FinalScore.png", + "hover": "resources/game_over/FinalScore.png", + "pressed": "resources/game_over/FinalScore.png" + } + }, + { + "type": "TextView", + "name": "scoreText", + "x": 350, + "y": 356, + "width": 600, + "height": 80, + "text": "0", + "fontSize": 36, + "color": [0, 217, 255, 1], + "align": "center" + }, + { + "type": "Button", + "name": "restartButton", + "x": 449, + "y": 308, + "width": 382, + "height": 56, + "textures": { + "normal": "resources/game_over/Filledbuttons.png", + "hover": "resources/game_over/Filledbuttons.png", + "pressed": "resources/game_over/Filledbuttons.png" } }, { "type": "Button", "name": "gameOverExitButton", - "x": 650, - "y": 300, - "width": 300, - "height": 80, + "x": 449, + "y": 240, + "width": 382, + "height": 56, "textures": { - "normal": "resources/sand2.png", - "hover": "resources/sand2.png", - "pressed": "resources/sand2.png" + "normal": "resources/game_over/Secondarybutton.png", + "hover": "resources/game_over/Secondarybutton.png", + "pressed": "resources/game_over/Secondarybutton.png" } } ] diff --git a/resources/game_over/Container.png b/resources/game_over/Container.png new file mode 100644 index 0000000..226e276 --- /dev/null +++ b/resources/game_over/Container.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d495f21543ab72e6a2cb5082507212616c666bef11bc99bd0447e6906a957836 +size 6709 diff --git a/resources/game_over/Filledbuttons.png b/resources/game_over/Filledbuttons.png new file mode 100644 index 0000000..808344f --- /dev/null +++ b/resources/game_over/Filledbuttons.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6283a169f71822d3c3f8b3c80369cd3ac25b70ef51d3f34685687b1d2819a1b +size 2406 diff --git a/resources/game_over/FinalScore.png b/resources/game_over/FinalScore.png new file mode 100644 index 0000000..7ac0bbc --- /dev/null +++ b/resources/game_over/FinalScore.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bb6e8e59482729d6da84188c830842cf07e4e379076ba64d7916b8b0d45cf09 +size 1244 diff --git a/resources/game_over/MissionFailed.png b/resources/game_over/MissionFailed.png new file mode 100644 index 0000000..1a8731d --- /dev/null +++ b/resources/game_over/MissionFailed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:930565f3911bcb11904808fe67ca278c1460f42f7c7c7dcfffc2301f5c0d408c +size 2678 diff --git a/resources/game_over/Secondarybutton.png b/resources/game_over/Secondarybutton.png new file mode 100644 index 0000000..5bb684a --- /dev/null +++ b/resources/game_over/Secondarybutton.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00a1c3c32da992686febd87918801868d4af2ec99904ed3f80b0c8f58009c5b2 +size 1840 diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index c153cf0..7dc8dc2 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -38,7 +38,7 @@ namespace ZL { uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); static bool isExitButtonAnimating = false; uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() { - std::cerr << "Settings button animation finished -> переход в настройки" << std::endl; + std::cerr << "Settings button animation finished -> ??????? ? ?????????" << std::endl; if (uiManager.pushMenuFromSavedRoot(settingsSavedRoot)) { uiManager.setButtonCallback("Opt1", [this](const std::string& n) { std::cerr << "Opt1 pressed: " << n << std::endl; @@ -184,7 +184,7 @@ namespace ZL { }); uiManager.setButtonCallback("multiplayerButton2", [this](const std::string& name) { - std::cerr << "Multiplayer button pressed → opening multiplayer menu\n"; + std::cerr << "Multiplayer button pressed ? opening multiplayer menu\n"; uiManager.startAnimationOnNode("playButton", "buttonsExit"); uiManager.startAnimationOnNode("settingsButton", "buttonsExit"); @@ -227,14 +227,17 @@ namespace ZL { }); } - void MenuManager::showGameOver() + void MenuManager::showGameOver(int score) { if (!uiGameOverShown) { if (uiManager.pushMenuFromSavedRoot(gameOverSavedRoot)) { + uiManager.setText("scoreText", std::string("Score: ") + std::to_string(score)); + uiManager.setButtonCallback("restartButton", [this](const std::string& name) { + uiManager.setText("scoreText", ""); uiGameOverShown = false; uiManager.popMenu(); - onRestartPressed(); + if (onRestartPressed) onRestartPressed(); }); uiManager.setButtonCallback("gameOverExitButton", [this](const std::string& name) { diff --git a/src/MenuManager.h b/src/MenuManager.h index 2bb4819..0b33f2b 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -28,7 +28,8 @@ namespace ZL { void setupMenu(); - void showGameOver(); + //void showGameOver(); + void showGameOver(int score); std::function onRestartPressed; std::function onVelocityChanged; diff --git a/src/Space.cpp b/src/Space.cpp index 3aafd38..c31f8f4 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -276,7 +276,7 @@ namespace ZL std::cerr << "Client: Failed to send RESPAWN\n"; } } - + this->playerScore = 0; std::cerr << "Game restarted\n"; }; @@ -1606,7 +1606,7 @@ namespace ZL std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n"; - menuManager.showGameOver(); + menuManager.showGameOver(this->playerScore); } else { bool stoneCollided = false; @@ -1681,7 +1681,7 @@ namespace ZL planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty; } - menuManager.showGameOver(); + menuManager.showGameOver(this->playerScore); } } } @@ -1785,12 +1785,17 @@ namespace ZL shipAlive = false; gameOver = true; Environment::shipState.velocity = 0.0f; - menuManager.showGameOver(); + menuManager.showGameOver(this->playerScore); } else { deadRemotePlayers.insert(d.targetId); std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl; } + if (d.killerId == localId) { + this->playerScore += 1; + std::cout << "Client: Increased local score to " << this->playerScore << std::endl; + + } } } diff --git a/src/Space.h b/src/Space.h index dde0f69..894fa39 100644 --- a/src/Space.h +++ b/src/Space.h @@ -122,6 +122,7 @@ namespace ZL { static constexpr float CLOSE_DIST = 600.0f; std::unordered_set deadRemotePlayers; + int playerScore = 0; static constexpr float TARGET_MAX_DIST = 50000.0f; static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST; From 2728ca9646a720798b684fc55f7a2ad56bb2f233 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sat, 28 Feb 2026 20:37:47 +0300 Subject: [PATCH 15/20] Adapting web version --- proj-web/space-game001plain.html | 164 ++++++++---------- resources/Cargo_Base_color_sRGB.png | 4 +- resources/DefaultMaterial_BaseColor_shine.png | 3 - resources/MainCharacter_Base_color_sRGB.png | 4 +- resources/game_over/Container.png | 4 +- resources/game_over/Filledbuttons.png | 4 +- resources/game_over/FinalScore.png | 4 +- resources/game_over/MissionFailed.png | 4 +- resources/game_over/Secondarybutton.png | 4 +- src/Environment.cpp | 20 +++ src/Environment.h | 9 +- src/Game.cpp | 87 +++++----- src/Space.cpp | 54 +++--- src/UiManager.cpp | 2 +- src/main.cpp | 67 ++++--- src/render/TextRenderer.cpp | 7 +- 16 files changed, 234 insertions(+), 207 deletions(-) delete mode 100644 resources/DefaultMaterial_BaseColor_shine.png diff --git a/proj-web/space-game001plain.html b/proj-web/space-game001plain.html index 17708db..72892cd 100644 --- a/proj-web/space-game001plain.html +++ b/proj-web/space-game001plain.html @@ -1,100 +1,76 @@ - - + + + + + + Space Game + + + + +
Downloading...
+ - - - - Space Game - - - - -
-
-
- - - - - \ No newline at end of file + + + + \ No newline at end of file diff --git a/resources/Cargo_Base_color_sRGB.png b/resources/Cargo_Base_color_sRGB.png index 6ec9e8e..58aee2f 100644 --- a/resources/Cargo_Base_color_sRGB.png +++ b/resources/Cargo_Base_color_sRGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bd5d071ed94f2bd8ce3ab060136e9b93b04b06d5eabaffc7845a99d73faeb30 -size 2345111 +oid sha256:88a34dad270df316707d8492823653a4c3143ee948f6c3fcdca232fa3f27a184 +size 2577457 diff --git a/resources/DefaultMaterial_BaseColor_shine.png b/resources/DefaultMaterial_BaseColor_shine.png deleted file mode 100644 index 53e6f41..0000000 --- a/resources/DefaultMaterial_BaseColor_shine.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:26c118a182e269aebfa22684489836d8632b0b2cb6631be646c2678c18493463 -size 90539 diff --git a/resources/MainCharacter_Base_color_sRGB.png b/resources/MainCharacter_Base_color_sRGB.png index 73d132c..ba3e58c 100644 --- a/resources/MainCharacter_Base_color_sRGB.png +++ b/resources/MainCharacter_Base_color_sRGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc8c7262949a4f2f86d2cc47daf572d908294868d89f7577a771149c7e2e60e5 -size 1296067 +oid sha256:6e49da5723497cd4bf152d5f3fd99608f597f2dc3646c05fb478507f36b89697 +size 1669304 diff --git a/resources/game_over/Container.png b/resources/game_over/Container.png index 226e276..e2af5b9 100644 --- a/resources/game_over/Container.png +++ b/resources/game_over/Container.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d495f21543ab72e6a2cb5082507212616c666bef11bc99bd0447e6906a957836 -size 6709 +oid sha256:4292ab255136aeeff003e265bfde42bef4aabb092427bd68f9b2ac42d86916a1 +size 27198 diff --git a/resources/game_over/Filledbuttons.png b/resources/game_over/Filledbuttons.png index 808344f..a9b2806 100644 --- a/resources/game_over/Filledbuttons.png +++ b/resources/game_over/Filledbuttons.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6283a169f71822d3c3f8b3c80369cd3ac25b70ef51d3f34685687b1d2819a1b -size 2406 +oid sha256:436d9137c479b475d8bc753961986ea3f58b6a2439de6f83b5d707172c3f2ff9 +size 7976 diff --git a/resources/game_over/FinalScore.png b/resources/game_over/FinalScore.png index 7ac0bbc..af06f07 100644 --- a/resources/game_over/FinalScore.png +++ b/resources/game_over/FinalScore.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bb6e8e59482729d6da84188c830842cf07e4e379076ba64d7916b8b0d45cf09 -size 1244 +oid sha256:ea4e2a8408fa1b68793fd8d81135902ff15ef2779ea898a99a9ef83ff6e147e8 +size 4142 diff --git a/resources/game_over/MissionFailed.png b/resources/game_over/MissionFailed.png index 1a8731d..ec8d6c3 100644 --- a/resources/game_over/MissionFailed.png +++ b/resources/game_over/MissionFailed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:930565f3911bcb11904808fe67ca278c1460f42f7c7c7dcfffc2301f5c0d408c -size 2678 +oid sha256:c2187818c1fbfb127f70c130f033aa7c16cc3b3a02a2ea59317413437f530c30 +size 9982 diff --git a/resources/game_over/Secondarybutton.png b/resources/game_over/Secondarybutton.png index 5bb684a..37cdae0 100644 --- a/resources/game_over/Secondarybutton.png +++ b/resources/game_over/Secondarybutton.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00a1c3c32da992686febd87918801868d4af2ec99904ed3f80b0c8f58009c5b2 -size 1840 +oid sha256:113c524330190fbdf36f0f7b4ebfc03032170aff5011f8c17201533b52db872f +size 5387 diff --git a/src/Environment.cpp b/src/Environment.cpp index 1fec925..9c41b4d 100644 --- a/src/Environment.cpp +++ b/src/Environment.cpp @@ -36,5 +36,25 @@ ClientState Environment::shipState; const float Environment::CONST_Z_NEAR = 5.f; const float Environment::CONST_Z_FAR = 5000.f; +float Environment::projectionWidth = 1280.0f; +float Environment::projectionHeight = 720.0f; + +void Environment::computeProjectionDimensions() +{ + if (width <= 0 || height <= 0) return; + + const float refShortSide = 720.0f; + float aspect = (float)width / (float)height; + + if (width >= height) { + // Landscape: fix height to 720, scale width to preserve aspect + projectionHeight = refShortSide; + projectionWidth = refShortSide * aspect; + } else { + // Portrait: fix width to 720, scale height to preserve aspect + projectionWidth = refShortSide; + projectionHeight = refShortSide / aspect; + } +} } // namespace ZL diff --git a/src/Environment.h b/src/Environment.h index 5d3c1ca..17125ac 100644 --- a/src/Environment.h +++ b/src/Environment.h @@ -35,8 +35,15 @@ public: static const float CONST_Z_NEAR; static const float CONST_Z_FAR; + // Virtual projection dimensions used for all 2D/UI rendering. + // These maintain the screen's actual aspect ratio but normalize the + // height to 720 (landscape) or width to 720 (portrait), giving a + // consistent coordinate space regardless of physical screen resolution. + static float projectionWidth; + static float projectionHeight; - + // Call this once at startup and whenever the window is resized. + static void computeProjectionDimensions(); }; } // namespace ZL diff --git a/src/Game.cpp b/src/Game.cpp index e653d94..253b475 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -83,6 +83,8 @@ namespace ZL void Game::setup() { glContext = SDL_GL_CreateContext(ZL::Environment::window); + Environment::computeProjectionDimensions(); + ZL::BindOpenGlFunctions(); ZL::CheckGlError(); renderer.InitOpenGL(); @@ -99,7 +101,7 @@ namespace ZL loadingTexture = std::make_unique(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE)); #endif - loadingMesh.data = CreateRect2D({ Environment::width * 0.5, Environment::height * 0.5 }, { Environment::width * 0.5, Environment::height*0.5 }, 3); + loadingMesh.data = CreateRect2D({ Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, { Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, 3); loadingMesh.RefreshVBO(); #ifdef EMSCRIPTEN @@ -141,17 +143,14 @@ namespace ZL Environment::shipState.nickname = nickname; Environment::shipState.shipType = shipType; - networkClient = std::make_unique(); + auto localClient = new LocalClient; + ClientState st = Environment::shipState; + st.id = localClient->GetClientId(); + localClient->setLocalPlayerState(st); + + networkClient = std::unique_ptr(localClient); networkClient->Connect("", 0); -#ifndef NETWORK - auto localClient = dynamic_cast(networkClient.get()); - if (localClient) { - ZL::ClientState st = Environment::shipState; - st.id = localClient->GetClientId(); - localClient->setLocalPlayerState(st); - } -#endif lastTickCount = 0; spaceGameStarted = 1; }; @@ -160,8 +159,7 @@ namespace ZL Environment::shipState.nickname = nickname; Environment::shipState.shipType = shipType; - networkClient = std::make_unique(); -#ifdef NETWORK + #ifdef EMSCRIPTEN networkClient = std::make_unique(); networkClient->Connect("localhost", 8081); @@ -169,19 +167,6 @@ namespace ZL networkClient = std::make_unique(taskManager.getIOContext()); networkClient->Connect("localhost", 8081); #endif -#else - networkClient->Connect("", 0); -#endif - -#ifndef NETWORK - auto localClient = dynamic_cast(networkClient.get()); - if (localClient) { - ZL::ClientState st = Environment::shipState; - st.id = localClient->GetClientId(); - localClient->setLocalPlayerState(st); - } -#endif - if (networkClient) { std::string joinMsg = std::string("JOIN:") + nickname + ":" + std::to_string(shipType); @@ -264,8 +249,8 @@ namespace ZL renderer.EnableVertexAttribArray(vPositionName); renderer.EnableVertexAttribArray(vTexCoordName); - float width = Environment::width; - float height = Environment::height; + float width = Environment::projectionWidth; + float height = Environment::projectionHeight; renderer.PushProjectionMatrix( 0, width, @@ -350,16 +335,18 @@ namespace ZL if (event.type == SDL_QUIT) { Environment::exitGameLoop = true; } -#if SDL_VERSION_ATLEAST(2,0,5) - else if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) { + + + if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) { // Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях Environment::width = event.window.data1; Environment::height = event.window.data2; - std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl; + Environment::computeProjectionDimensions(); + std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl; space.clearTextRendererCache(); } -#endif + #ifdef __ANDROID__ if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) { Environment::exitGameLoop = true; @@ -368,22 +355,38 @@ namespace ZL #ifdef __ANDROID__ if (event.type == SDL_FINGERDOWN) { - int mx = static_cast(event.tfinger.x * Environment::width); - int my = static_cast(event.tfinger.y * Environment::height); + int mx = static_cast(event.tfinger.x * Environment::projectionWidth); + int my = static_cast(event.tfinger.y * Environment::projectionHeight); handleDown(mx, my); } else if (event.type == SDL_FINGERUP) { - int mx = static_cast(event.tfinger.x * Environment::width); - int my = static_cast(event.tfinger.y * Environment::height); + int mx = static_cast(event.tfinger.x * Environment::projectionWidth); + int my = static_cast(event.tfinger.y * Environment::projectionHeight); handleUp(mx, my); } else if (event.type == SDL_FINGERMOTION) { - int mx = static_cast(event.tfinger.x * Environment::width); - int my = static_cast(event.tfinger.y * Environment::height); + int mx = static_cast(event.tfinger.x * Environment::projectionWidth); + int my = static_cast(event.tfinger.y * Environment::projectionHeight); handleMotion(mx, my); } #else - if (event.type == SDL_MOUSEBUTTONDOWN) { + + + if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) { + // Преобразуем экранные пиксели в проекционные единицы + int mx = static_cast((float)event.button.x / Environment::width * Environment::projectionWidth); + int my = static_cast((float)event.button.y / Environment::height * Environment::projectionHeight); + + if (event.type == SDL_MOUSEBUTTONDOWN) handleDown(mx, my); + else handleUp(mx, my); + } + else if (event.type == SDL_MOUSEMOTION) { + int mx = static_cast((float)event.motion.x / Environment::width * Environment::projectionWidth); + int my = static_cast((float)event.motion.y / Environment::height * Environment::projectionHeight); + handleMotion(mx, my); + } + + /*if (event.type == SDL_MOUSEBUTTONDOWN) { int mx = event.button.x; int my = event.button.y; handleDown(mx, my); @@ -397,7 +400,7 @@ namespace ZL int mx = event.motion.x; int my = event.motion.y; handleMotion(mx, my); - } + }*/ if (event.type == SDL_MOUSEWHEEL) { static const float zoomstep = 2.0f; @@ -454,7 +457,7 @@ namespace ZL void Game::handleDown(int mx, int my) { int uiX = mx; - int uiY = Environment::height - my; + int uiY = Environment::projectionHeight - my; menuManager.uiManager.onMouseDown(uiX, uiY); @@ -482,7 +485,7 @@ namespace ZL void Game::handleUp(int mx, int my) { int uiX = mx; - int uiY = Environment::height - my; + int uiY = Environment::projectionHeight - my; menuManager.uiManager.onMouseUp(uiX, uiY); @@ -497,7 +500,7 @@ namespace ZL void Game::handleMotion(int mx, int my) { int uiX = mx; - int uiY = Environment::height - my; + int uiY = Environment::projectionHeight - my; menuManager.uiManager.onMouseMove(uiX, uiY); diff --git a/src/Space.cpp b/src/Space.cpp index 146d7bc..ba82066 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -159,15 +159,15 @@ namespace ZL // В пределах экрана? // (можно оставить, можно клампить) - float sx = (ndc.x() * 0.5f + 0.5f) * Environment::width; - float sy = (ndc.y() * 0.5f + 0.5f) * Environment::height; + float sx = (ndc.x() * 0.5f + 0.5f) * Environment::projectionWidth; + float sy = (ndc.y() * 0.5f + 0.5f) * Environment::projectionHeight; outX = sx; outY = sy; // Можно отсеять те, что вне: - if (sx < -200 || sx > Environment::width + 200) return false; - if (sy < -200 || sy > Environment::height + 200) return false; + if (sx < -200 || sx > Environment::projectionWidth + 200) return false; + if (sy < -200 || sy > Environment::projectionHeight + 200) return false; return true; } @@ -296,12 +296,12 @@ namespace ZL cubemapTexture = std::make_shared( std::array{ - CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), - CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), - CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), - CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), - CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE), - CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE) + CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE), + CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE), + CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE), + CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE), + CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE), + CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE) }); @@ -859,8 +859,8 @@ namespace ZL // если ничего не изменилось — не трогаем VBO if (crosshairMeshValid && - crosshairLastW == Environment::width && - crosshairLastH == Environment::height && + crosshairLastW == Environment::projectionWidth && + crosshairLastH == Environment::projectionHeight && std::abs(crosshairLastAlpha - crosshairCfg.alpha) < 1e-6f && std::abs(crosshairLastThickness - crosshairCfg.thicknessPx) < 1e-6f && std::abs(crosshairLastGap - crosshairCfg.gapPx) < 1e-6f && @@ -869,18 +869,18 @@ namespace ZL return; } - crosshairLastW = Environment::width; - crosshairLastH = Environment::height; + crosshairLastW = Environment::projectionWidth; + crosshairLastH = Environment::projectionHeight; crosshairLastAlpha = crosshairCfg.alpha; crosshairLastThickness = crosshairCfg.thicknessPx; crosshairLastGap = crosshairCfg.gapPx; crosshairLastScaleMul = crosshairCfg.scaleMul; - float cx = Environment::width * 0.5f; - float cy = Environment::height * 0.5f; + float cx = Environment::projectionWidth * 0.5f; + float cy = Environment::projectionHeight * 0.5f; // масштаб от reference (стандартно: по высоте) - float scale = (crosshairCfg.refH > 0) ? (Environment::height / (float)crosshairCfg.refH) : 1.0f; + float scale = (crosshairCfg.refH > 0) ? (Environment::projectionHeight / (float)crosshairCfg.refH) : 1.0f; scale *= crosshairCfg.scaleMul; float thickness = crosshairCfg.thicknessPx * scale; @@ -940,7 +940,7 @@ namespace ZL glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); renderer.shaderManager.PushShader("defaultColor"); - renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); + renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f); renderer.PushMatrix(); renderer.LoadIdentity(); @@ -1187,15 +1187,15 @@ namespace ZL // 4) Настройки стиля Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный - float thickness = 10.0f; // толщина линий (px) + float thickness = 2.0f; // толщина линий (px) float z = 0.0f; // 2D слой // 5) Если цель в кадре: рисуем скобки if (onScreen) { // перевод NDC -> экран (в пикселях) - float sx = (ndcX * 0.5f + 0.5f) * Environment::width; - float sy = (ndcY * 0.5f + 0.5f) * Environment::height; + float sx = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth; + float sy = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight; // анимация “снаружи внутрь” // targetAcquireAnim растёт к 1, быстро (похоже на захват) @@ -1234,7 +1234,7 @@ namespace ZL glClear(GL_DEPTH_BUFFER_BIT); renderer.shaderManager.PushShader("defaultColor"); - renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); + renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f); renderer.PushMatrix(); renderer.LoadIdentity(); @@ -1248,8 +1248,8 @@ namespace ZL float leadNdcX, leadNdcY, leadNdcZ, leadClipW; if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) { if (leadNdcX >= -1 && leadNdcX <= 1 && leadNdcY >= -1 && leadNdcY <= 1) { - float lx = (leadNdcX * 0.5f + 0.5f) * Environment::width; - float ly = (leadNdcY * 0.5f + 0.5f) * Environment::height; + float lx = (leadNdcX * 0.5f + 0.5f) * Environment::projectionWidth; + float ly = (leadNdcY * 0.5f + 0.5f) * Environment::projectionHeight; float distLead = (Environment::shipState.position - leadWorld).norm(); float r = 30.0f / (distLead * 0.01f + 1.0f); @@ -1323,8 +1323,8 @@ namespace ZL float edgeNdcX = dirX * k; float edgeNdcY = dirY * k; - float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::width; - float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height; + float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::projectionWidth; + float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::projectionHeight; float bob = std::sin(t * 6.0f) * 6.0f; edgeX += dirX * bob; @@ -1367,7 +1367,7 @@ namespace ZL glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); renderer.shaderManager.PushShader("defaultColor"); - renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); + renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f); renderer.PushMatrix(); renderer.LoadIdentity(); diff --git a/src/UiManager.cpp b/src/UiManager.cpp index 647d72f..6b3dc95 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -657,7 +657,7 @@ namespace ZL { } void UiManager::draw(Renderer& renderer) { - renderer.PushProjectionMatrix(Environment::width, Environment::height, -1, 1); + renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, -1, 1); renderer.PushMatrix(); renderer.LoadIdentity(); diff --git a/src/main.cpp b/src/main.cpp index d1bdc1e..6b62092 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -44,20 +44,34 @@ EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void return EM_TRUE; } -// Resize the canvas, notify SDL, and push a synthetic SDL_WINDOWEVENT_RESIZED -// so Game::update()'s existing handler updates Environment::width/height and clears caches. -static void applyResize(int w, int h) { - if (w <= 0 || h <= 0) return; - // Resize the actual WebGL canvas — without this the rendered pixels stay at - // the original size no matter what Environment::width/height say. - emscripten_set_canvas_element_size("#canvas", w, h); - if (ZL::Environment::window) - SDL_SetWindowSize(ZL::Environment::window, w, h); +static void applyResize(int logicalW, int logicalH) { + // Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung) + double dpr = emscripten_get_device_pixel_ratio(); + + // Вычисляем реальные физические пиксели + int physicalW = static_cast(logicalW * dpr); + int physicalH = static_cast(logicalH * dpr); + + // Устанавливаем размер внутреннего буфера канваса + emscripten_set_canvas_element_size("#canvas", physicalW, physicalH); + + // Сообщаем SDL о новом размере. + // ВАЖНО: SDL2 в Emscripten ожидает здесь именно физические пиксели + // для корректной работы последующих вызовов glViewport. + if (ZL::Environment::window) { + SDL_SetWindowSize(ZL::Environment::window, physicalW, physicalH); + } + + // Обновляем ваши внутренние переменные окружения + ZL::Environment::width = physicalW; + ZL::Environment::height = physicalH; + + // Пушим событие, чтобы движок пересчитал матрицы проекции SDL_Event e = {}; e.type = SDL_WINDOWEVENT; e.window.event = SDL_WINDOWEVENT_RESIZED; - e.window.data1 = w; - e.window.data2 = h; + e.window.data1 = physicalW; + e.window.data2 = physicalH; SDL_PushEvent(&e); } @@ -69,17 +83,11 @@ EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*u } EM_BOOL onFullscreenChanged(int /*eventType*/, const EmscriptenFullscreenChangeEvent* e, void* /*userData*/) { - if (e->isFullscreen) { - // e->screenWidth/screenHeight comes from screen.width/screen.height in JS, - // which on mobile browsers returns physical pixels (e.g. 2340x1080), - // causing the canvas to extend far off-screen. window.innerWidth/innerHeight - // always gives CSS logical pixels and is correct on both desktop and mobile. - int w = EM_ASM_INT({ return window.innerWidth; }); - int h = EM_ASM_INT({ return window.innerHeight; }); - applyResize(w, h); - } - // Exiting fullscreen: the browser fires a window resize event next, - // which onWindowResized handles automatically. + // Вместо window.innerWidth, попробуйте запросить размер целевого элемента + // так как после перехода в FS именно он растягивается на весь экран. + double clientW, clientH; + emscripten_get_element_css_size("#canvas", &clientW, &clientH); + applyResize(clientW, clientH); return EM_FALSE; } @@ -226,6 +234,21 @@ int main(int argc, char *argv[]) { // Keep Environment::width/height in sync when the canvas is resized. emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized); emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_FALSE, onFullscreenChanged); + + // 2. ИНИЦИАЛИЗАЦИЯ РАЗМЕРОВ: + // Получаем реальные размеры окна браузера на момент запуска + int canvasW = EM_ASM_INT({ return window.innerWidth; }); + int canvasH = EM_ASM_INT({ return window.innerHeight; }); + + // Вызываем вашу функцию — она сама применит DPR, выставит физический размер + // канваса и отправит SDL_WINDOWEVENT_RESIZED для настройки проекции. + applyResize(canvasW, canvasH); + + // 3. Создаем игру и вызываем setup (теперь проекция уже будет знать верный size) + g_game = new ZL::Game(); + g_game->setup(); + + SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); emscripten_set_main_loop(MainLoop, 0, 1); #else diff --git a/src/render/TextRenderer.cpp b/src/render/TextRenderer.cpp index 585ca8d..73e2db1 100644 --- a/src/render/TextRenderer.cpp +++ b/src/render/TextRenderer.cpp @@ -362,9 +362,10 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca // 4. Рендеринг r->shaderManager.PushShader(shaderName); - // Матрица проекции (экрана) - float W = (float)Environment::width; - float H = (float)Environment::height; + // Матрица проекции — используем виртуальные проекционные размеры, + // чтобы координаты текста были независимы от физического разрешения экрана. + float W = Environment::projectionWidth; + float H = Environment::projectionHeight; Eigen::Matrix4f proj = Eigen::Matrix4f::Identity(); proj(0, 0) = 2.0f / W; proj(1, 1) = 2.0f / H; From ffbecbbcde3cdf2c20f425258a804dcea47276e0 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sat, 28 Feb 2026 22:21:08 +0300 Subject: [PATCH 16/20] Working on ui --- resources/config/main_menu.json | 35 +---- src/UiManager.cpp | 242 +++++++++++++++++++++++++++----- src/UiManager.h | 57 +++++++- 3 files changed, 259 insertions(+), 75 deletions(-) diff --git a/resources/config/main_menu.json b/resources/config/main_menu.json index 1807e74..f0f8c8b 100644 --- a/resources/config/main_menu.json +++ b/resources/config/main_menu.json @@ -10,30 +10,17 @@ "type": "LinearLayout", "name": "settingsButtons", "orientation": "vertical", + "vertical_align" : "center", + "horizontal_align" : "center", "spacing": 10, "x": 0, "y": 0, - "width": 300, - "height": 300, + "width": 1280, + "height": 720, "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": { @@ -45,8 +32,6 @@ { "type": "Button", "name": "underlineBtn", - "x": 516, - "y": 465, "width": 168, "height": 44, "textures": { @@ -58,8 +43,6 @@ { "type": "Button", "name": "subtitleBtn", - "x": 528, - "y": 455, "width": 144, "height": 11, "textures": { @@ -71,8 +54,6 @@ { "type": "Button", "name": "singleButton", - "x": 409, - "y": 360, "width": 382, "height": 56, "textures": { @@ -84,8 +65,6 @@ { "type": "Button", "name": "multiplayerButton", - "x": 409, - "y": 289, "width": 382, "height": 56, "textures": { @@ -97,8 +76,6 @@ { "type": "Button", "name": "multiplayerButton2", - "x": 409, - "y": 218, "width": 382, "height": 56, "textures": { @@ -110,8 +87,6 @@ { "type": "Button", "name": "exitButton", - "x": 409, - "y": 147, "width": 382, "height": 56, "textures": { @@ -123,8 +98,6 @@ { "type": "Button", "name": "versionLabel", - "x": 559.5, - "y": 99, "width": 81, "height": 9, "textures": { diff --git a/src/UiManager.cpp b/src/UiManager.cpp index 6b3dc95..80f5f95 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -184,21 +184,54 @@ namespace ZL { std::shared_ptr parseNode(const json& j, Renderer& renderer, const std::string& zipFile) { auto node = std::make_shared(); - if (j.contains("type") && j["type"].is_string()) node->type = j["type"].get(); - if (j.contains("name") && j["name"].is_string()) node->name = j["name"].get(); - if (j.contains("x")) node->rect.x = j["x"].get(); - if (j.contains("y")) node->rect.y = j["y"].get(); - if (j.contains("width")) node->rect.w = j["width"].get(); - if (j.contains("height")) node->rect.h = j["height"].get(); + // 1. Определяем тип контейнера и ориентацию + std::string typeStr = j.value("type", "FrameLayout"); // По умолчанию FrameLayout + if (typeStr == "LinearLayout") { + node->layoutType = LayoutType::Linear; + } + else { + node->layoutType = LayoutType::Frame; + } - if (j.contains("orientation") && j["orientation"].is_string()) node->orientation = j["orientation"].get(); - if (j.contains("spacing")) node->spacing = j["spacing"].get(); + if (j.contains("name")) node->name = j["name"].get(); - if (node->type == "Button") { + // 2. Читаем размеры во временные "локальные" поля + // Это критически важно: мы не пишем сразу в screenRect, + // так как LinearLayout их пересчитает. + node->localX = j.value("x", 0.0f); + node->localY = j.value("y", 0.0f); + node->width = j.value("width", 0.0f); + node->height = j.value("height", 0.0f); + + // 3. Параметры компоновки + if (j.contains("orientation")) { + std::string orient = j["orientation"].get(); + node->orientation = (orient == "horizontal") ? Orientation::Horizontal : Orientation::Vertical; + } + node->spacing = j.value("spacing", 0.0f); + + if (j.contains("horizontal_align")) { + std::string halign = j["horizontal_align"]; + if (halign == "center") node->layoutSettings.hAlign = HorizontalAlign::Center; + else if (halign == "right") node->layoutSettings.hAlign = HorizontalAlign::Right; + } + + if (j.contains("vertical_align")) { + std::string valign = j["vertical_align"]; + if (valign == "center") node->layoutSettings.vAlign = VerticalAlign::Center; + else if (valign == "top") node->layoutSettings.vAlign = VerticalAlign::Top; + } + // Подготавливаем базовый rect для компонентов (кнопок и т.д.) + // На этапе парсинга мы даем им "желаемый" размер + UiRect initialRect = { node->localX, node->localY, node->width, node->height }; + + + + if (typeStr == "Button") { auto btn = std::make_shared(); btn->name = node->name; - btn->rect = node->rect; + btn->rect = initialRect; if (!j.contains("textures") || !j["textures"].is_object()) { std::cerr << "UiManager: Button '" << btn->name << "' missing textures" << std::endl; @@ -225,10 +258,10 @@ namespace ZL { node->button = btn; } - else if (node->type == "Slider") { + else if (typeStr == "Slider") { auto s = std::make_shared(); s->name = node->name; - s->rect = node->rect; + s->rect = initialRect; if (!j.contains("textures") || !j["textures"].is_object()) { std::cerr << "UiManager: Slider '" << s->name << "' missing textures" << std::endl; @@ -261,10 +294,10 @@ namespace ZL { node->slider = s; } - else if (node->type == "TextField") { + else if (typeStr == "TextField") { auto tf = std::make_shared(); tf->name = node->name; - tf->rect = node->rect; + tf->rect = initialRect; if (j.contains("placeholder")) tf->placeholder = j["placeholder"].get(); if (j.contains("fontPath")) tf->fontPath = j["fontPath"].get(); @@ -331,11 +364,11 @@ namespace ZL { } } - if (node->type == "TextView") { + if (typeStr == "TextView") { auto tv = std::make_shared(); - tv->name = node->name; - tv->rect = node->rect; + tv->name = node->name; + tv->rect = initialRect; if (j.contains("text")) tv->text = j["text"].get(); if (j.contains("fontPath")) tv->fontPath = j["fontPath"].get(); if (j.contains("fontSize")) tv->fontSize = j["fontSize"].get(); @@ -400,6 +433,7 @@ namespace ZL { throw std::runtime_error("Failed to load UI file: " + path); } + root = parseNode(j["root"], renderer, zipFile); return root; @@ -407,7 +441,7 @@ namespace ZL { void UiManager::replaceRoot(std::shared_ptr newRoot) { root = newRoot; - layoutNode(root); + layoutNode(root, 0, 0); buttons.clear(); sliders.clear(); textViews.clear(); @@ -433,37 +467,167 @@ namespace ZL { - void UiManager::layoutNode(const std::shared_ptr& node) { + void UiManager::updateLayout(const std::shared_ptr& node, float parentX, float parentY) { + // 1. Сначала определяем позицию самого контейнера + // Если это корень, он обычно занимает весь экран или заданные координаты + node->screenRect.x = parentX + node->localX; + node->screenRect.y = parentY + node->localY; + node->screenRect.w = node->width; + node->screenRect.h = node->height; + + float currentX = node->screenRect.x; + // В UI обычно Y растет сверху вниз, но в нашем OpenGL (Renderer.cpp) + // используется орто-матрица с Y-вверх. Учтем это. + float currentY = node->screenRect.y + node->screenRect.h; + for (auto& child : node->children) { - child->rect.x += node->rect.x; - child->rect.y += node->rect.y; - } - - if (node->type == "LinearLayout") { - std::string orient = node->orientation; - std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower); - - float cursorX = node->rect.x; - float cursorY = node->rect.y; - for (auto& child : node->children) { - if (orient == "horizontal") { - child->rect.x = cursorX; - child->rect.y = node->rect.y; - cursorX += child->rect.w + node->spacing; + if (node->layoutType == LayoutType::Linear) { + if (node->orientation == Orientation::Vertical) { + // Игнорируем child->localX/Y для LinearVertical + currentY -= child->height; // Сдвигаемся вниз на высоту элемента + updateLayout(child, node->screenRect.x, currentY - node->screenRect.y); + currentY -= node->spacing; // Добавляем отступ } else { - child->rect.x = node->rect.x; - child->rect.y = cursorY; - cursorY += child->rect.h + node->spacing; + // Логика для Horizontal... + updateLayout(child, currentX, 0); // упрощенно + currentX += child->width + node->spacing; } - layoutNode(child); + } + else { + // FrameLayout: используем честные localX и localY + updateLayout(child, node->screenRect.x, node->screenRect.y); + } + } + + // После вычисления позиций, нужно обновить меши кнопок (buildMesh) + // на основе вычисленных screenRect. + if (node->button) { + node->button->rect = node->screenRect; + node->button->buildMesh(); + } + } + + void UiManager::layoutNode(const std::shared_ptr& node, float parentX, float parentY) { + // 1. Базовый расчет позиции текущего узла относительно родителя + node->screenRect.x = parentX + node->localX; + node->screenRect.y = parentY + node->localY; + node->screenRect.w = node->width; + node->screenRect.h = node->height; + + if (node->layoutType == LayoutType::Linear) { + float totalContentWidth = 0; + float totalContentHeight = 0; + + // Предварительный расчет занимаемого места всеми детьми + for (size_t i = 0; i < node->children.size(); ++i) { + if (node->orientation == Orientation::Vertical) { + totalContentHeight += node->children[i]->height; + if (i < node->children.size() - 1) totalContentHeight += node->spacing; + } + else { + totalContentWidth += node->children[i]->width; + if (i < node->children.size() - 1) totalContentWidth += node->spacing; + } + } + + // Вычисляем начальные смещения (Offsets) + float startX = 0; + float startY = node->height; // Начинаем сверху (Y растет вверх) + + // Вертикальное выравнивание всего блока внутри контейнера + if (node->orientation == Orientation::Vertical) { + if (node->layoutSettings.vAlign == VerticalAlign::Center) { + startY = (node->height + totalContentHeight) / 2.0f; + } + else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) { + startY = totalContentHeight; + } + } + + // Горизонтальное выравнивание всего блока + if (node->orientation == Orientation::Horizontal) { + if (node->layoutSettings.hAlign == HorizontalAlign::Center) { + startX = (node->width - totalContentWidth) / 2.0f; + } + else if (node->layoutSettings.hAlign == HorizontalAlign::Right) { + startX = node->width - totalContentWidth; + } + } + + float cursorX = startX; + float cursorY = startY; + + for (auto& child : node->children) { + if (node->orientation == Orientation::Vertical) { + cursorY -= child->height; + + // Горизонтальное выравнивание внутри строки (Cross-axis alignment) + float childX = 0; + float freeSpaceX = node->width - child->width; + if (node->layoutSettings.hAlign == HorizontalAlign::Center) childX = freeSpaceX / 2.0f; + else if (node->layoutSettings.hAlign == HorizontalAlign::Right) childX = freeSpaceX; + + child->localX = childX; + child->localY = cursorY; + cursorY -= node->spacing; + } + else { + // Вертикальное выравнивание внутри колонки + float childY = 0; + float freeSpaceY = node->height - child->height; + if (node->layoutSettings.vAlign == VerticalAlign::Center) childY = freeSpaceY / 2.0f; + else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) childY = 0; // Внизу + else childY = freeSpaceY; // Вверху + + child->localX = cursorX; + child->localY = childY; + cursorX += child->width + node->spacing; + } + layoutNode(child, node->screenRect.x, node->screenRect.y); } } else { + // Если Frame, просто идем по детям с их фиксированными координатами for (auto& child : node->children) { - layoutNode(child); + layoutNode(child, node->screenRect.x, node->screenRect.y); } } + + // Обновляем меши визуальных компонентов + syncComponentRects(node); + } + + void UiManager::syncComponentRects(const std::shared_ptr& node) { + if (!node) return; + + // 1. Обновляем кнопку + if (node->button) { + node->button->rect = node->screenRect; + // Если у кнопки есть анимационные смещения, они учитываются внутри buildMesh + // или при рендеринге через Uniform-переменные матрицы модели. + node->button->buildMesh(); + } + + // 2. Обновляем слайдер + if (node->slider) { + node->slider->rect = node->screenRect; + node->slider->buildTrackMesh(); + node->slider->buildKnobMesh(); + } + + // 3. Обновляем текстовое поле (TextView) + if (node->textView) { + node->textView->rect = node->screenRect; + // Если в TextView реализован кэш меша для текста, его нужно обновить здесь + // node->textView->rebuildText(); + } + + // 4. Обновляем поле ввода (TextField) + if (node->textField) { + node->textField->rect = node->screenRect; + // Аналогично для курсора и фонового меша + } } void UiManager::collectButtonsAndSliders(const std::shared_ptr& node) { diff --git a/src/UiManager.h b/src/UiManager.h index 05af33c..88a7082 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -31,6 +31,34 @@ namespace ZL { Pressed }; + enum class LayoutType { + Frame, // Позиционирование по X, Y + Linear // Автоматическое позиционирование + }; + + enum class Orientation { + Vertical, + Horizontal + }; + + enum class HorizontalAlign { + Left, + Center, + Right + }; + + enum class VerticalAlign { + Top, + Center, + Bottom + }; + + // В структуру или класс, отвечающий за LinearLayout (вероятно, это свойства UiNode) + struct LayoutSettings { + HorizontalAlign hAlign = HorizontalAlign::Left; + VerticalAlign vAlign = VerticalAlign::Top; + }; + struct UiButton { std::string name; UiRect rect; @@ -111,21 +139,38 @@ namespace ZL { }; struct UiNode { - std::string type; - UiRect rect; std::string name; + LayoutType layoutType = LayoutType::Frame; + Orientation orientation = Orientation::Vertical; + float spacing = 0.0f; + + LayoutSettings layoutSettings; + + // Внутренние вычисленные координаты для OpenGL + // Именно их мы передаем в Vertex Buffer при buildMesh() + UiRect screenRect; + + // Данные из JSON (желаемые размеры и смещения) + float localX = 0; + float localY = 0; + float width = 0; + float height = 0; + + // Иерархия std::vector> children; + + // Компоненты (только один из них обычно активен для ноды) std::shared_ptr button; std::shared_ptr slider; std::shared_ptr textView; std::shared_ptr textField; - std::string orientation = "vertical"; - float spacing = 0.0f; + // Анимации struct AnimStep { std::string type; float toX = 0.0f; float toY = 0.0f; + float toScale = 1.0f; // Полезно добавить для UI float durationMs = 0.0f; std::string easing = "linear"; }; @@ -202,7 +247,9 @@ namespace ZL { bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function cb); private: - void layoutNode(const std::shared_ptr& node); + void updateLayout(const std::shared_ptr& node, float parentX, float parentY); + void layoutNode(const std::shared_ptr& node, float parentX, float parentY); + void syncComponentRects(const std::shared_ptr& node); void collectButtonsAndSliders(const std::shared_ptr& node); struct ActiveAnim { From 6b4b549b3cf418d88207a617479bfca98e52dd3e Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sat, 28 Feb 2026 23:01:26 +0300 Subject: [PATCH 17/20] Working on UI for web --- resources/config/main_menu.json | 159 +++++++++++++++----------------- src/Game.cpp | 1 + src/Space.cpp | 2 +- src/UiManager.cpp | 144 +++++++++++++++-------------- src/UiManager.h | 4 +- 5 files changed, 151 insertions(+), 159 deletions(-) diff --git a/resources/config/main_menu.json b/resources/config/main_menu.json index f0f8c8b..129d810 100644 --- a/resources/config/main_menu.json +++ b/resources/config/main_menu.json @@ -1,114 +1,103 @@ { "root": { - "type": "FrameLayout", - "x": 0, - "y": 0, - "width": 1280, - "height": 720, - "children": [ - { - "type": "LinearLayout", - "name": "settingsButtons", - "orientation": "vertical", - "vertical_align" : "center", - "horizontal_align" : "center", - "spacing": 10, - "x": 0, - "y": 0, - "width": 1280, - "height": 720, - "children": [ - { - "type": "Button", - "name": "titleBtn", - "width": 254, - "height": 35, - "textures": { + "type": "LinearLayout", + "orientation": "vertical", + "vertical_align": "center", + "horizontal_align": "center", + "spacing": 10, + "x": 0, + "y": 0, + "width": "match_parent", + "height": "match_parent", + "children": [ + { + "type": "Button", + "name": "titleBtn", + "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", - "width": 168, - "height": 44, - "textures": { + } + }, + { + "type": "Button", + "name": "underlineBtn", + "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", - "width": 144, - "height": 11, - "textures": { + } + }, + { + "type": "Button", + "name": "subtitleBtn", + "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", - "width": 382, - "height": 56, - "textures": { + } + }, + { + "type": "Button", + "name": "singleButton", + "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", - "width": 382, - "height": 56, - "textures": { + } + }, + { + "type": "Button", + "name": "multiplayerButton", + "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": "multiplayerButton2", - "width": 382, - "height": 56, - "textures": { + } + }, + { + "type": "Button", + "name": "multiplayerButton2", + "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", - "width": 382, - "height": 56, - "textures": { + } + }, + { + "type": "Button", + "name": "exitButton", + "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", - "width": 81, - "height": 9, - "textures": { + } + }, + { + "type": "Button", + "name": "versionLabel", + "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 +} \ No newline at end of file diff --git a/src/Game.cpp b/src/Game.cpp index 253b475..210d181 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -342,6 +342,7 @@ namespace ZL Environment::width = event.window.data1; Environment::height = event.window.data2; Environment::computeProjectionDimensions(); + menuManager.uiManager.updateAllLayouts(); std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl; space.clearTextRendererCache(); diff --git a/src/Space.cpp b/src/Space.cpp index ba82066..691af98 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -362,7 +362,7 @@ namespace ZL } crosshairCfgLoaded = loadCrosshairConfig("resources/config/crosshair_config.json"); - std::cerr << "[Crosshair] loaded=" << crosshairCfgLoaded + std::cout << "[Crosshair] loaded=" << crosshairCfgLoaded << " enabled=" << crosshairCfg.enabled << " w=" << Environment::width << " h=" << Environment::height << " alpha=" << crosshairCfg.alpha diff --git a/src/UiManager.cpp b/src/UiManager.cpp index 80f5f95..d063248 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -201,9 +201,31 @@ namespace ZL { // так как LinearLayout их пересчитает. node->localX = j.value("x", 0.0f); node->localY = j.value("y", 0.0f); - node->width = j.value("width", 0.0f); - node->height = j.value("height", 0.0f); - + if (j.contains("width")) { + if (j["width"].is_string() && j["width"] == "match_parent") { + node->width = -1.0f; // Наш маркер для match_parent + } + else { + node->width = j["width"].get(); + } + } + else + { + node->width = 0.0f; + } + if (j.contains("height")) { + if (j["height"].is_string() && j["height"] == "match_parent") { + node->height = -1.0f; // Наш маркер для match_parent + } + else { + node->height = j["height"].get(); + } + } + else + { + node->height = 0.0f; + } + // 3. Параметры компоновки if (j.contains("orientation")) { std::string orient = j["orientation"].get(); @@ -220,14 +242,13 @@ namespace ZL { if (j.contains("vertical_align")) { std::string valign = j["vertical_align"]; if (valign == "center") node->layoutSettings.vAlign = VerticalAlign::Center; - else if (valign == "top") node->layoutSettings.vAlign = VerticalAlign::Top; + else if (valign == "bottom") node->layoutSettings.vAlign = VerticalAlign::Bottom; } // Подготавливаем базовый rect для компонентов (кнопок и т.д.) // На этапе парсинга мы даем им "желаемый" размер UiRect initialRect = { node->localX, node->localY, node->width, node->height }; - if (typeStr == "Button") { auto btn = std::make_shared(); btn->name = node->name; @@ -441,7 +462,7 @@ namespace ZL { void UiManager::replaceRoot(std::shared_ptr newRoot) { root = newRoot; - layoutNode(root, 0, 0); + layoutNode(root, 0, 0, Environment::projectionWidth, Environment::projectionHeight); buttons.clear(); sliders.clear(); textViews.clear(); @@ -465,55 +486,18 @@ namespace ZL { replaceRoot(newRoot); } - - void UiManager::updateLayout(const std::shared_ptr& node, float parentX, float parentY) { - // 1. Сначала определяем позицию самого контейнера - // Если это корень, он обычно занимает весь экран или заданные координаты + void UiManager::layoutNode(const std::shared_ptr& node, float parentX, float parentY, float parentW, float parentH) { + node->screenRect.w = (node->width < 0) ? parentW : node->width; + node->screenRect.h = (node->height < 0) ? parentH : node->height; + + // 2. Позиция относительно родителя node->screenRect.x = parentX + node->localX; node->screenRect.y = parentY + node->localY; - node->screenRect.w = node->width; - node->screenRect.h = node->height; - float currentX = node->screenRect.x; - // В UI обычно Y растет сверху вниз, но в нашем OpenGL (Renderer.cpp) - // используется орто-матрица с Y-вверх. Учтем это. - float currentY = node->screenRect.y + node->screenRect.h; - - for (auto& child : node->children) { - if (node->layoutType == LayoutType::Linear) { - if (node->orientation == Orientation::Vertical) { - // Игнорируем child->localX/Y для LinearVertical - currentY -= child->height; // Сдвигаемся вниз на высоту элемента - updateLayout(child, node->screenRect.x, currentY - node->screenRect.y); - currentY -= node->spacing; // Добавляем отступ - } - else { - // Логика для Horizontal... - updateLayout(child, currentX, 0); // упрощенно - currentX += child->width + node->spacing; - } - } - else { - // FrameLayout: используем честные localX и localY - updateLayout(child, node->screenRect.x, node->screenRect.y); - } - } - - // После вычисления позиций, нужно обновить меши кнопок (buildMesh) - // на основе вычисленных screenRect. - if (node->button) { - node->button->rect = node->screenRect; - node->button->buildMesh(); - } - } - - void UiManager::layoutNode(const std::shared_ptr& node, float parentX, float parentY) { - // 1. Базовый расчет позиции текущего узла относительно родителя - node->screenRect.x = parentX + node->localX; - node->screenRect.y = parentY + node->localY; - node->screenRect.w = node->width; - node->screenRect.h = node->height; + // Используем удобные локальные переменные для расчетов детей + float currentW = node->screenRect.w; + float currentH = node->screenRect.h; if (node->layoutType == LayoutType::Linear) { float totalContentWidth = 0; @@ -531,14 +515,12 @@ namespace ZL { } } - // Вычисляем начальные смещения (Offsets) float startX = 0; - float startY = node->height; // Начинаем сверху (Y растет вверх) + float startY = currentH; - // Вертикальное выравнивание всего блока внутри контейнера if (node->orientation == Orientation::Vertical) { if (node->layoutSettings.vAlign == VerticalAlign::Center) { - startY = (node->height + totalContentHeight) / 2.0f; + startY = (currentH + totalContentHeight) / 2.0f; } else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) { startY = totalContentHeight; @@ -548,10 +530,10 @@ namespace ZL { // Горизонтальное выравнивание всего блока if (node->orientation == Orientation::Horizontal) { if (node->layoutSettings.hAlign == HorizontalAlign::Center) { - startX = (node->width - totalContentWidth) / 2.0f; + startX = (currentW - totalContentWidth) / 2.0f; } else if (node->layoutSettings.hAlign == HorizontalAlign::Right) { - startX = node->width - totalContentWidth; + startX = currentW - totalContentWidth; } } @@ -559,12 +541,15 @@ namespace ZL { float cursorY = startY; for (auto& child : node->children) { - if (node->orientation == Orientation::Vertical) { - cursorY -= child->height; - // Горизонтальное выравнивание внутри строки (Cross-axis alignment) + float childW = (child->width < 0) ? currentW : child->width; + float childH = (child->height < 0) ? currentH : child->height; + + if (node->orientation == Orientation::Vertical) { + cursorY -= childH; // используем вычисленный childH + float childX = 0; - float freeSpaceX = node->width - child->width; + float freeSpaceX = currentW - childW; if (node->layoutSettings.hAlign == HorizontalAlign::Center) childX = freeSpaceX / 2.0f; else if (node->layoutSettings.hAlign == HorizontalAlign::Right) childX = freeSpaceX; @@ -573,24 +558,34 @@ namespace ZL { cursorY -= node->spacing; } else { - // Вертикальное выравнивание внутри колонки - float childY = 0; - float freeSpaceY = node->height - child->height; - if (node->layoutSettings.vAlign == VerticalAlign::Center) childY = freeSpaceY / 2.0f; - else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) childY = 0; // Внизу - else childY = freeSpaceY; // Вверху - child->localX = cursorX; + + // Вертикальное выравнивание внутри "строки" (Cross-axis alignment) + float childY = 0; + float freeSpaceY = currentH - childH; + + if (node->layoutSettings.vAlign == VerticalAlign::Center) { + childY = freeSpaceY / 2.0f; + } + else if (node->layoutSettings.vAlign == VerticalAlign::Top) { + childY = freeSpaceY; // Прижимаем к верхнему краю (т.к. Y растет вверх) + } + else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) { + childY = 0; // Прижимаем к нижнему краю + } + child->localY = childY; - cursorX += child->width + node->spacing; + + // Сдвигаем курсор вправо для следующего элемента + cursorX += childW + node->spacing; } - layoutNode(child, node->screenRect.x, node->screenRect.y); + layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH); } } else { // Если Frame, просто идем по детям с их фиксированными координатами for (auto& child : node->children) { - layoutNode(child, node->screenRect.x, node->screenRect.y); + layoutNode(child, node->screenRect.x, node->screenRect.y, node->width, node->height); } } @@ -630,6 +625,13 @@ namespace ZL { } } + void UiManager::updateAllLayouts() { + if (!root) return; + + // Запускаем расчет от корня, передавая размеры экрана как "родительские" + layoutNode(root, 0, 0, Environment::projectionWidth, Environment::projectionHeight); + } + void UiManager::collectButtonsAndSliders(const std::shared_ptr& node) { if (node->button) { buttons.push_back(node->button); diff --git a/src/UiManager.h b/src/UiManager.h index 88a7082..0d2a704 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -245,10 +245,10 @@ namespace ZL { bool startAnimationOnNode(const std::string& nodeName, const std::string& animName); bool stopAnimationOnNode(const std::string& nodeName, const std::string& animName); bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function cb); + void updateAllLayouts(); private: - void updateLayout(const std::shared_ptr& node, float parentX, float parentY); - void layoutNode(const std::shared_ptr& node, float parentX, float parentY); + void layoutNode(const std::shared_ptr& node, float parentX, float parentY, float parentW, float parentH); void syncComponentRects(const std::shared_ptr& node); void collectButtonsAndSliders(const std::shared_ptr& node); From bb0f584bf6d68a3ad76ea78c68ed4869ec80e20e Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sun, 1 Mar 2026 21:22:57 +0300 Subject: [PATCH 18/20] Working on UI and web game --- resources/Cargo_Base_color_sRGB.png | 4 +- resources/MainCharacter_Base_color_sRGB.png | 4 +- resources/config/game_over.json | 174 ++++++------- resources/config/game_over_old.json | 93 +++++++ resources/config/main_menu.json | 22 -- resources/config/ship_selection_menu.json | 119 +++++---- resources/config/ui.json | 239 ++++-------------- resources/config/ui_old.json | 194 ++++++++++++++ resources/multiplayer_menu/ship_cargo.png | 3 + .../multiplayer_menu/ship_cargo_pressed.png | 3 + resources/multiplayer_menu/ship_fighter.png | 3 + .../multiplayer_menu/ship_fighter_pressed.png | 3 + src/MenuManager.cpp | 10 +- src/UiManager.cpp | 61 ++++- src/UiManager.h | 16 +- 15 files changed, 566 insertions(+), 382 deletions(-) create mode 100644 resources/config/game_over_old.json create mode 100644 resources/config/ui_old.json create mode 100644 resources/multiplayer_menu/ship_cargo.png create mode 100644 resources/multiplayer_menu/ship_cargo_pressed.png create mode 100644 resources/multiplayer_menu/ship_fighter.png create mode 100644 resources/multiplayer_menu/ship_fighter_pressed.png diff --git a/resources/Cargo_Base_color_sRGB.png b/resources/Cargo_Base_color_sRGB.png index 58aee2f..493514c 100644 --- a/resources/Cargo_Base_color_sRGB.png +++ b/resources/Cargo_Base_color_sRGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88a34dad270df316707d8492823653a4c3143ee948f6c3fcdca232fa3f27a184 -size 2577457 +oid sha256:d8505521fa1598d9140e518deabcc7c20b226b90a7758e1b1ff5795c9a3b73a5 +size 2890059 diff --git a/resources/MainCharacter_Base_color_sRGB.png b/resources/MainCharacter_Base_color_sRGB.png index ba3e58c..9c074e3 100644 --- a/resources/MainCharacter_Base_color_sRGB.png +++ b/resources/MainCharacter_Base_color_sRGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e49da5723497cd4bf152d5f3fd99608f597f2dc3646c05fb478507f36b89697 -size 1669304 +oid sha256:69a783d983e5356aa0559f0f333ed6a083d4e5c9cd6190bf68a28d122af66ec8 +size 2823280 diff --git a/resources/config/game_over.json b/resources/config/game_over.json index 4fcac9d..6248579 100644 --- a/resources/config/game_over.json +++ b/resources/config/game_over.json @@ -1,93 +1,85 @@ { - "root": { - "type": "LinearLayout", - "orientation": "vertical", - "align": "center", - "x": 0, - "y": 0, - "width": 1920, - "height": 1080, - "background": { - "color": [0, 0, 0, 0.7] - }, - "children": [ - { - "type": "Button", - "name": "gameOverText", - "x": 476.5, - "y": 500, - "width": 327, - "height": 26, - "textures": { - "normal": "resources/game_over/MissionFailed.png", - "hover": "resources/game_over/MissionFailed.png", - "pressed": "resources/game_over/MissionFailed.png" - } - }, - { - "type": "Button", - "name": "underlineBtn", - "x": 556, - "y": 465, - "width": 168, - "height": 44, - "textures": { - "normal": "resources/game_over/Container.png", - "hover": "resources/game_over/Container.png", - "pressed": "resources/game_over/Container.png" - } - }, - { - "type": "Button", - "name": "finalscore", - "x": 596.5, - "y": 436, - "width": 87, - "height": 9, - "textures": { - "normal": "resources/game_over/FinalScore.png", - "hover": "resources/game_over/FinalScore.png", - "pressed": "resources/game_over/FinalScore.png" - } - }, - { - "type": "TextView", - "name": "scoreText", - "x": 350, - "y": 356, - "width": 600, - "height": 80, - "text": "0", - "fontSize": 36, - "color": [0, 217, 255, 1], - "align": "center" - }, - { - "type": "Button", - "name": "restartButton", - "x": 449, - "y": 308, - "width": 382, - "height": 56, - "textures": { - "normal": "resources/game_over/Filledbuttons.png", - "hover": "resources/game_over/Filledbuttons.png", - "pressed": "resources/game_over/Filledbuttons.png" - } - }, - { - "type": "Button", - "name": "gameOverExitButton", - "x": 449, - "y": 240, - "width": 382, - "height": 56, - "textures": { - "normal": "resources/game_over/Secondarybutton.png", - "hover": "resources/game_over/Secondarybutton.png", - "pressed": "resources/game_over/Secondarybutton.png" - } - } - ] - } + "root": { + "type": "LinearLayout", + "orientation": "vertical", + "vertical_align": "center", + "horizontal_align": "center", + "spacing": 10, + "x": 0, + "y": 0, + "width": "match_parent", + "height": "match_parent", + "children": [ + { + "type": "Button", + "name": "gameOverText", + "width": 327, + "height": 26, + "textures": { + "normal": "resources/game_over/MissionFailed.png", + "hover": "resources/game_over/MissionFailed.png", + "pressed": "resources/game_over/MissionFailed.png" + } + }, + { + "type": "Button", + "name": "underlineBtn", + "width": 168, + "height": 44, + "textures": { + "normal": "resources/game_over/Container.png", + "hover": "resources/game_over/Container.png", + "pressed": "resources/game_over/Container.png" + } + }, + { + "type": "Button", + "name": "finalscore", + "width": 87, + "height": 9, + "textures": { + "normal": "resources/game_over/FinalScore.png", + "hover": "resources/game_over/FinalScore.png", + "pressed": "resources/game_over/FinalScore.png" + } + }, + { + "type": "TextView", + "name": "scoreText", + "width": 600, + "height": 80, + "text": "0", + "fontSize": 36, + "color": [ + 0, + 217, + 255, + 1 + ], + "align": "center" + }, + { + "type": "Button", + "name": "restartButton", + "width": 382, + "height": 56, + "textures": { + "normal": "resources/game_over/Filledbuttons.png", + "hover": "resources/game_over/Filledbuttons.png", + "pressed": "resources/game_over/Filledbuttons.png" + } + }, + { + "type": "Button", + "name": "gameOverExitButton", + "width": 382, + "height": 56, + "textures": { + "normal": "resources/game_over/Secondarybutton.png", + "hover": "resources/game_over/Secondarybutton.png", + "pressed": "resources/game_over/Secondarybutton.png" + } + } + ] + } } \ No newline at end of file diff --git a/resources/config/game_over_old.json b/resources/config/game_over_old.json new file mode 100644 index 0000000..4fcac9d --- /dev/null +++ b/resources/config/game_over_old.json @@ -0,0 +1,93 @@ +{ + "root": { + "type": "LinearLayout", + "orientation": "vertical", + "align": "center", + "x": 0, + "y": 0, + "width": 1920, + "height": 1080, + "background": { + "color": [0, 0, 0, 0.7] + }, + "children": [ + { + "type": "Button", + "name": "gameOverText", + "x": 476.5, + "y": 500, + "width": 327, + "height": 26, + "textures": { + "normal": "resources/game_over/MissionFailed.png", + "hover": "resources/game_over/MissionFailed.png", + "pressed": "resources/game_over/MissionFailed.png" + } + }, + { + "type": "Button", + "name": "underlineBtn", + "x": 556, + "y": 465, + "width": 168, + "height": 44, + "textures": { + "normal": "resources/game_over/Container.png", + "hover": "resources/game_over/Container.png", + "pressed": "resources/game_over/Container.png" + } + }, + { + "type": "Button", + "name": "finalscore", + "x": 596.5, + "y": 436, + "width": 87, + "height": 9, + "textures": { + "normal": "resources/game_over/FinalScore.png", + "hover": "resources/game_over/FinalScore.png", + "pressed": "resources/game_over/FinalScore.png" + } + }, + { + "type": "TextView", + "name": "scoreText", + "x": 350, + "y": 356, + "width": 600, + "height": 80, + "text": "0", + "fontSize": 36, + "color": [0, 217, 255, 1], + "align": "center" + }, + { + "type": "Button", + "name": "restartButton", + "x": 449, + "y": 308, + "width": 382, + "height": 56, + "textures": { + "normal": "resources/game_over/Filledbuttons.png", + "hover": "resources/game_over/Filledbuttons.png", + "pressed": "resources/game_over/Filledbuttons.png" + } + }, + { + "type": "Button", + "name": "gameOverExitButton", + "x": 449, + "y": 240, + "width": 382, + "height": 56, + "textures": { + "normal": "resources/game_over/Secondarybutton.png", + "hover": "resources/game_over/Secondarybutton.png", + "pressed": "resources/game_over/Secondarybutton.png" + } + } + ] + } +} \ No newline at end of file diff --git a/resources/config/main_menu.json b/resources/config/main_menu.json index 129d810..55bbb1f 100644 --- a/resources/config/main_menu.json +++ b/resources/config/main_menu.json @@ -65,28 +65,6 @@ "pressed": "resources/main_menu/multi.png" } }, - { - "type": "Button", - "name": "multiplayerButton2", - "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", - "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", diff --git a/resources/config/ship_selection_menu.json b/resources/config/ship_selection_menu.json index 7d953fe..1a305de 100644 --- a/resources/config/ship_selection_menu.json +++ b/resources/config/ship_selection_menu.json @@ -1,64 +1,59 @@ { - "root": { - "name": "shipSelectionRoot", - "type": "node", - "children": [ - - { - "type": "TextField", - "name": "nicknameInput", - "x": 400, - "y": 150, - "width": 400, - "height": 50, - "placeholder": "Enter your nickname", - "fontPath": "resources/fonts/DroidSans.ttf", - "fontSize": 16, - "maxLength": 256, - "color": [122, 156, 198, 1], - "placeholderColor": [122, 156, 198, 1], - "backgroundColor": [15, 29, 51, 1], - "borderColor": [15, 29, 51, 1] - }, - { - "type": "Button", - "name": "spaceshipButton", - "x": 300, - "y": 320, - "width": 200, - "height": 80, - "textures": { - "normal": "resources/multiplayer_menu/JoinServer.png", - "hover": "resources/multiplayer_menu/JoinServer.png", - "pressed": "resources/multiplayer_menu/JoinServer.png" + "root": { + "type": "LinearLayout", + "orientation": "vertical", + "vertical_align": "center", + "horizontal_align": "center", + "spacing": 10, + "x": 0, + "y": 0, + "width": "match_parent", + "height": "match_parent", + "children": [ + { + "type": "LinearLayout", + "orientation": "horizontal", + "vertical_align": "center", + "horizontal_align": "center", + "spacing": 10, + "width": "match_parent", + "height": 260, + "children": [ + { + "type": "Button", + "name": "spaceshipButton", + "width": 256, + "height": 256, + "textures": { + "normal": "resources/multiplayer_menu/ship_fighter.png", + "hover": "resources/multiplayer_menu/ship_fighter_pressed.png", + "pressed": "resources/multiplayer_menu/ship_fighter_pressed.png" + } + }, + { + "type": "Button", + "name": "cargoshipButton", + "width": 256, + "height": 256, + "textures": { + "normal": "resources/multiplayer_menu/ship_cargo.png", + "hover": "resources/multiplayer_menu/ship_cargo_pressed.png", + "pressed": "resources/multiplayer_menu/ship_cargo_pressed.png" + } + } + ] + }, + { + "type": "Button", + "name": "backButton", + "width": 382, + "height": 56, + "textures": { + "normal": "resources/multiplayer_menu/Backbutton.png", + "hover": "resources/multiplayer_menu/Backbutton.png", + "pressed": "resources/multiplayer_menu/Backbutton.png" + } } - }, - { - "type": "Button", - "name": "cargoshipButton", - "x": 700, - "y": 320, - "width": 200, - "height": 80, - "textures": { - "normal": "resources/multiplayer_menu/JoinServer.png", - "hover": "resources/multiplayer_menu/JoinServer.png", - "pressed": "resources/multiplayer_menu/JoinServer.png" - } - }, - { - "type": "Button", - "name": "backButton", - "x": 449, - "y": 280, - "width": 382, - "height": 56, - "textures": { - "normal": "resources/multiplayer_menu/Backbutton.png", - "hover": "resources/multiplayer_menu/Backbutton.png", - "pressed": "resources/multiplayer_menu/Backbutton.png" - } - } - ] - } - } \ No newline at end of file + ] + } +} \ No newline at end of file diff --git a/resources/config/ui.json b/resources/config/ui.json index f8f1f7d..fa27292 100644 --- a/resources/config/ui.json +++ b/resources/config/ui.json @@ -1,194 +1,57 @@ { "root": { - "type": "FrameLayout", - "x": 0, - "y": 0, - "width": 1280, - "height": 720, - "children": [ - { - "type": "FrameLayout", - "name": "leftPanel", - "x": 100, - "y": 100, - "width": 320, - "height": 400, - "children": [ + "type": "FrameLayout", + "x": 0, + "y": 0, + "width": "match_parent", + "height": "match_parent", + "children": [ { - "type": "LinearLayout", - "name": "mainButtons", - "orientation": "vertical", - "spacing": 10, - "x": 0, - "y": 0, - "width": 300, - "height": 300, - "children": [ - { - "type": "Button", - "name": "playButton", - "x": -1000, - "y": 500, - "width": 200, - "height": 50, - "animations": { - "buttonsExit": { - "repeat": false, - "steps": [ - { - "type": "move", - "to": [ - -400, - 0 - ], - "duration": 1.0, - "easing": "easein" - } - ] - } - }, - "textures": { - "normal": "./resources/sand2.png", - "hover": "./resources/sand2.png", - "pressed": "./resources/sand2.png" - } - }, - { - "type": "Button", - "name": "settingsButton", - "x": -1000, - "y": 400, - "width": 200, - "height": 50, - "animations": { - "buttonsExit": { - "repeat": false, - "steps": [ - { - "type": "wait", - "duration": 0.5 - }, - { - "type": "move", - "to": [ - -400, - 0 - ], - "duration": 1.0, - "easing": "easein" - } - ] - } - }, - "textures": { - "normal": "./resources/sand2.png", - "hover": "./resources/sand2.png", - "pressed": "./resources/sand2.png" - } - }, - { - "type": "Button", - "name": "exitButton", - "x": -1000, - "y": 300, - "width": 200, - "height": 50, - "animations": { - "buttonsExit": { - "repeat": false, - "steps": [ - { - "type": "wait", - "duration": 1.0 - }, - { - "type": "move", - "to": [ - -400, - 0 - ], - "duration": 1.0, - "easing": "easein" - } - ] - }, - "bgScroll": { - "repeat": true, - "steps": [ - { - "type": "move", - "to": [ - 1280, - 0 - ], - "duration": 5.0, - "easing": "linear" - } - ] - } - }, - "textures": { - "normal": "./resources/sand2.png", - "hover": "./resources/sand2.png", - "pressed": "./resources/sand2.png" - } + "type": "Button", + "name": "shootButton", + "x": 0, + "y": 0, + "width": 150, + "height": 150, + "horizontal_gravity": "right", + "vertical_gravity": "bottom", + "textures": { + "normal": "resources/shoot_normal.png", + "hover": "resources/shoot_hover.png", + "pressed": "resources/shoot_pressed.png" + } + }, + { + "type": "Button", + "name": "shootButton2", + "x": 0, + "y": 0, + "width": 150, + "height": 150, + "horizontal_gravity": "left", + "vertical_gravity": "bottom", + "textures": { + "normal": "resources/shoot_normal.png", + "hover": "resources/shoot_hover.png", + "pressed": "resources/shoot_pressed.png" + } + }, + { + "type": "Slider", + "name": "velocitySlider", + "x": 10, + "y": 200, + "width": 80, + "height": 300, + "value": 0.0, + "orientation": "vertical", + "horizontal_gravity": "right", + "vertical_gravity": "bottom", + "textures": { + "track": "resources/velocitySliderTexture.png", + "knob": "resources/velocitySliderButton.png" } - ] } - ] - }, - { - "type": "Slider", - "name": "velocitySlider", - "x": 1140, - "y": 300, - "width": 50, - "height": 300, - "value": 0.0, - "orientation": "vertical", - "textures": { - "track": "resources/velocitySliderTexture.png", - "knob": "resources/velocitySliderButton.png" - } - }, - { - "type": "Button", - "name": "shootButton", - "x": 100, - "y": 100, - "width": 100, - "height": 100, - "textures": { - "normal": "resources/shoot_normal.png", - "hover": "resources/shoot_hover.png", - "pressed": "resources/shoot_pressed.png" - } - }, - { - "type": "Button", - "name": "shootButton2", - "x": 1000, - "y": 100, - "width": 100, - "height": 100, - "textures": { - "normal": "resources/shoot_normal.png", - "hover": "resources/shoot_hover.png", - "pressed": "resources/shoot_pressed.png" - } - }, - { - "type": "TextView", - "name": "velocityText", - "x": 10, - "y": 10, - "width": 200, - "height": 40, - "text": "Velocity: 0", - "fontSize": 24, - "color": [1.0, 1.0, 1.0, 1.0], - "centered": false - } - ] + ] } - } \ No newline at end of file +} \ No newline at end of file diff --git a/resources/config/ui_old.json b/resources/config/ui_old.json new file mode 100644 index 0000000..f8f1f7d --- /dev/null +++ b/resources/config/ui_old.json @@ -0,0 +1,194 @@ +{ + "root": { + "type": "FrameLayout", + "x": 0, + "y": 0, + "width": 1280, + "height": 720, + "children": [ + { + "type": "FrameLayout", + "name": "leftPanel", + "x": 100, + "y": 100, + "width": 320, + "height": 400, + "children": [ + { + "type": "LinearLayout", + "name": "mainButtons", + "orientation": "vertical", + "spacing": 10, + "x": 0, + "y": 0, + "width": 300, + "height": 300, + "children": [ + { + "type": "Button", + "name": "playButton", + "x": -1000, + "y": 500, + "width": 200, + "height": 50, + "animations": { + "buttonsExit": { + "repeat": false, + "steps": [ + { + "type": "move", + "to": [ + -400, + 0 + ], + "duration": 1.0, + "easing": "easein" + } + ] + } + }, + "textures": { + "normal": "./resources/sand2.png", + "hover": "./resources/sand2.png", + "pressed": "./resources/sand2.png" + } + }, + { + "type": "Button", + "name": "settingsButton", + "x": -1000, + "y": 400, + "width": 200, + "height": 50, + "animations": { + "buttonsExit": { + "repeat": false, + "steps": [ + { + "type": "wait", + "duration": 0.5 + }, + { + "type": "move", + "to": [ + -400, + 0 + ], + "duration": 1.0, + "easing": "easein" + } + ] + } + }, + "textures": { + "normal": "./resources/sand2.png", + "hover": "./resources/sand2.png", + "pressed": "./resources/sand2.png" + } + }, + { + "type": "Button", + "name": "exitButton", + "x": -1000, + "y": 300, + "width": 200, + "height": 50, + "animations": { + "buttonsExit": { + "repeat": false, + "steps": [ + { + "type": "wait", + "duration": 1.0 + }, + { + "type": "move", + "to": [ + -400, + 0 + ], + "duration": 1.0, + "easing": "easein" + } + ] + }, + "bgScroll": { + "repeat": true, + "steps": [ + { + "type": "move", + "to": [ + 1280, + 0 + ], + "duration": 5.0, + "easing": "linear" + } + ] + } + }, + "textures": { + "normal": "./resources/sand2.png", + "hover": "./resources/sand2.png", + "pressed": "./resources/sand2.png" + } + } + ] + } + ] + }, + { + "type": "Slider", + "name": "velocitySlider", + "x": 1140, + "y": 300, + "width": 50, + "height": 300, + "value": 0.0, + "orientation": "vertical", + "textures": { + "track": "resources/velocitySliderTexture.png", + "knob": "resources/velocitySliderButton.png" + } + }, + { + "type": "Button", + "name": "shootButton", + "x": 100, + "y": 100, + "width": 100, + "height": 100, + "textures": { + "normal": "resources/shoot_normal.png", + "hover": "resources/shoot_hover.png", + "pressed": "resources/shoot_pressed.png" + } + }, + { + "type": "Button", + "name": "shootButton2", + "x": 1000, + "y": 100, + "width": 100, + "height": 100, + "textures": { + "normal": "resources/shoot_normal.png", + "hover": "resources/shoot_hover.png", + "pressed": "resources/shoot_pressed.png" + } + }, + { + "type": "TextView", + "name": "velocityText", + "x": 10, + "y": 10, + "width": 200, + "height": 40, + "text": "Velocity: 0", + "fontSize": 24, + "color": [1.0, 1.0, 1.0, 1.0], + "centered": false + } + ] + } + } \ No newline at end of file diff --git a/resources/multiplayer_menu/ship_cargo.png b/resources/multiplayer_menu/ship_cargo.png new file mode 100644 index 0000000..88a6f68 --- /dev/null +++ b/resources/multiplayer_menu/ship_cargo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c86eb962f4abf04aacf7ebbf07433611e9afed42b0f7806fc892ae4a81b45b58 +size 13296 diff --git a/resources/multiplayer_menu/ship_cargo_pressed.png b/resources/multiplayer_menu/ship_cargo_pressed.png new file mode 100644 index 0000000..30590cf --- /dev/null +++ b/resources/multiplayer_menu/ship_cargo_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97d66a6cd037fd75d6f78e72d37b14362683a0de38557470806db53d5e5675d9 +size 13694 diff --git a/resources/multiplayer_menu/ship_fighter.png b/resources/multiplayer_menu/ship_fighter.png new file mode 100644 index 0000000..4c6ea5f --- /dev/null +++ b/resources/multiplayer_menu/ship_fighter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:072bf292acb2760e1ce0ab6c783eca114614d95a9451ba3c86088eee9d824b43 +size 29780 diff --git a/resources/multiplayer_menu/ship_fighter_pressed.png b/resources/multiplayer_menu/ship_fighter_pressed.png new file mode 100644 index 0000000..c0628eb --- /dev/null +++ b/resources/multiplayer_menu/ship_fighter_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7a120a716261b122f6a90a04d7a9bbf1c9582e8c9c114e633bc348c15f7d002 +size 29926 diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index b87a72e..c5b072d 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -96,6 +96,7 @@ namespace ZL { } }); + uiManager.setButtonCallback("shootButton", [this](const std::string& name) { onFirePressed(); }); @@ -103,6 +104,7 @@ namespace ZL { onFirePressed(); }); uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) { + int newVel = roundf(value * 10); if (newVel > 2) { @@ -183,8 +185,8 @@ namespace ZL { } }); - uiManager.setButtonCallback("multiplayerButton2", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) { - /*std::cerr << "Multiplayer button pressed → opening multiplayer menu\n"; + /*uiManager.setButtonCallback("multiplayerButton2", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) { + std::cerr << "Multiplayer button pressed → opening multiplayer menu\n"; uiManager.startAnimationOnNode("playButton", "buttonsExit"); uiManager.startAnimationOnNode("settingsButton", "buttonsExit"); @@ -219,7 +221,7 @@ namespace ZL { } else { std::cerr << "Failed to load multiplayer menu\n"; - }*/ + } std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n"; if (!shipSelectionRoot) { std::cerr << "Failed to load ship selection UI\n"; @@ -255,7 +257,7 @@ namespace ZL { uiManager.setButtonCallback("exitButton", [](const std::string& name) { std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n"; Environment::exitGameLoop = true; - }); + });*/ } void MenuManager::showGameOver(int score) diff --git a/src/UiManager.cpp b/src/UiManager.cpp index d063248..f06e47f 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -244,6 +244,20 @@ namespace ZL { if (valign == "center") node->layoutSettings.vAlign = VerticalAlign::Center; else if (valign == "bottom") node->layoutSettings.vAlign = VerticalAlign::Bottom; } + + if (j.contains("horizontal_gravity")) { + std::string hg = j["horizontal_gravity"].get(); + if (hg == "right") node->layoutSettings.hGravity = HorizontalGravity::Right; + else node->layoutSettings.hGravity = HorizontalGravity::Left; + } + + // Читаем Vertical Gravity + if (j.contains("vertical_gravity")) { + std::string vg = j["vertical_gravity"].get(); + if (vg == "bottom") node->layoutSettings.vGravity = VerticalGravity::Bottom; + else node->layoutSettings.vGravity = VerticalGravity::Top; + } + // Подготавливаем базовый rect для компонентов (кнопок и т.д.) // На этапе парсинга мы даем им "желаемый" размер UiRect initialRect = { node->localX, node->localY, node->width, node->height }; @@ -462,7 +476,14 @@ namespace ZL { void UiManager::replaceRoot(std::shared_ptr newRoot) { root = newRoot; - layoutNode(root, 0, 0, Environment::projectionWidth, Environment::projectionHeight); + layoutNode( + root, + 0.0f, 0.0f, // parentX, parentY (экран начинается с 0,0) + Environment::projectionWidth, // parentW + Environment::projectionHeight, // parentH + root->localX, // finalLocalX + root->localY // finalLocalY + ); buttons.clear(); sliders.clear(); textViews.clear(); @@ -487,15 +508,15 @@ namespace ZL { } - void UiManager::layoutNode(const std::shared_ptr& node, float parentX, float parentY, float parentW, float parentH) { + void UiManager::layoutNode(const std::shared_ptr& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY) { + node->screenRect.w = (node->width < 0) ? parentW : node->width; node->screenRect.h = (node->height < 0) ? parentH : node->height; - // 2. Позиция относительно родителя - node->screenRect.x = parentX + node->localX; - node->screenRect.y = parentY + node->localY; + // ТЕПЕРЬ используем переданные координаты, а не node->localX напрямую + node->screenRect.x = parentX + finalLocalX; + node->screenRect.y = parentY + finalLocalY; - // Используем удобные локальные переменные для расчетов детей float currentW = node->screenRect.w; float currentH = node->screenRect.h; @@ -579,13 +600,26 @@ namespace ZL { // Сдвигаем курсор вправо для следующего элемента cursorX += childW + node->spacing; } - layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH); + layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, child->localX, child->localY); } } else { - // Если Frame, просто идем по детям с их фиксированными координатами for (auto& child : node->children) { - layoutNode(child, node->screenRect.x, node->screenRect.y, node->width, node->height); + float childW = (child->width < 0) ? currentW : child->width; + float childH = (child->height < 0) ? currentH : child->height; + + float fLX = child->localX; + float fLY = child->localY; + + if (child->layoutSettings.hGravity == HorizontalGravity::Right) { + fLX = currentW - childW - child->localX; + } + if (child->layoutSettings.vGravity == VerticalGravity::Top) { + fLY = currentH - childH - child->localY; + } + + // Передаем рассчитанные fLX, fLY в рекурсию + layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, fLX, fLY); } } @@ -629,7 +663,14 @@ namespace ZL { if (!root) return; // Запускаем расчет от корня, передавая размеры экрана как "родительские" - layoutNode(root, 0, 0, Environment::projectionWidth, Environment::projectionHeight); + layoutNode( + root, + 0.0f, 0.0f, // parentX, parentY (экран начинается с 0,0) + Environment::projectionWidth, // parentW + Environment::projectionHeight, // parentH + root->localX, // finalLocalX + root->localY // finalLocalY + ); } void UiManager::collectButtonsAndSliders(const std::shared_ptr& node) { diff --git a/src/UiManager.h b/src/UiManager.h index 0d2a704..59743d2 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -53,10 +53,24 @@ namespace ZL { Bottom }; + enum class HorizontalGravity { + Left, + Right + }; + + enum class VerticalGravity { + Bottom, // Обычно в OpenGL Y растет вверх, так что низ - это 0 + Top + }; + + // В структуру или класс, отвечающий за LinearLayout (вероятно, это свойства UiNode) struct LayoutSettings { HorizontalAlign hAlign = HorizontalAlign::Left; VerticalAlign vAlign = VerticalAlign::Top; + + HorizontalGravity hGravity = HorizontalGravity::Left; + VerticalGravity vGravity = VerticalGravity::Top; }; struct UiButton { @@ -248,7 +262,7 @@ namespace ZL { void updateAllLayouts(); private: - void layoutNode(const std::shared_ptr& node, float parentX, float parentY, float parentW, float parentH); + void layoutNode(const std::shared_ptr& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY); void syncComponentRects(const std::shared_ptr& node); void collectButtonsAndSliders(const std::shared_ptr& node); From 70a617b688624b134a038c5f2f0ed21183d697cb Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sun, 1 Mar 2026 22:34:12 +0300 Subject: [PATCH 19/20] More changes --- resources/spark2.png | 3 +++ src/Projectile.cpp | 2 +- src/Projectile.h | 3 +++ src/Space.cpp | 38 +++++++++++++++++++++++++++---------- src/Space.h | 2 +- src/SparkEmitter.cpp | 17 +++++++++++++++++ src/SparkEmitter.h | 3 ++- src/network/ClientState.h | 8 ++++++++ src/network/LocalClient.cpp | 11 +++-------- 9 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 resources/spark2.png diff --git a/resources/spark2.png b/resources/spark2.png new file mode 100644 index 0000000..b0e806f --- /dev/null +++ b/resources/spark2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11cde0c85c95f91eb9ace65cead22a37ba80d215897f32a2d6be1410210c1acf +size 2656 diff --git a/src/Projectile.cpp b/src/Projectile.cpp index 419419d..9f98d4f 100644 --- a/src/Projectile.cpp +++ b/src/Projectile.cpp @@ -41,7 +41,7 @@ namespace ZL { } void Projectile::rebuildMesh(Renderer&) { - float half = size * 0.5f; + float half = 10 * size * 0.5f; mesh.data.PositionData.clear(); mesh.data.TexCoordData.clear(); diff --git a/src/Projectile.h b/src/Projectile.h index 872177f..e72552e 100644 --- a/src/Projectile.h +++ b/src/Projectile.h @@ -3,6 +3,7 @@ #include "render/Renderer.h" #include "render/TextureManager.h" #include +#include "SparkEmitter.h" namespace ZL { @@ -19,6 +20,8 @@ namespace ZL { Vector3f getPosition() const { return pos; } void deactivate() { active = false; } + + SparkEmitter projectileEmitter; private: Vector3f pos; Vector3f vel; diff --git a/src/Space.cpp b/src/Space.cpp index 691af98..3639912 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -335,6 +335,7 @@ namespace ZL cargo.AssignFrom(cargoBase); cargo.RefreshVBO(); + //projectileTexture = std::make_shared(CreateTextureDataFromPng("resources/spark2.png", CONST_ZIP_FILE)); //Boxes boxTexture = std::make_unique(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE)); @@ -490,13 +491,25 @@ namespace ZL glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + renderer.shaderManager.PushShader("default"); + renderer.RenderUniform1i(textureUniformName, 0); + renderer.EnableVertexAttribArray(vPositionName); + renderer.EnableVertexAttribArray(vTexCoordName); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE); for (const auto& p : projectiles) { if (p && p->isActive()) { p->draw(renderer); + p->projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); } } + glDisable(GL_BLEND); - projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); + renderer.DisableVertexAttribArray(vPositionName); + renderer.DisableVertexAttribArray(vTexCoordName); + renderer.shaderManager.PopShader(); + + //projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); if (shipAlive) { renderer.PushMatrix(); @@ -1124,7 +1137,7 @@ namespace ZL // Lead Indicator // скорость пули (как в fireProjectiles) - const float projectileSpeed = 60.0f; + const float projectileSpeed = PROJECTILE_VELOCITY; // позиция вылета Vector3f shooterPos = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0f, 0.9f - 6.0f, 5.0f }; @@ -1540,22 +1553,25 @@ namespace ZL } } - std::vector projCameraPoints; + for (const auto& p : projectiles) { if (p && p->isActive()) { Vector3f worldPos = p->getPosition(); Vector3f rel = worldPos - Environment::shipState.position; Vector3f camPos = Environment::inverseShipMatrix * rel; - projCameraPoints.push_back(camPos); + p->projectileEmitter.setEmissionPoints({ camPos }); + p->projectileEmitter.emit(); + p->projectileEmitter.update(static_cast(delta)); } } + /* if (!projCameraPoints.empty()) { projectileEmitter.setEmissionPoints(projCameraPoints); projectileEmitter.emit(); } else { projectileEmitter.setEmissionPoints(std::vector()); - } + }*/ std::vector shipCameraPoints; for (const auto& lp : shipLocalEmissionPoints) { @@ -1567,7 +1583,7 @@ namespace ZL } sparkEmitter.update(static_cast(delta)); - projectileEmitter.update(static_cast(delta)); + //projectileEmitter.update(static_cast(delta)); explosionEmitter.update(static_cast(delta)); if (showExplosion) { @@ -1696,8 +1712,8 @@ namespace ZL Vector3f{ 1.5f, 0.9f - 6.f, 5.0f } }; - const float projectileSpeed = 60.0f; - const float lifeMs = 50000.0f; + const float projectileSpeed = PROJECTILE_VELOCITY; + const float lifeMs = PROJECTILE_LIFE; const float size = 0.5f; Vector3f localForward = { 0,0,-1 }; @@ -1710,6 +1726,7 @@ namespace ZL for (auto& p : projectiles) { if (!p->isActive()) { p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer); + p->projectileEmitter = SparkEmitter(projectileEmitter); break; } } @@ -1721,8 +1738,8 @@ namespace ZL if (networkClient) { auto pending = networkClient->getPendingProjectiles(); if (!pending.empty()) { - const float projectileSpeed = 60.0f; - const float lifeMs = 5000.0f; + const float projectileSpeed = PROJECTILE_VELOCITY; + const float lifeMs = PROJECTILE_LIFE; const float size = 0.5f; for (const auto& pi : pending) { const std::vector localOffsets = { @@ -1747,6 +1764,7 @@ namespace ZL for (auto& p : projectiles) { if (!p->isActive()) { p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer); + p->projectileEmitter = SparkEmitter(projectileEmitter); break; } } diff --git a/src/Space.h b/src/Space.h index 894fa39..b38b98e 100644 --- a/src/Space.h +++ b/src/Space.h @@ -96,7 +96,7 @@ namespace ZL { std::shared_ptr projectileTexture; float projectileCooldownMs = 500.0f; int64_t lastProjectileFireTime = 0; - int maxProjectiles = 32; + int maxProjectiles = 500; std::vector shipLocalEmissionPoints; diff --git a/src/SparkEmitter.cpp b/src/SparkEmitter.cpp index 05f686c..5f70710 100644 --- a/src/SparkEmitter.cpp +++ b/src/SparkEmitter.cpp @@ -25,6 +25,23 @@ namespace ZL { sparkQuad.data = VertexDataStruct(); } + SparkEmitter::SparkEmitter(const SparkEmitter& copyFrom) + : particles(copyFrom.particles), emissionPoints(copyFrom.emissionPoints), + lastEmissionTime(copyFrom.lastEmissionTime), emissionRate(copyFrom.emissionRate), + isActive(copyFrom.isActive), drawPositions(copyFrom.drawPositions), + drawTexCoords(copyFrom.drawTexCoords), drawDataDirty(copyFrom.drawDataDirty), + sparkQuad(copyFrom.sparkQuad), texture(copyFrom.texture), + maxParticles(copyFrom.maxParticles), particleSize(copyFrom.particleSize), + biasX(copyFrom.biasX), speedRange(copyFrom.speedRange), + zSpeedRange(copyFrom.zSpeedRange), + scaleRange(copyFrom.scaleRange), + lifeTimeRange(copyFrom.lifeTimeRange), + shaderProgramName(copyFrom.shaderProgramName), + configured(copyFrom.configured), useWorldSpace(copyFrom.useWorldSpace) + { + } + + SparkEmitter::SparkEmitter(const std::vector& positions, float rate) : emissionPoints(positions), emissionRate(rate), isActive(true), drawDataDirty(true), maxParticles(positions.size() * 100), diff --git a/src/SparkEmitter.h b/src/SparkEmitter.h index 3eed0aa..812b828 100644 --- a/src/SparkEmitter.h +++ b/src/SparkEmitter.h @@ -41,7 +41,7 @@ namespace ZL { float biasX; // Ranges (used when config supplies intervals) - struct FloatRange { float min; float max; }; + struct FloatRange { float min=0; float max=0; }; FloatRange speedRange; // XY speed FloatRange zSpeedRange; // Z speed FloatRange scaleRange; @@ -55,6 +55,7 @@ namespace ZL { public: SparkEmitter(); + SparkEmitter(const SparkEmitter& copyFrom); SparkEmitter(const std::vector& positions, float rate = 100.0f); SparkEmitter(const std::vector& positions, std::shared_ptr tex, diff --git a/src/network/ClientState.h b/src/network/ClientState.h index 2624679..5bbd8b3 100644 --- a/src/network/ClientState.h +++ b/src/network/ClientState.h @@ -27,6 +27,14 @@ constexpr long long SERVER_DELAY = 0; //ms constexpr long long CLIENT_DELAY = 500; //ms constexpr long long CUTOFF_TIME = 5000; //ms +constexpr float PROJECTILE_VELOCITY = 600.f; +constexpr float PROJECTILE_LIFE = 15000.f; //ms + +const float projectileHitRadius = 1.5f * 5; +const float boxCollisionRadius = 2.0f * 5; +const float shipCollisionRadius = 15.0f * 5; +const float npcCollisionRadius = 5.0f * 5; + uint32_t fnv1a_hash(const std::string& data); struct ClientState { diff --git a/src/network/LocalClient.cpp b/src/network/LocalClient.cpp index 0321249..1d43a78 100644 --- a/src/network/LocalClient.cpp +++ b/src/network/LocalClient.cpp @@ -21,8 +21,8 @@ namespace ZL { std::random_device rd; std::mt19937 gen(rd()); - const float MIN_COORD = -100.0f; - const float MAX_COORD = 100.0f; + const float MIN_COORD = -1000.0f; + const float MAX_COORD = 1000.0f; const float MIN_DISTANCE = 3.0f; const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE; const int MAX_ATTEMPTS = 1000; @@ -68,7 +68,7 @@ namespace ZL { Eigen::Vector3f LocalClient::generateRandomPosition() { std::random_device rd; std::mt19937 gen(rd()); - std::uniform_real_distribution<> distrib(-500.0, 500.0); + std::uniform_real_distribution<> distrib(-5000.0, 5000.0); return Eigen::Vector3f( (float)distrib(gen), @@ -238,11 +238,6 @@ namespace ZL { auto now_ms = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); - const float projectileHitRadius = 1.5f; - const float boxCollisionRadius = 2.0f; - const float shipCollisionRadius = 15.0f; - const float npcCollisionRadius = 5.0f; - std::vector> boxProjectileCollisions; for (size_t bi = 0; bi < serverBoxes.size(); ++bi) { From 5fcb1d1234a1a0a3f862baec68ff65244d432e60 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sun, 1 Mar 2026 22:35:17 +0300 Subject: [PATCH 20/20] server changes --- server/server.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/server/server.cpp b/server/server.cpp index b981e90..e7fa469 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -53,7 +53,7 @@ struct Projectile { uint64_t spawnMs = 0; Eigen::Vector3f pos; Eigen::Vector3f vel; - float lifeMs = 5000.0f; + float lifeMs = PROJECTILE_LIFE; }; struct BoxDestroyedInfo { @@ -564,11 +564,12 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) { } } + + // --- Tick: box-projectile collisions --- { std::lock_guard bm(g_boxes_mutex); - const float projectileHitRadius = 5.0f; - const float boxCollisionRadius = 2.0f; + std::vector> boxProjectileCollisions; @@ -614,9 +615,6 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) { std::lock_guard bm(g_boxes_mutex); std::lock_guard lm(g_sessions_mutex); - const float shipCollisionRadius = 15.0f; - const float boxCollisionRadius = 2.0f; - for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) { if (g_serverBoxes[bi].destroyed) continue; @@ -705,8 +703,8 @@ std::vector generateServerBoxes(int count) { std::random_device rd; std::mt19937 gen(rd()); - const float MIN_COORD = -100.0f; - const float MAX_COORD = 100.0f; + const float MIN_COORD = -1000.0f; + const float MAX_COORD = 1000.0f; const float MIN_DISTANCE = 3.0f; const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE; const int MAX_ATTEMPTS = 1000;