This commit is contained in:
Vladislav Khorev 2026-02-26 21:22:38 +03:00
commit e8f1786a2a
3 changed files with 328 additions and 43 deletions

View File

@ -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 }
]
}

View File

@ -361,6 +361,19 @@ namespace ZL
throw std::runtime_error("Failed to load spark emitter config file!"); 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<ZL::TextRenderer>(); textRenderer = std::make_unique<ZL::TextRenderer>();
if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) { if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) {
std::cerr << "Failed to init TextRenderer\n"; std::cerr << "Failed to init TextRenderer\n";
@ -597,6 +610,7 @@ namespace ZL
drawBoxesLabels(); drawBoxesLabels();
drawShip(); drawShip();
drawCrosshair();
drawTargetHud(); drawTargetHud();
CheckGlError(); CheckGlError();
} }
@ -726,6 +740,230 @@ namespace ZL
glEnable(GL_DEPTH_TEST); glEnable(GL_DEPTH_TEST);
} }
// хелпер прицела: добавляет повернутую 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<bool>();
if (j.contains("referenceResolution") && j["referenceResolution"].is_array() && j["referenceResolution"].size() == 2) {
crosshairCfg.refW = j["referenceResolution"][0].get<int>();
crosshairCfg.refH = j["referenceResolution"][1].get<int>();
}
if (j.contains("scale")) crosshairCfg.scaleMul = j["scale"].get<float>();
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<float>(),
j["color"][1].get<float>(),
j["color"][2].get<float>()
);
}
if (j.contains("cl_crosshairalpha")) crosshairCfg.alpha = j["cl_crosshairalpha"].get<float>();
if (j.contains("cl_crosshairthickness")) crosshairCfg.thicknessPx = j["cl_crosshairthickness"].get<float>();
if (j.contains("centerGapPx")) crosshairCfg.gapPx = j["centerGapPx"].get<float>();
if (j.contains("top") && j["top"].is_object()) {
auto t = j["top"];
if (t.contains("lengthPx")) crosshairCfg.topLenPx = t["lengthPx"].get<float>();
if (t.contains("angleDeg")) crosshairCfg.topAngleDeg = t["angleDeg"].get<float>();
}
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 Space::pickTargetId() const
{ {
int bestId = -1; int bestId = -1;
@ -896,41 +1134,38 @@ namespace ZL
Vector3f shooterVel = ForwardFromRotation(Environment::shipState.rotation) * Environment::shipState.velocity; Vector3f shooterVel = ForwardFromRotation(Environment::shipState.rotation) * Environment::shipState.velocity;
Vector3f targetVel = ForwardFromRotation(st.rotation) * st.velocity; Vector3f targetVel = ForwardFromRotation(st.rotation) * st.velocity;
// условие "если враг не движется — круг не рисуем"
const float minTargetSpeed = 0.5f; // подобрать (в твоих единицах) const float minTargetSpeed = 0.5f; // подобрать (в твоих единицах)
bool targetMoving = (targetVel.norm() > minTargetSpeed); bool targetMoving = (targetVel.norm() > minTargetSpeed);
// альфа круга
float leadAlpha = targetMoving ? 1.0f : 0.5f;
Vector3f leadWorld = shipWorld; Vector3f leadWorld = shipWorld;
bool haveLead = false; bool haveLead = false;
//if (targetMoving) { // чтобы круг не улетал далеко: максимум 4 секунды (подстроить под игру)
// float tLead = 0.0f; float distToTarget = (Environment::shipState.position - shipWorld).norm();
// if (SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead)) { float maxLeadTime = std::clamp((distToTarget / projectileSpeed) * 1.2f, 0.05f, 4.0f);
// // ограничим случаи, чтобы круг не улетал далеко
// if (tLead > 0.0f && tLead < 8.0f) {
// // подобрать максимум (сек)
// leadWorld = shipWorld + targetVel * tLead;
// haveLead = true;
// }
// }
//}
if (targetMoving) { if (!targetMoving) {
// Цель стоит: рисуем lead прямо на ней, но полупрозрачный
leadWorld = shipWorld;
haveLead = true;
}
else {
float tLead = 0.0f; float tLead = 0.0f;
float distToTarget = (Environment::shipState.position - shipWorld).norm();
const float leadMaxDist = 2500.0f; // максимум // 1) Пытаемся “правильное” решение перехвата
float allowedDist = min(distToTarget, leadMaxDist); bool ok = SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead);
// + небольшой запас 1020% чтобы не моргало на границе // 2) Если решения нет / оно плохое — fallback (чтобы круг не пропадал при пролёте "вбок")
const float maxLeadTime = (allowedDist / projectileSpeed) * 1.2f; // Это ключевое изменение: lead всегда будет.
if (!ok || !(tLead > 0.0f) || tLead > maxLeadTime) {
if (SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead)) { tLead = std::clamp(distToTarget / projectileSpeed, 0.05f, maxLeadTime);
if (tLead > 0.0f && tLead < maxLeadTime) {
leadWorld = shipWorld + targetVel * tLead;
haveLead = true;
}
} }
leadWorld = shipWorld + targetVel * tLead;
haveLead = true;
} }
// 2) проекция // 2) проекция
@ -1006,7 +1241,10 @@ namespace ZL
renderer.EnableVertexAttribArray("vPosition"); renderer.EnableVertexAttribArray("vPosition");
// рисуем кружок упреждения (только если есть решение) Eigen::Vector4f hudColor = enemyColor;
renderer.RenderUniform4fv("uColor", hudColor.data());
if (haveLead) { if (haveLead) {
float leadNdcX, leadNdcY, leadNdcZ, leadClipW; float leadNdcX, leadNdcY, leadNdcZ, leadClipW;
if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) { if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) {
@ -1021,28 +1259,30 @@ namespace ZL
float thicknessPx = 2.5f; float thicknessPx = 2.5f;
float innerR = max(1.0f, r - thicknessPx); float innerR = max(1.0f, r - thicknessPx);
float outerR = r + thicknessPx; float outerR = r + thicknessPx;
Eigen::Vector4f leadColor = enemyColor;
leadColor.w() = leadAlpha;
renderer.RenderUniform4fv("uColor", leadColor.data());
VertexDataStruct ring = MakeRing2D(lx, ly, innerR, outerR, 0.0f, 32, enemyColor); VertexDataStruct ring = MakeRing2D(lx, ly, innerR, outerR, 0.0f, 32, enemyColor);
hudTempMesh.AssignFrom(ring); hudTempMesh.AssignFrom(ring);
renderer.DrawVertexRenderStruct(hudTempMesh); renderer.DrawVertexRenderStruct(hudTempMesh);
renderer.RenderUniform4fv("uColor", hudColor.data());
} }
} }
} }
renderer.EnableVertexAttribArray("vPosition");
// верх-лево: горизонт + вертикаль
drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness); drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness);
drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen); drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen);
// верх-право
drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness); drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness);
drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen); drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen);
// низ-лево
drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness); drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness);
drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen); drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen);
// низ-право
drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness); drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness);
drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen); drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen);
@ -1060,12 +1300,9 @@ namespace ZL
return; return;
} }
// 6) Если цель offscreen: рисуем стрелку на краю
// dir: куда “смотреть” в NDC
float dirX = ndcX; float dirX = ndcX;
float dirY = ndcY; float dirY = ndcY;
// если позади камеры — разворачиваем направление
if (behind) { if (behind) {
dirX = -dirX; dirX = -dirX;
dirY = -dirY; dirY = -dirY;
@ -1076,7 +1313,6 @@ namespace ZL
dirX /= len; dirX /= len;
dirY /= len; dirY /= len;
// пересечение луча с прямоугольником [-1..1] с отступом
float marginNdc = 0.08f; float marginNdc = 0.08f;
float maxX = 1.0f - marginNdc; float maxX = 1.0f - marginNdc;
float maxY = 1.0f - marginNdc; float maxY = 1.0f - marginNdc;
@ -1091,16 +1327,13 @@ namespace ZL
float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::width; float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::width;
float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height; float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height;
// лёгкая анимация “зова”: смещение по направлению
float bob = std::sin(t * 6.0f) * 6.0f; float bob = std::sin(t * 6.0f) * 6.0f;
edgeX += dirX * bob; edgeX += dirX * bob;
edgeY += dirY * bob; edgeY += dirY * bob;
// стрелка как треугольник + маленький “хвост”
float arrowLen = 26.0f; float arrowLen = 26.0f;
float arrowWid = 14.0f; float arrowWid = 14.0f;
// перпендикуляр
float px = -dirY; float px = -dirY;
float py = dirX; float py = dirX;
@ -1139,23 +1372,18 @@ namespace ZL
renderer.PushMatrix(); renderer.PushMatrix();
renderer.LoadIdentity(); renderer.LoadIdentity();
// треугольник-стрелка
drawTri(tip, left, right); drawTri(tip, left, right);
// “хвост” (короткая черта)
float tailLen = 14.0f; float tailLen = 14.0f;
float tailX = edgeX - dirX * 6.0f; float tailX = edgeX - dirX * 6.0f;
float tailY = edgeY - dirY * 6.0f; float tailY = edgeY - dirY * 6.0f;
// хвост рисуем как тонкий прямоугольник, ориентированный примерно по направлению:
// (упрощение: горизонт/вертикаль не поворачиваем, но выглядит ок. Хочешь — сделаем поворот матрицей)
drawBar(tailX, tailY, max(thickness, tailLen), thickness); drawBar(tailX, tailY, max(thickness, tailLen), thickness);
renderer.PopMatrix(); renderer.PopMatrix();
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
// дистанция рядом со стрелкой
// (у тебя ещё будет “статично под прицелом” — это просто другой TextView / drawText)
{ {
std::string d = std::to_string((int)dist) + "m"; std::string d = std::to_string((int)dist) + "m";
float tx = edgeX + px * 18.0f; float tx = edgeX + px * 18.0f;

View File

@ -139,6 +139,42 @@ namespace ZL {
int pickTargetId() const; // ???????? ???? (????: ????????? ????? ????????? ?????) int pickTargetId() const; // ???????? ???? (????: ????????? ????? ????????? ?????)
void clearTextRendererCache(); 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<Arm> 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();
}; };