Add enemy target brackets + off-screen arrows
This commit is contained in:
parent
b56fafa0e0
commit
528c94e921
288
src/Game.cpp
288
src/Game.cpp
@ -174,6 +174,25 @@ namespace ZL
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Game::projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW) const
|
||||||
|
{
|
||||||
|
float aspect = static_cast<float>(Environment::width) / static_cast<float>(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()
|
void Game::drawBoxesLabels()
|
||||||
{
|
{
|
||||||
if (!textRenderer) return;
|
if (!textRenderer) return;
|
||||||
@ -205,11 +224,9 @@ namespace ZL
|
|||||||
float uiX = sx;
|
float uiX = sx;
|
||||||
float uiY = sy; // если окажется вверх ногами — замени на (Environment::height - sy)
|
float uiY = sy; // если окажется вверх ногами — замени на (Environment::height - sy)
|
||||||
|
|
||||||
// Можно делать масштаб по дальности: чем дальше — тем меньше.
|
|
||||||
// depth в NDC: ближе к -1 (near) и к 1 (far). Стабильнее считать по расстоянию:
|
|
||||||
float dist = (Environment::shipState.position - boxWorld).norm();
|
float dist = (Environment::shipState.position - boxWorld).norm();
|
||||||
float scaleRaw = 120.0f / (dist + 1.0f);
|
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);
|
scale = std::clamp(scale, 0.6f, 1.2f);
|
||||||
|
|
||||||
textRenderer->drawText(boxLabels[i], uiX, uiY, scale, /*centered*/true);
|
textRenderer->drawText(boxLabels[i], uiX, uiY, scale, /*centered*/true);
|
||||||
@ -738,6 +755,7 @@ namespace ZL
|
|||||||
drawBoxesLabels();
|
drawBoxesLabels();
|
||||||
|
|
||||||
drawUI();
|
drawUI();
|
||||||
|
drawTargetHud();
|
||||||
CheckGlError();
|
CheckGlError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -877,6 +895,270 @@ namespace ZL
|
|||||||
#endif
|
#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<float>(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() {
|
void Game::processTickCount() {
|
||||||
|
|
||||||
if (lastTickCount == 0) {
|
if (lastTickCount == 0) {
|
||||||
|
|||||||
11
src/Game.h
11
src/Game.h
@ -133,7 +133,18 @@ namespace ZL {
|
|||||||
|
|
||||||
std::unordered_set<int> deadRemotePlayers;
|
std::unordered_set<int> 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; // выбирает цель (пока: ближайший живой удаленный игрок)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user