From 528c94e921a8e16270e7298b5b10c8a4e3d8d8c9 Mon Sep 17 00:00:00 2001 From: vottozi Date: Thu, 19 Feb 2026 01:13:08 +0600 Subject: [PATCH] Add enemy target brackets + off-screen arrows --- src/Game.cpp | 288 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/Game.h | 11 ++ 2 files changed, 296 insertions(+), 3 deletions(-) diff --git a/src/Game.cpp b/src/Game.cpp index 32b95d7..e1e2a8c 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -174,6 +174,25 @@ namespace ZL return true; } + bool Game::projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW) const + { + float aspect = static_cast(Environment::width) / static_cast(Environment::height); + Eigen::Matrix4f V = makeViewMatrix_FromYourCamera(); + Eigen::Matrix4f P = makePerspective(1.0f / 1.5f, aspect, Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + + Eigen::Vector4f w(world.x(), world.y(), world.z(), 1.0f); + Eigen::Vector4f clip = P * V * w; + + clipW = clip.w(); + if (std::abs(clipW) < 1e-6f) return false; + + Eigen::Vector3f ndc = clip.head<3>() / clipW; + ndcX = ndc.x(); + ndcY = ndc.y(); + ndcZ = ndc.z(); + return true; + } + void Game::drawBoxesLabels() { if (!textRenderer) return; @@ -205,11 +224,9 @@ namespace ZL float uiX = sx; float uiY = sy; // если окажется вверх ногами — замени на (Environment::height - sy) - // Можно делать масштаб по дальности: чем дальше — тем меньше. - // depth в NDC: ближе к -1 (near) и к 1 (far). Стабильнее считать по расстоянию: float dist = (Environment::shipState.position - boxWorld).norm(); float scaleRaw = 120.0f / (dist + 1.0f); - float scale = std::round(scaleRaw * 10.0f) / 10.0f; + float scale = std::round(scaleRaw * 10.0f) / 10.0f; // округление до 0.1 scale = std::clamp(scale, 0.6f, 1.2f); textRenderer->drawText(boxLabels[i], uiX, uiY, scale, /*centered*/true); @@ -738,6 +755,7 @@ namespace ZL drawBoxesLabels(); drawUI(); + drawTargetHud(); CheckGlError(); } @@ -877,6 +895,270 @@ namespace ZL #endif } + int Game::pickTargetId() const + { + int bestId = -1; + constexpr float INF_F = 1e30f; + float bestDistSq = INF_F; + + for (auto const& [id, st] : remotePlayerStates) { + if (deadRemotePlayers.count(id)) continue; + + float d2 = (Environment::shipState.position - st.position).squaredNorm(); + if (d2 < bestDistSq) { + bestDistSq = d2; + bestId = id; + } + } + return bestId; + } + + static VertexDataStruct MakeColoredRect2D(float cx, float cy, float hw, float hh, float z, + const Eigen::Vector4f& rgba) + { + VertexDataStruct v; + // 2 triangles + Vector3f p1{ cx - hw, cy - hh, z }; + Vector3f p2{ cx - hw, cy + hh, z }; + Vector3f p3{ cx + hw, cy + hh, z }; + Vector3f p4{ cx + hw, cy - hh, z }; + + v.PositionData = { p1, p2, p3, p3, p4, p1 }; + + // defaultColor shader likely uses vColor (vec3), но нам нужен alpha. + // У тебя в Renderer есть RenderUniform4fv, но шейдер может брать vColor. + // Поэтому: сделаем ColorData vec3, а alpha дадим через uniform uColor, если есть. + // Если в defaultColor нет uniform uColor — тогда alpha будет 1.0. + // Для совместимости: кладём RGB, alpha будем задавать uniform'ом отдельно. + Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() }; + v.ColorData = { rgb, rgb, rgb, rgb, rgb, rgb }; + return v; + } + + void Game::drawTargetHud() + { + if (!textRenderer) return; + + // 1) выбираем цель + int targetIdNow = pickTargetId(); + if (targetIdNow < 0) { + trackedTargetId = -1; + targetAcquireAnim = 0.f; + targetWasVisible = false; + return; + } + + // если цель сменилась — сброс анимации “схлопывания” + if (trackedTargetId != targetIdNow) { + trackedTargetId = targetIdNow; + targetAcquireAnim = 0.0f; + targetWasVisible = false; + } + + const ClientState& st = remotePlayerStates.at(trackedTargetId); + Vector3f shipWorld = st.position; + + // 2) проекция + float ndcX, ndcY, ndcZ, clipW; + if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return; + + // behind camera? + bool behind = (clipW <= 0.0f); + + // on-screen check (NDC) + bool onScreen = (!behind && + ndcX >= -1.0f && ndcX <= 1.0f && + ndcY >= -1.0f && ndcY <= 1.0f); + + // 3) расстояние + float dist = (Environment::shipState.position - shipWorld).norm(); + + // time for arrow bob + float t = static_cast(SDL_GetTicks64()) * 0.001f; + + // 4) Настройки стиля (как X3) + Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный + 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; + + // анимация “снаружи внутрь” + // targetAcquireAnim растёт к 1, быстро (похоже на захват) + float dt = 1.0f / 60.0f; // у тебя нет dt в draw, берём константу, выглядит норм + targetAcquireAnim = min(1.0f, targetAcquireAnim + dt * 6.5f); + + // базовый размер рамки в зависимости от дистанции (как у лейблов) + float size = 220.0f / (dist * 0.01f + 1.0f); // подстройка + size = std::clamp(size, 35.0f, 120.0f); // min/max + + // “схлопывание”: сначала больше, потом ближе к кораблю + // expand 1.6 -> 1.0 + float expand = 1.6f - 0.6f * targetAcquireAnim; + + float half = size * expand; + float cornerLen = max(10.0f, half * 0.35f); + + // точки углов + float left = sx - half; + float right = sx + half; + float bottom = sy - half; + float top = sy + half; + + // рисуем 8 тонких прямоугольников (2 на угол) + auto drawBar = [&](float cx, float cy, float w, float h) + { + VertexDataStruct v = MakeColoredRect2D(cx, cy, w * 0.5f, h * 0.5f, z, enemyColor); + hudTempMesh.AssignFrom(v); + renderer.DrawVertexRenderStruct(hudTempMesh); + }; + + // включаем 2D режим + 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(); + + // верх-лево: горизонт + вертикаль + drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness); + drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen); + + // верх-право + drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness); + drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen); + + // низ-лево + drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness); + drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen); + + // низ-право + drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness); + drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen); + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); + + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + + targetWasVisible = true; + return; + } + + // 6) Если цель offscreen: рисуем стрелку на краю + // dir: куда “смотреть” в NDC + float dirX = ndcX; + float dirY = ndcY; + + // если позади камеры — разворачиваем направление + if (behind) { + dirX = -dirX; + dirY = -dirY; + } + + float len = std::sqrt(dirX * dirX + dirY * dirY); + if (len < 1e-5f) return; + dirX /= len; + dirY /= len; + + // пересечение луча с прямоугольником [-1..1] с отступом + float marginNdc = 0.08f; + float maxX = 1.0f - marginNdc; + float maxY = 1.0f - marginNdc; + + float tx = (std::abs(dirX) < 1e-6f) ? 1e9f : (maxX / std::abs(dirX)); + float ty = (std::abs(dirY) < 1e-6f) ? 1e9f : (maxY / std::abs(dirY)); + float k = min(tx, ty); + + 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 bob = std::sin(t * 6.0f) * 6.0f; + edgeX += dirX * bob; + edgeY += dirY * bob; + + // стрелка как треугольник + маленький “хвост” + float arrowLen = 26.0f; + float arrowWid = 14.0f; + + // перпендикуляр + float px = -dirY; + float py = dirX; + + Vector3f tip{ edgeX + dirX * arrowLen, edgeY + dirY * arrowLen, z }; + Vector3f left{ edgeX + px * (arrowWid * 0.5f), edgeY + py * (arrowWid * 0.5f), z }; + Vector3f right{ edgeX - px * (arrowWid * 0.5f), edgeY - py * (arrowWid * 0.5f), z }; + + auto drawTri = [&](const Vector3f& a, const Vector3f& b, const Vector3f& c) + { + VertexDataStruct v; + v.PositionData = { a, b, c }; + Vector3f rgb{ enemyColor.x(), enemyColor.y(), enemyColor.z() }; + v.ColorData = { rgb, rgb, rgb }; + hudTempMesh.AssignFrom(v); + renderer.DrawVertexRenderStruct(hudTempMesh); + }; + + auto drawBar = [&](float cx, float cy, float w, float h) + { + VertexDataStruct v = MakeColoredRect2D(cx, cy, w * 0.5f, h * 0.5f, z, enemyColor); + hudTempMesh.AssignFrom(v); + renderer.DrawVertexRenderStruct(hudTempMesh); + }; + + 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(); + + // треугольник-стрелка + drawTri(tip, left, right); + + // “хвост” (короткая черта) + float tailLen = 14.0f; + float tailX = edgeX - dirX * 6.0f; + float tailY = edgeY - dirY * 6.0f; + // хвост рисуем как тонкий прямоугольник, ориентированный примерно по направлению: + // (упрощение: горизонт/вертикаль не поворачиваем, но выглядит ок. Хочешь — сделаем поворот матрицей) + drawBar(tailX, tailY, max(thickness, tailLen), thickness); + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); + + // дистанция рядом со стрелкой + // (у тебя ещё будет “статично под прицелом” — это просто другой TextView / drawText) + { + std::string d = std::to_string((int)dist) + "m"; + float tx = edgeX + px * 18.0f; + float ty = edgeY + py * 18.0f; + textRenderer->drawText(d, tx, ty, 0.6f, true, { 1.f, 0.f, 0.f, 1.f }); + } + + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + + targetWasVisible = false; + } + void Game::processTickCount() { if (lastTickCount == 0) { diff --git a/src/Game.h b/src/Game.h index 02711f6..34e1972 100644 --- a/src/Game.h +++ b/src/Game.h @@ -133,7 +133,18 @@ namespace ZL { std::unordered_set deadRemotePlayers; + // --- Target HUD (brackets + offscreen arrow) --- + int trackedTargetId = -1; + bool targetWasVisible = false; + float targetAcquireAnim = 0.0f; // 0..1 схлопывание (0 = далеко, 1 = на месте) + // временный меш для HUD (будем перезаливать VBO маленькими порциями) + VertexRenderStruct hudTempMesh; + + // helpers + bool projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW) const; + void drawTargetHud(); // рисует рамку или стрелку + int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок) };