#include "Space.h" #include "AnimatedModel.h" #include "BoneAnimatedModel.h" #include "planet/PlanetData.h" #include "utils/Utils.h" #include "render/OpenGlExtensions.h" #include #include "render/TextureManager.h" #include "TextModel.h" #include #include #include #include #ifdef __ANDROID__ #include #endif #ifdef NETWORK #ifdef EMSCRIPTEN #include "network/WebSocketClientEmscripten.h" #else #include "network/WebSocketClient.h" #endif #else #include "network/LocalClient.h" #endif namespace ZL { extern const char* CONST_ZIP_FILE; extern float x; Eigen::Quaternionf generateRandomQuaternion(std::mt19937& gen) { std::normal_distribution<> distrib(0.0, 1.0); Eigen::Quaternionf randomQuat = { (float)distrib(gen), (float)distrib(gen), (float)distrib(gen), (float)distrib(gen) }; return randomQuat.normalized(); } std::vector generateRandomBoxCoords(int N) { const float MIN_DISTANCE = 3.0f; const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE; const float MIN_COORD = -100.0f; const float MAX_COORD = 100.0f; const int MAX_ATTEMPTS = 1000; std::vector boxCoordsArr; std::random_device rd; std::mt19937 gen(rd()); std::uniform_real_distribution<> distrib(MIN_COORD, MAX_COORD); int generatedCount = 0; while (generatedCount < N) { bool accepted = false; int attempts = 0; while (!accepted && attempts < MAX_ATTEMPTS) { Vector3f newPos( (float)distrib(gen), (float)distrib(gen), (float)distrib(gen) ); accepted = true; for (const auto& existingBox : boxCoordsArr) { Vector3f diff = newPos - existingBox.pos; float distanceSquared = diff.squaredNorm(); if (distanceSquared < MIN_DISTANCE_SQUARED) { accepted = false; break; } } if (accepted) { Eigen::Quaternionf randomQuat = generateRandomQuaternion(gen); Matrix3f randomMatrix = randomQuat.toRotationMatrix(); boxCoordsArr.emplace_back(BoxCoords{ newPos, randomMatrix }); generatedCount++; } attempts++; } if (!accepted) { break; } } return boxCoordsArr; } static Eigen::Matrix4f makeViewMatrix_FromYourCamera() { Eigen::Matrix4f Tz = Eigen::Matrix4f::Identity(); Tz(2, 3) = -1.0f * ZL::Environment::zoom; Eigen::Matrix4f R = Eigen::Matrix4f::Identity(); R.block<3, 3>(0, 0) = ZL::Environment::inverseShipMatrix; Eigen::Matrix4f Tship = Eigen::Matrix4f::Identity(); Tship(0, 3) = -ZL::Environment::shipState.position.x(); Tship(1, 3) = -ZL::Environment::shipState.position.y(); Tship(2, 3) = -ZL::Environment::shipState.position.z(); return Tz * R * Tship; } static Eigen::Matrix4f makePerspective(float fovyRadians, float aspect, float zNear, float zFar) { // Стандартная перспектива float f = 1.0f / std::tan(fovyRadians * 0.5f); Eigen::Matrix4f P = Eigen::Matrix4f::Zero(); P(0, 0) = f / aspect; P(1, 1) = f; P(2, 2) = (zFar + zNear) / (zNear - zFar); P(2, 3) = (2.0f * zFar * zNear) / (zNear - zFar); P(3, 2) = -1.0f; return P; } bool worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth) { // Матрицы должны совпасть с drawBoxes/drawShip по смыслу 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; if (clip.w() <= 0.0001f) return false; // позади камеры Eigen::Vector3f ndc = clip.head<3>() / clip.w(); // [-1..1] outDepth = ndc.z(); // В пределах экрана? // (можно оставить, можно клампить) float sx = (ndc.x() * 0.5f + 0.5f) * Environment::width; float sy = (ndc.y() * 0.5f + 0.5f) * Environment::height; outX = sx; outY = sy; // Можно отсеять те, что вне: if (sx < -200 || sx > Environment::width + 200) return false; if (sy < -200 || sy > Environment::height + 200) return false; return true; } bool projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW) { 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 Space::drawBoxesLabels() { if (!textRenderer) return; // Текст рисуем как 2D поверх всего 3D, но ДО drawUI или после — как хочешь. // Чтобы подписи были поверх — делай после drawBoxes и до drawUI (как мы и вставили). glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); for (size_t i = 0; i < boxCoordsArr.size(); ++i) { if (i >= boxAlive.size() || !boxAlive[i]) continue; if (i >= boxLabels.size()) continue; // ВАЖНО: твои боксы рисуются с Translate({0,0,45000}) + pos Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.0f, 0.0f, 45000.0f }; // Чуть выше бокса по Y (или по Z — как нравится) Vector3f labelWorld = boxWorld + Vector3f{ 0.0f, 2.2f, 0.0f }; float sx, sy, depth; if (!worldToScreen(labelWorld, sx, sy, depth)) continue; // В твоей UI-системе Y обычно перевёрнут (ты делаешь uiY = height - my). // Наш worldToScreen отдаёт Y в системе "низ=0, верх=height" (NDC->screen). // Чтобы совпало с твоей UI-логикой, перевернём: float uiX = sx; float uiY = sy; // если окажется вверх ногами — замени на (Environment::height - sy) float dist = (Environment::shipState.position - boxWorld).norm(); float scaleRaw = 120.0f / (dist + 1.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); } glDisable(GL_BLEND); glEnable(GL_DEPTH_TEST); } Space::Space(Renderer& iRenderer, TaskManager& iTaskManager, MainThreadHandler& iMainThreadHandler, std::unique_ptr& iNetworkClient, MenuManager& iMenuManager) : renderer(iRenderer), taskManager(iTaskManager), mainThreadHandler(iMainThreadHandler), planetObject(renderer, taskManager, mainThreadHandler), networkClient(iNetworkClient), menuManager(iMenuManager) { projectiles.reserve(maxProjectiles); for (int i = 0; i < maxProjectiles; ++i) { projectiles.emplace_back(std::make_unique()); } } Space::~Space() { } void Space::setup() { menuManager.onRestartPressed = [this]() { this->shipAlive = true; this->gameOver = false; this->showExplosion = false; this->explosionEmitter.setEmissionPoints(std::vector()); Environment::shipState.position = Vector3f{ 0, 0, 45000.f }; Environment::shipState.velocity = 0.0f; Environment::shipState.rotation = Eigen::Matrix3f::Identity(); Environment::inverseShipMatrix = Eigen::Matrix3f::Identity(); Environment::zoom = DEFAULT_ZOOM; Environment::tapDownHold = false; std::cerr << "Game restarted\n"; }; menuManager.onVelocityChanged = [this](float newVelocity) { newShipVelocity = newVelocity; }; menuManager.onFirePressed = [this]() { firePressed = true; }; bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE); bool projCfgLoaded = projectileEmitter.loadFromJsonFile("resources/config/spark_projectile_config.json", renderer, CONST_ZIP_FILE); bool explosionCfgLoaded = explosionEmitter.loadFromJsonFile("resources/config/explosion_config.json", renderer, CONST_ZIP_FILE); explosionEmitter.setEmissionPoints(std::vector()); projectileEmitter.setEmissionPoints(std::vector()); 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) }); cubemap.data = ZL::CreateCubemap(500); cubemap.RefreshVBO(); //Load texture spaceshipTexture = std::make_unique(CreateTextureDataFromPng("resources/MainCharacter_Base_color_sRGB.png", CONST_ZIP_FILE)); spaceshipBase = LoadFromTextFile02("resources/spaceshipnew001.txt", CONST_ZIP_FILE); spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix()); spaceshipBase.Move(Vector3f{ 1.2, 0, -5 }); spaceship.AssignFrom(spaceshipBase); spaceship.RefreshVBO(); //Boxes boxTexture = std::make_unique(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE)); boxBase = LoadFromTextFile02("resources/box/box.txt", CONST_ZIP_FILE); boxCoordsArr = generateRandomBoxCoords(50); boxRenderArr.resize(boxCoordsArr.size()); for (int i = 0; i < boxCoordsArr.size(); i++) { boxRenderArr[i].AssignFrom(boxBase); boxRenderArr[i].RefreshVBO(); } boxAlive.resize(boxCoordsArr.size(), true); ZL::CheckGlError(); boxLabels.clear(); boxLabels.reserve(boxCoordsArr.size()); for (size_t i = 0; i < boxCoordsArr.size(); ++i) { boxLabels.push_back("Box " + std::to_string(i + 1)); } if (!cfgLoaded) { throw std::runtime_error("Failed to load spark emitter config file!"); } textRenderer = std::make_unique(); if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) { std::cerr << "Failed to init TextRenderer\n"; } ZL::CheckGlError(); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); planetObject.init(); } void Space::drawCubemap(float skyPercent) { static const std::string defaultShaderName = "default"; static const std::string envShaderName = "env_sky"; static const std::string vPositionName = "vPosition"; static const std::string vTexCoordName = "vTexCoord"; static const std::string textureUniformName = "Texture"; static const std::string skyPercentUniformName = "skyPercent"; renderer.shaderManager.PushShader(envShaderName); renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1f(skyPercentUniformName, skyPercent); renderer.EnableVertexAttribArray(vPositionName); renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, static_cast(Environment::width) / static_cast(Environment::height), Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); renderer.PushMatrix(); renderer.LoadIdentity(); renderer.RotateMatrix(Environment::inverseShipMatrix); Vector3f worldLightDir = Vector3f(1.0f, -1.0f, -1.0f).normalized(); Matrix3f viewMatrix = Environment::inverseShipMatrix; Vector3f viewLightDir = (viewMatrix * worldLightDir).normalized(); // Передаем вектор НА источник света Vector3f lightToSource = -viewLightDir; renderer.RenderUniform3fv("uLightDirView", lightToSource.data()); // 2. Базовый цвет атмосферы (голубой) Vector3f skyColor = { 0.0f, 0.5f, 1.0f }; renderer.RenderUniform3fv("uSkyColor", skyColor.data()); // 1. Вектор направления от центра планеты к игроку (в мировых координатах) // Предполагаем, что планета в (0,0,0). Если нет, то (shipPosition - planetCenter) Vector3f playerDirWorld = Environment::shipState.position.normalized(); // 2. Направление света в мировом пространстве //Vector3f worldLightDir = Vector3f(1.0f, -1.0f, -1.0f).normalized(); // 3. Считаем глобальную освещенность для игрока (насколько он на свету) // Это одно число для всего кадра float playerLightFactor = playerDirWorld.dot(-worldLightDir); // Ограничиваем и делаем переход мягче playerLightFactor = std::clamp((playerLightFactor + 0.2f) / 1.2f, 0.0f, 1.0f); renderer.RenderUniform1f("uPlayerLightFactor", playerLightFactor); renderer.RenderUniform1f("skyPercent", skyPercent); CheckGlError(); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture->getTexID()); renderer.DrawVertexRenderStruct(cubemap); CheckGlError(); renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.DisableVertexAttribArray(vPositionName); renderer.shaderManager.PopShader(); CheckGlError(); } void Space::drawShip() { static const std::string defaultShaderName = "default"; static const std::string envShaderName = "env"; 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); renderer.EnableVertexAttribArray(vPositionName); renderer.EnableVertexAttribArray(vTexCoordName); renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, static_cast(Environment::width) / static_cast(Environment::height), Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); renderer.PushMatrix(); renderer.LoadIdentity(); renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); renderer.PushMatrix(); renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset if (shipAlive) { glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID()); renderer.DrawVertexRenderStruct(spaceship); } renderer.PopMatrix(); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); for (const auto& p : projectiles) { if (p && p->isActive()) { p->draw(renderer); } } projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); if (shipAlive) { renderer.PushMatrix(); renderer.TranslateMatrix({ 0, 0, 16 }); renderer.TranslateMatrix({ 0, -6.f, 0 }); sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); renderer.PopMatrix(); } if (showExplosion) { explosionEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); } //glBindTexture(GL_TEXTURE_2D, basePlatformTexture->getTexID()); //renderer.DrawVertexRenderStruct(basePlatform); glDisable(GL_BLEND); renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.DisableVertexAttribArray(vPositionName); renderer.DisableVertexAttribArray(vTexCoordName); renderer.shaderManager.PopShader(); CheckGlError(); } void Space::drawBoxes() { static const std::string defaultShaderName = "default"; static const std::string envShaderName = "env"; 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); renderer.EnableVertexAttribArray(vPositionName); renderer.EnableVertexAttribArray(vTexCoordName); renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, static_cast(Environment::width) / static_cast(Environment::height), Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); for (int i = 0; i < boxCoordsArr.size(); i++) { if (!boxAlive[i]) continue; renderer.PushMatrix(); renderer.LoadIdentity(); renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); renderer.RotateMatrix(Environment::inverseShipMatrix); renderer.TranslateMatrix(-Environment::shipState.position); renderer.TranslateMatrix({ 0.f, 0.f, 45000.f }); renderer.TranslateMatrix(boxCoordsArr[i].pos); renderer.RotateMatrix(boxCoordsArr[i].m); glBindTexture(GL_TEXTURE_2D, boxTexture->getTexID()); //glBindTexture(GL_TEXTURE_2D, rockTexture->getTexID()); renderer.DrawVertexRenderStruct(boxRenderArr[i]); renderer.PopMatrix(); } renderer.PopProjectionMatrix(); renderer.DisableVertexAttribArray(vPositionName); renderer.DisableVertexAttribArray(vTexCoordName); renderer.shaderManager.PopShader(); CheckGlError(); } void Space::drawScene() { static const std::string defaultShaderName = "default"; static const std::string envShaderName = "env"; static const std::string vPositionName = "vPosition"; static const std::string vTexCoordName = "vTexCoord"; static const std::string textureUniformName = "Texture"; glClearColor(0.0f, 1.0f, 0.0f, 1.0f); glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); glViewport(0, 0, Environment::width, Environment::height); CheckGlError(); float skyPercent = 0.0; float distance = planetObject.distanceToPlanetSurface(Environment::shipState.position); if (distance > 1500.f) { skyPercent = 0.0f; } else if (distance < 800.f) { skyPercent = 1.0f; } else { skyPercent = (1500.f - distance) / (1500.f - 800.f); } drawCubemap(skyPercent); planetObject.draw(renderer); if (planetObject.distanceToPlanetSurface(Environment::shipState.position) > 100.f) { glClear(GL_DEPTH_BUFFER_BIT); } drawRemoteShips(); drawRemoteShipsLabels(); drawBoxes(); drawBoxesLabels(); drawShip(); drawTargetHud(); CheckGlError(); } 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); renderer.EnableVertexAttribArray(vPositionName); renderer.EnableVertexAttribArray(vTexCoordName); renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, static_cast(Environment::width) / static_cast(Environment::height), Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); // Биндим текстуру корабля один раз для всех удаленных игроков (оптимизация батчинга) glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID()); // Если сервер прислал коробки, применяем их однократно вместо локальной генерации if (!serverBoxesApplied && networkClient) { auto sboxes = networkClient->getServerBoxes(); if (!sboxes.empty()) { boxCoordsArr.clear(); for (auto& b : sboxes) { BoxCoords bc; bc.pos = b.first; bc.m = b.second; boxCoordsArr.push_back(bc); } boxRenderArr.resize(boxCoordsArr.size()); for (int i = 0; i < (int)boxCoordsArr.size(); ++i) { boxRenderArr[i].AssignFrom(boxBase); boxRenderArr[i].RefreshVBO(); } boxAlive.assign(boxCoordsArr.size(), true); serverBoxesApplied = true; } } // Итерируемся по актуальным данным из extrapolateRemotePlayers for (auto const& [id, remotePlayer] : remotePlayerStates) { const ClientState& playerState = remotePlayer; if (deadRemotePlayers.count(id)) continue; renderer.PushMatrix(); renderer.LoadIdentity(); renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset renderer.RotateMatrix(Environment::inverseShipMatrix); renderer.TranslateMatrix(-Environment::shipState.position); Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition; renderer.TranslateMatrix(relativePos); // 3. Поворот врага renderer.RotateMatrix(playerState.rotation); renderer.DrawVertexRenderStruct(spaceship); renderer.PopMatrix(); } renderer.PopProjectionMatrix(); renderer.DisableVertexAttribArray(vPositionName); renderer.DisableVertexAttribArray(vTexCoordName); renderer.shaderManager.PopShader(); CheckGlError(); } void Space::drawRemoteShipsLabels() { if (!textRenderer) return; //#ifdef NETWORK // 2D поверх 3D glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); for (auto const& [id, remotePlayer] : remotePlayerStates) { 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 sx, sy, depth; if (!worldToScreen(labelWorld, sx, sy, depth)) continue; 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"; // 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, uiY, scale, true, { 1.f, 1.f, 1.f, alpha }); } glDisable(GL_BLEND); glEnable(GL_DEPTH_TEST); //#endif } int Space::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 Space::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 Space::processTickCount(int64_t newTickCount, int64_t delta) { auto now_ms = newTickCount; sparkEmitter.update(static_cast(delta)); planetObject.update(static_cast(delta)); if (firePressed) { firePressed = false; if (now_ms - lastProjectileFireTime >= static_cast(projectileCooldownMs)) { lastProjectileFireTime = now_ms; const float projectileSpeed = 250.0f; this->fireProjectiles(); Eigen::Vector3f localForward = { 0, 0, -1 }; Eigen::Vector3f worldForward = (Environment::shipState.rotation * localForward).normalized(); Eigen::Vector3f centerPos = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0, 0.9f, 5.0f }; Eigen::Quaternionf q(Environment::shipState.rotation); float speedToSend = projectileSpeed + Environment::shipState.velocity; int shotCount = 2; std::string fireMsg = "FIRE:" + std::to_string(now_ms) + ":" + std::to_string(centerPos.x()) + ":" + std::to_string(centerPos.y()) + ":" + std::to_string(centerPos.z()) + ":" + std::to_string(q.w()) + ":" + std::to_string(q.x()) + ":" + std::to_string(q.y()) + ":" + std::to_string(q.z()) + ":" + std::to_string(speedToSend) + ":" + std::to_string(shotCount); networkClient->Send(fireMsg); } } //Handle input: if (newShipVelocity != Environment::shipState.selectedVelocity) { Environment::shipState.selectedVelocity = newShipVelocity; std::string msg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent(); networkClient->Send(msg); } float discreteMag; int discreteAngle; if (Environment::tapDownHold) { float diffx = Environment::tapDownCurrentPos(0) - Environment::tapDownStartPos(0); float diffy = Environment::tapDownCurrentPos(1) - Environment::tapDownStartPos(1); float rawMag = sqrtf(diffx * diffx + diffy * diffy); float maxRadius = 200.0f; // Максимальный вынос джойстика if (rawMag > 10.0f) { // Мертвая зона // 1. Дискретизируем отклонение (0.0 - 1.0 с шагом 0.1) float normalizedMag = min(rawMag / maxRadius, 1.0f); discreteMag = std::round(normalizedMag * 10.0f) / 10.0f; // 2. Дискретизируем угол (0-359 градусов) // atan2 возвращает радианы, переводим в градусы float radians = atan2f(diffy, diffx); discreteAngle = static_cast(radians * 180.0f / M_PI); if (discreteAngle < 0) discreteAngle += 360; } else { discreteAngle = -1; discreteMag = 0.0f; } } else { discreteAngle = -1; discreteMag = 0.0f; } if (discreteAngle != Environment::shipState.discreteAngle || discreteMag != Environment::shipState.discreteMag) { Environment::shipState.discreteAngle = discreteAngle; Environment::shipState.discreteMag = discreteMag; std::string msg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent(); networkClient->Send(msg); std::cout << "Sending: " << msg << std::endl; } long long leftoverDelta = delta; while (leftoverDelta > 0) { long long miniDelta = 50; Environment::shipState.simulate_physics(miniDelta); leftoverDelta -= miniDelta; } Environment::inverseShipMatrix = Environment::shipState.rotation.inverse(); static float pingTimer = 0.0f; pingTimer += delta; if (pingTimer >= 1000.0f) { std::string pingMsg = "UPD:" + std::to_string(now_ms) + ":" + Environment::shipState.formPingMessageContent(); networkClient->Send(pingMsg); std::cout << "Sending: " << pingMsg << std::endl; pingTimer = 0.0f; } auto latestRemotePlayers = networkClient->getRemotePlayers(); std::chrono::system_clock::time_point nowRoundedWithDelay{ std::chrono::milliseconds(newTickCount - CLIENT_DELAY) }; for (auto const& [id, remotePlayer] : latestRemotePlayers) { if (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay)) { continue; } ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay); remotePlayerStates[id] = playerState; } for (auto& p : projectiles) { if (p && p->isActive()) { p->update(static_cast(delta), renderer); } } 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); } } if (!projCameraPoints.empty()) { projectileEmitter.setEmissionPoints(projCameraPoints); projectileEmitter.emit(); } else { projectileEmitter.setEmissionPoints(std::vector()); } std::vector shipCameraPoints; for (const auto& lp : shipLocalEmissionPoints) { Vector3f adjusted = lp + Vector3f{ 0.0f, -Environment::zoom * 0.03f, 0.0f }; shipCameraPoints.push_back(adjusted); } if (!shipCameraPoints.empty()) { sparkEmitter.setEmissionPoints(shipCameraPoints); } sparkEmitter.update(static_cast(delta)); projectileEmitter.update(static_cast(delta)); explosionEmitter.update(static_cast(delta)); if (showExplosion) { uint64_t now = SDL_GetTicks64(); if (lastExplosionTime != 0 && now - lastExplosionTime >= explosionDurationMs) { showExplosion = false; explosionEmitter.setEmissionPoints(std::vector()); explosionEmitter.setUseWorldSpace(false); } } if (shipAlive) { float distToSurface = planetObject.distanceToPlanetSurface(Environment::shipState.position); if (distToSurface <= 0.0f) { Vector3f dir = (Environment::shipState.position - PlanetData::PLANET_CENTER_OFFSET).normalized(); Vector3f collisionPoint = PlanetData::PLANET_CENTER_OFFSET + dir * PlanetData::PLANET_RADIUS; Environment::shipState.position = PlanetData::PLANET_CENTER_OFFSET + dir * (PlanetData::PLANET_RADIUS + shipCollisionRadius + 0.1f); shipAlive = false; gameOver = true; Environment::shipState.velocity = 0.0f; showExplosion = true; explosionEmitter.setUseWorldSpace(true); explosionEmitter.setEmissionPoints(std::vector{ collisionPoint }); explosionEmitter.emit(); lastExplosionTime = SDL_GetTicks64(); std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n"; menuManager.showGameOver(); } else { bool stoneCollided = false; int collidedTriIdx = -1; Vector3f collidedStonePos = Vector3f{ 0.0f, 0.0f, 0.0f }; float collidedStoneRadius = 0.0f; for (int triIdx : planetObject.triangleIndicesToDraw) { if (triIdx < 0 || triIdx >= static_cast(planetObject.planetStones.allInstances.size())) continue; if (planetObject.planetStones.statuses.size() <= static_cast(triIdx)) continue; if (planetObject.planetStones.statuses[triIdx] != ChunkStatus::Live) continue; const auto& instances = planetObject.planetStones.allInstances[triIdx]; for (const auto& inst : instances) { Vector3f stoneWorld = inst.position; Vector3f diff = Environment::shipState.position - stoneWorld; float maxScale = (std::max)({ inst.scale(0), inst.scale(1), inst.scale(2) }); float stoneRadius = StoneParams::BASE_SCALE * maxScale * 0.9f; float thresh = shipCollisionRadius + stoneRadius; if (diff.squaredNorm() <= thresh * thresh) { stoneCollided = true; collidedTriIdx = triIdx; collidedStonePos = stoneWorld; collidedStoneRadius = stoneRadius; break; } } if (stoneCollided) break; } if (stoneCollided) { Vector3f away = (Environment::shipState.position - collidedStonePos); if (away.squaredNorm() <= 1e-6f) { away = Vector3f{ 0.0f, 1.0f, 0.0f }; } away.normalize(); Environment::shipState.position = collidedStonePos + away * (collidedStoneRadius + shipCollisionRadius + 0.1f); shipAlive = false; gameOver = true; Environment::shipState.velocity = 0.0f; showExplosion = true; explosionEmitter.setUseWorldSpace(true); explosionEmitter.setEmissionPoints(std::vector{ collidedStonePos }); explosionEmitter.emit(); lastExplosionTime = SDL_GetTicks64(); std::cerr << "GAME OVER: collision with stone on triangle " << collidedTriIdx << std::endl; if (collidedTriIdx >= 0 && collidedTriIdx < static_cast(planetObject.stonesToRender.size())) { planetObject.stonesToRender[collidedTriIdx].data.PositionData.clear(); planetObject.stonesToRender[collidedTriIdx].vao.reset(); planetObject.stonesToRender[collidedTriIdx].positionVBO.reset(); planetObject.stonesToRender[collidedTriIdx].normalVBO.reset(); planetObject.stonesToRender[collidedTriIdx].tangentVBO.reset(); planetObject.stonesToRender[collidedTriIdx].binormalVBO.reset(); planetObject.stonesToRender[collidedTriIdx].colorVBO.reset(); planetObject.stonesToRender[collidedTriIdx].texCoordVBO.reset(); } if (collidedTriIdx >= 0 && collidedTriIdx < static_cast(planetObject.planetStones.statuses.size())) { planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty; } menuManager.showGameOver(); } } } // update velocity text if (shipAlive && !gameOver) { auto velocityTv = menuManager.uiManager.findTextView("velocityText"); if (velocityTv) { std::string velocityStr = "Velocity: " + std::to_string(static_cast(Environment::shipState.velocity)); menuManager.uiManager.setText("velocityText", velocityStr); } } } void Space::fireProjectiles() { std::vector localOffsets = { Vector3f{ -1.5f, 0.9f - 6.f, 5.0f }, Vector3f{ 1.5f, 0.9f - 6.f, 5.0f } }; const float projectileSpeed = 60.0f; const float lifeMs = 5000.0f; const float size = 0.5f; Vector3f localForward = { 0,0,-1 }; Vector3f worldForward = (Environment::shipState.rotation * localForward).normalized(); for (const auto& lo : localOffsets) { Vector3f worldPos = Environment::shipState.position + Environment::shipState.rotation * lo; Vector3f worldVel = worldForward * (projectileSpeed + Environment::shipState.velocity); for (auto& p : projectiles) { if (!p->isActive()) { p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer); break; } } } } void Space::update() { if (networkClient) { auto pending = networkClient->getPendingProjectiles(); if (!pending.empty()) { const float projectileSpeed = 60.0f; const float lifeMs = 5000.0f; const float size = 0.5f; for (const auto& pi : pending) { const std::vector localOffsets = { Vector3f{ -1.5f, 0.9f, 5.0f }, Vector3f{ 1.5f, 0.9f, 5.0f } }; Vector3f localForward = { 0, 0, -1 }; Vector3f worldForward = pi.rotation * localForward; float len = worldForward.norm(); if (len <= 1e-6f) { continue; } worldForward /= len; Vector3f baseVel = worldForward * pi.velocity; for (const auto& off : localOffsets) { Vector3f shotPos = pi.position + (pi.rotation * off); for (auto& p : projectiles) { if (!p->isActive()) { p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer); break; } } } } } // Обработка событий смерти, присланных сервером auto deaths = networkClient->getPendingDeaths(); if (!deaths.empty()) { int localId = networkClient->GetClientId(); std::cout << "Client: Received " << deaths.size() << " death events" << std::endl; for (const auto& d : deaths) { std::cout << "Client: Processing death - target=" << d.targetId << ", killer=" << d.killerId << ", pos=(" << d.position.x() << ", " << d.position.y() << ", " << d.position.z() << ")" << std::endl; showExplosion = true; explosionEmitter.setUseWorldSpace(true); explosionEmitter.setEmissionPoints(std::vector{ d.position }); explosionEmitter.emit(); lastExplosionTime = SDL_GetTicks64(); std::cout << "Client: Explosion emitted at (" << d.position.x() << ", " << d.position.y() << ", " << d.position.z() << ")" << std::endl; if (d.targetId == localId) { std::cout << "Client: Local ship destroyed!" << std::endl; shipAlive = false; gameOver = true; Environment::shipState.velocity = 0.0f; menuManager.showGameOver(); } else { deadRemotePlayers.insert(d.targetId); std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl; } } } auto respawns = networkClient->getPendingRespawns(); if (!respawns.empty()) { for (const auto& respawnId : respawns) { deadRemotePlayers.erase(respawnId); auto it = remotePlayerStates.find(respawnId); if (it != remotePlayerStates.end()) { it->second.position = Vector3f{ 0.f, 0.f, 45000.f }; it->second.velocity = 0.0f; it->second.rotation = Eigen::Matrix3f::Identity(); } std::cout << "Client: Remote player " << respawnId << " respawned, removed from dead list" << std::endl; } } auto boxDestructions = networkClient->getPendingBoxDestructions(); if (!boxDestructions.empty()) { std::cout << "Game: Received " << boxDestructions.size() << " box destruction events" << std::endl; for (const auto& destruction : boxDestructions) { int idx = destruction.boxIndex; if (idx >= 0 && idx < (int)boxCoordsArr.size()) { if (boxAlive[idx]) { boxAlive[idx] = false; boxRenderArr[idx].data.PositionData.clear(); boxRenderArr[idx].vao.reset(); boxRenderArr[idx].positionVBO.reset(); boxRenderArr[idx].texCoordVBO.reset(); showExplosion = true; explosionEmitter.setUseWorldSpace(true); explosionEmitter.setEmissionPoints(std::vector{ destruction.position }); explosionEmitter.emit(); lastExplosionTime = SDL_GetTicks64(); std::cout << "Game: Box " << idx << " destroyed by player " << destruction.destroyedBy << std::endl; } } } } } } void Space::handleDown(int mx, int my) { Environment::tapDownHold = true; Environment::tapDownStartPos(0) = mx; Environment::tapDownStartPos(1) = my; Environment::tapDownCurrentPos(0) = mx; Environment::tapDownCurrentPos(1) = my; } void Space::handleUp(int mx, int my) { Environment::tapDownHold = false; } void Space::handleMotion(int mx, int my) { if (Environment::tapDownHold) { Environment::tapDownCurrentPos(0) = mx; Environment::tapDownCurrentPos(1) = my; } } void Space::clearTextRendererCache() { if (textRenderer) { textRenderer->ClearCache(); } } /* std::string Space::formPingMessageContent() { Eigen::Quaternionf q(Environment::shipMatrix); std::string pingMsg = std::to_string(Environment::shipPosition.x()) + ":" + std::to_string(Environment::shipPosition.y()) + ":" + std::to_string(Environment::shipPosition.z()) + ":" + std::to_string(q.w()) + ":" + std::to_string(q.x()) + ":" + std::to_string(q.y()) + ":" + std::to_string(q.z()) + ":" + std::to_string(Environment::currentAngularVelocity.x()) + ":" + std::to_string(Environment::currentAngularVelocity.y()) + ":" + std::to_string(Environment::currentAngularVelocity.z()) + ":" + std::to_string(Environment::shipVelocity) + ":" + std::to_string(Environment::shipSelectedVelocity) + ":" + std::to_string(Environment::lastSentMagnitude) + ":" // Используем те же static переменные из блока ROT + std::to_string(Environment::lastSentAngle); return pingMsg; }*/ } // namespace ZL