2014 lines
65 KiB
C++
2014 lines
65 KiB
C++
#include "Space.h"
|
||
#include "AnimatedModel.h"
|
||
#include "BoneAnimatedModel.h"
|
||
#include "planet/PlanetData.h"
|
||
#include "utils/Utils.h"
|
||
#include "render/OpenGlExtensions.h"
|
||
#include <iostream>
|
||
#include "render/TextureManager.h"
|
||
#include "TextModel.h"
|
||
#include <random>
|
||
#include <cmath>
|
||
#include <algorithm>
|
||
#include <functional>
|
||
#ifdef __ANDROID__
|
||
#include <android/log.h>
|
||
#endif
|
||
|
||
#ifdef NETWORK
|
||
#ifdef EMSCRIPTEN
|
||
#include "network/WebSocketClientEmscripten.h"
|
||
#else
|
||
#include "network/WebSocketClient.h"
|
||
#endif
|
||
#else
|
||
#include "network/LocalClient.h"
|
||
#endif
|
||
|
||
#include "GameConstants.h"
|
||
|
||
namespace ZL
|
||
{
|
||
|
||
extern const char* CONST_ZIP_FILE;
|
||
|
||
extern float x;
|
||
extern float y;
|
||
extern float z;
|
||
|
||
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<BoxCoords> 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<BoxCoords> 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<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;
|
||
|
||
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::projectionWidth;
|
||
float sy = (ndc.y() * 0.5f + 0.5f) * Environment::projectionHeight;
|
||
|
||
outX = sx;
|
||
outY = sy;
|
||
|
||
// Можно отсеять те, что вне:
|
||
if (sx < -200 || sx > Environment::projectionWidth + 200) return false;
|
||
if (sy < -200 || sy > Environment::projectionHeight + 200) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
bool projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW)
|
||
{
|
||
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 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>& 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<Projectile>());
|
||
}
|
||
}
|
||
|
||
Space::~Space() {
|
||
}
|
||
|
||
void Space::resetPlayerState()
|
||
{
|
||
shipAlive = true;
|
||
gameOver = false;
|
||
showExplosion = false;
|
||
explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
|
||
Environment::shipState.position = Vector3f{ 0, 0, 45000.f };
|
||
Environment::shipState.velocity = 0.0f;
|
||
Environment::shipState.selectedVelocity = 0;
|
||
Environment::shipState.rotation = Eigen::Matrix3f::Identity();
|
||
Environment::inverseShipMatrix = Eigen::Matrix3f::Identity();
|
||
Environment::zoom = DEFAULT_ZOOM;
|
||
Environment::tapDownHold = false;
|
||
playerScore = 0;
|
||
if (menuManager.uiManager.findButton("minusButton"))
|
||
{
|
||
menuManager.uiManager.findButton("minusButton")->state = ButtonState::Disabled;
|
||
}
|
||
|
||
}
|
||
|
||
void Space::setup() {
|
||
|
||
|
||
menuManager.onRestartPressed = [this]() {
|
||
resetPlayerState();
|
||
|
||
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";
|
||
};
|
||
|
||
menuManager.onVelocityChanged = [this](float newVelocity) {
|
||
newShipVelocity = newVelocity;
|
||
if (Environment::shipState.shipType == 0)
|
||
{
|
||
if (newVelocity > 2)
|
||
{
|
||
this->menuManager.uiManager.findButton("shootButton")->state = ButtonState::Disabled;
|
||
this->menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Disabled;
|
||
|
||
}
|
||
else
|
||
{
|
||
this->menuManager.uiManager.findButton("shootButton")->state = ButtonState::Normal;
|
||
this->menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Normal;
|
||
}
|
||
}
|
||
};
|
||
|
||
menuManager.onFirePressed = [this]() {
|
||
firePressed = true;
|
||
};
|
||
|
||
bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE);
|
||
bool cfgLoaded2 = sparkEmitterCargo.loadFromJsonFile("resources/config/spark_config_cargo.json", renderer, CONST_ZIP_FILE);
|
||
sparkEmitter.setIsActive(false);
|
||
sparkEmitterCargo.setIsActive(false);
|
||
|
||
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<Vector3f>());
|
||
//projectileEmitter.setEmissionPoints({ Vector3f{0,0,45000} });
|
||
//projectileEmitter.setUseWorldSpace(true);
|
||
|
||
|
||
cubemapTexture = std::make_shared<Texture>(
|
||
std::array<TextureDataStruct, 6>{
|
||
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)
|
||
});
|
||
|
||
|
||
cubemap.data = ZL::CreateCubemap(500);
|
||
cubemap.RefreshVBO();
|
||
|
||
|
||
|
||
//Load texture
|
||
spaceshipTexture = std::make_unique<Texture>(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 });
|
||
spaceshipBase.Scale(0.4f);
|
||
|
||
spaceship.AssignFrom(spaceshipBase);
|
||
spaceship.RefreshVBO();
|
||
|
||
// Load cargo
|
||
cargoTexture = std::make_shared<Texture>(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{ 0, 0, -5 });
|
||
cargo.AssignFrom(cargoBase);
|
||
cargo.RefreshVBO();
|
||
|
||
//projectileTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/spark2.png", CONST_ZIP_FILE));
|
||
|
||
//Boxes
|
||
boxTexture = std::make_unique<Texture>(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);
|
||
#ifdef NETWORK
|
||
std::fill(boxAlive.begin(), boxAlive.end(), false);
|
||
serverBoxesApplied = false;
|
||
#endif
|
||
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!");
|
||
}
|
||
|
||
crosshairCfgLoaded = loadCrosshairConfig("resources/config/crosshair_config.json");
|
||
std::cout << "[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>();
|
||
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 envSkyShaderName = "env_sky";
|
||
static const std::string skyPercentUniformName = "skyPercent";
|
||
|
||
renderer.shaderManager.PushShader(envSkyShaderName);
|
||
renderer.RenderUniform1i(textureUniformName, 0);
|
||
renderer.RenderUniform1f(skyPercentUniformName, skyPercent);
|
||
|
||
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
|
||
static_cast<float>(Environment::width) / static_cast<float>(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.shaderManager.PopShader();
|
||
CheckGlError();
|
||
}
|
||
|
||
void Space::drawShip()
|
||
{
|
||
renderer.shaderManager.PushShader(defaultShaderName);
|
||
renderer.RenderUniform1i(textureUniformName, 0);
|
||
|
||
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
|
||
static_cast<float>(Environment::width) / static_cast<float>(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) {
|
||
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);
|
||
}
|
||
|
||
|
||
drawShipSparkEmitters();
|
||
}
|
||
|
||
renderer.PopMatrix();
|
||
glEnable(GL_BLEND);
|
||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||
|
||
renderer.shaderManager.PushShader(defaultShaderName);
|
||
renderer.RenderUniform1i(textureUniformName, 0);
|
||
glEnable(GL_BLEND);
|
||
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
|
||
renderer.PushMatrix();
|
||
renderer.LoadIdentity();
|
||
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
|
||
renderer.RotateMatrix(Environment::inverseShipMatrix);
|
||
renderer.TranslateMatrix(-Environment::shipState.position);
|
||
for (const auto& p : projectiles) {
|
||
if (p && p->isActive()) {
|
||
p->projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
|
||
}
|
||
}
|
||
renderer.PopMatrix();
|
||
glDisable(GL_BLEND);
|
||
|
||
renderer.shaderManager.PopShader();
|
||
|
||
if (showExplosion) {
|
||
explosionEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height, false);
|
||
}
|
||
|
||
glDisable(GL_BLEND);
|
||
renderer.PopMatrix();
|
||
renderer.PopProjectionMatrix();
|
||
renderer.shaderManager.PopShader();
|
||
CheckGlError();
|
||
}
|
||
|
||
void Space::drawBoxes()
|
||
{
|
||
renderer.shaderManager.PushShader(defaultShaderName);
|
||
renderer.RenderUniform1i(textureUniformName, 0);
|
||
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
|
||
static_cast<float>(Environment::width) / static_cast<float>(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());
|
||
renderer.DrawVertexRenderStruct(boxRenderArr[i]);
|
||
|
||
renderer.PopMatrix();
|
||
}
|
||
renderer.PopProjectionMatrix();
|
||
renderer.shaderManager.PopShader();
|
||
CheckGlError();
|
||
}
|
||
|
||
void Space::drawScene() {
|
||
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);
|
||
|
||
prepareSparkEmittersForDraw();
|
||
|
||
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();
|
||
|
||
drawCrosshair();
|
||
drawTargetHud();
|
||
CheckGlError();
|
||
}
|
||
|
||
void Space::drawRemoteShips() {
|
||
renderer.shaderManager.PushShader(defaultShaderName);
|
||
renderer.RenderUniform1i(textureUniformName, 0);
|
||
|
||
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
|
||
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||
|
||
if (!serverBoxesApplied && networkClient) {
|
||
auto sboxes = networkClient->getServerBoxes();
|
||
auto destroyedFlags = networkClient->getServerBoxDestroyedFlags();
|
||
if (!sboxes.empty()) {
|
||
boxCoordsArr.clear();
|
||
boxCoordsArr.resize(sboxes.size());
|
||
for (size_t i = 0; i < sboxes.size(); ++i) {
|
||
BoxCoords 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) {
|
||
boxRenderArr[i].AssignFrom(boxBase);
|
||
boxRenderArr[i].RefreshVBO();
|
||
}
|
||
boxAlive.assign(boxCoordsArr.size(), true);
|
||
|
||
size_t n = (std::min)(destroyedFlags.size(), boxAlive.size());
|
||
for (size_t i = 0; i < n; ++i) {
|
||
if (destroyedFlags[i]) boxAlive[i] = false; // destroyed => не рисуем
|
||
}
|
||
serverBoxesApplied = true;
|
||
}
|
||
}
|
||
|
||
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.RotateMatrix(Environment::inverseShipMatrix);
|
||
renderer.TranslateMatrix(-Environment::shipState.position);
|
||
|
||
|
||
Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition;
|
||
renderer.TranslateMatrix(relativePos);
|
||
|
||
renderer.RotateMatrix(playerState.rotation);
|
||
|
||
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();
|
||
}
|
||
|
||
renderer.PopProjectionMatrix();
|
||
renderer.shaderManager.PopShader();
|
||
|
||
CheckGlError();
|
||
}
|
||
|
||
void Space::drawRemoteShipsLabels()
|
||
{
|
||
if (!textRenderer) return;
|
||
|
||
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();
|
||
float dist = sqrt(distSq);
|
||
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;
|
||
|
||
float uiX = sx, uiY = sy;
|
||
float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);
|
||
|
||
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";
|
||
|
||
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);
|
||
}
|
||
|
||
// хелпер прицела: добавляет повернутую 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::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 &&
|
||
std::abs(crosshairLastScaleMul - crosshairCfg.scaleMul) < 1e-6f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
crosshairLastW = Environment::projectionWidth;
|
||
crosshairLastH = Environment::projectionHeight;
|
||
crosshairLastAlpha = crosshairCfg.alpha;
|
||
crosshairLastThickness = crosshairCfg.thicknessPx;
|
||
crosshairLastGap = crosshairCfg.gapPx;
|
||
crosshairLastScaleMul = crosshairCfg.scaleMul;
|
||
|
||
float cx = Environment::projectionWidth * 0.5f;
|
||
float cy = Environment::projectionHeight * 0.5f;
|
||
|
||
// масштаб от reference (стандартно: по высоте)
|
||
float scale = (crosshairCfg.refH > 0) ? (Environment::projectionHeight / (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(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
|
||
renderer.PushMatrix();
|
||
renderer.LoadIdentity();
|
||
|
||
Eigen::Vector4f uColor(crosshairCfg.color.x(), crosshairCfg.color.y(), crosshairCfg.color.z(), crosshairCfg.alpha);
|
||
renderer.RenderUniform4fv("uColor", uColor.data());
|
||
|
||
renderer.DrawVertexRenderStruct(crosshairMesh);
|
||
|
||
renderer.PopMatrix();
|
||
renderer.PopProjectionMatrix();
|
||
renderer.shaderManager.PopShader();
|
||
|
||
glDisable(GL_BLEND);
|
||
glEnable(GL_DEPTH_TEST);
|
||
}
|
||
|
||
int Space::pickTargetId() const
|
||
{
|
||
int bestId = -1;
|
||
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;
|
||
}
|
||
}
|
||
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)
|
||
{
|
||
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'ом отдельно.
|
||
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;
|
||
}
|
||
|
||
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;
|
||
|
||
// Lead Indicator
|
||
// скорость пули (как в fireProjectiles)
|
||
const float projectileSpeed = PROJECTILE_VELOCITY;
|
||
|
||
// позиция вылета
|
||
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;
|
||
|
||
float shooterSpeed = std::abs(Environment::shipState.velocity);
|
||
// В нашей физике линейная скорость корабля всегда направлена по его forward (-Z)
|
||
// Когда игрок наводится на lead indicator, forward (и скорость) становятся сонаправлены с выстрелом
|
||
// поэтому эффективная скорость снаряда в мире ≈ muzzle + shipSpeed.
|
||
const float effectiveProjectileSpeed = projectileSpeed + shooterSpeed;
|
||
Vector3f shooterVel = Vector3f::Zero(); // скорость уже учтена в effectiveProjectileSpeed
|
||
Vector3f targetVel = ForwardFromRotation(st.rotation) * st.velocity;
|
||
|
||
// ВАЖНО: remote state берется на now_ms - CLIENT_DELAY
|
||
// Значит shipWorld - это позиция ~0.5 сек назад.
|
||
// Для корректного lead нужно предсказать положение цели на сейчас
|
||
const float clientDelaySec = (float)CLIENT_DELAY / 1000.0f;
|
||
Vector3f targetPosNow = shipWorld + targetVel * clientDelaySec;
|
||
|
||
const float minTargetSpeed = 0.5f; // подобрать (в твоих единицах)
|
||
bool targetMoving = (targetVel.norm() > minTargetSpeed);
|
||
|
||
// альфа круга
|
||
float leadAlpha = targetMoving ? 1.0f : 0.5f;
|
||
|
||
Vector3f leadWorld = targetPosNow;
|
||
bool haveLead = false;
|
||
|
||
// Дистанцию лучше считать от реальной точки вылета
|
||
float distToTarget = (shooterPos - targetPosNow).norm();
|
||
// Максимальное время перехвата ограничиваем жизнью пули
|
||
const float projectileLifeSec = (float)PROJECTILE_LIFE / 1000.0f;
|
||
float maxLeadTime = std::clamp((distToTarget / effectiveProjectileSpeed) * 1.25f, 0.01f, projectileLifeSec * 0.98f);
|
||
|
||
if (!targetMoving) {
|
||
// Цель стоит: рисуем lead прямо на ней, но полупрозрачный
|
||
leadWorld = targetPosNow;
|
||
haveLead = true;
|
||
}
|
||
else {
|
||
float tLead = 0.0f;
|
||
|
||
// 1) Пытаемся “правильное” решение перехвата
|
||
bool ok = SolveLeadInterceptTime(shooterPos, shooterVel, targetPosNow, targetVel, effectiveProjectileSpeed, tLead);
|
||
|
||
// 2) Если решения нет / оно плохое — fallback (чтобы круг не пропадал при пролёте "вбок")
|
||
// Это ключевое изменение: lead всегда будет.
|
||
if (!ok || !(tLead > 0.0f) || tLead > maxLeadTime) {
|
||
tLead = std::clamp(distToTarget / effectiveProjectileSpeed, 0.05f, maxLeadTime);
|
||
}
|
||
|
||
leadWorld = targetPosNow + targetVel * tLead;
|
||
haveLead = true;
|
||
}
|
||
|
||
// Проекция цели (для рамок/стрелки)
|
||
float ndcX, ndcY, ndcZ, clipW;
|
||
if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return;
|
||
|
||
bool behind = (clipW <= 0.0f);
|
||
|
||
bool onScreen = (!behind &&
|
||
ndcX >= -1.0f && ndcX <= 1.0f &&
|
||
ndcY >= -1.0f && ndcY <= 1.0f);
|
||
|
||
float dist = (Environment::shipState.position - shipWorld).norm();
|
||
float t = static_cast<float>(SDL_GetTicks64()) * 0.001f;
|
||
|
||
// Проекция Lead
|
||
float leadNdcX = 0.f, leadNdcY = 0.f, leadNdcZ = 0.f, leadClipW = 0.f;
|
||
bool leadOnScreen = false;
|
||
|
||
if (haveLead) {
|
||
if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) {
|
||
leadOnScreen =
|
||
(leadNdcX >= -1.0f && leadNdcX <= 1.0f &&
|
||
leadNdcY >= -1.0f && leadNdcY <= 1.0f);
|
||
}
|
||
}
|
||
|
||
// Настройки HUD стилизация
|
||
Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный
|
||
float thickness = 2.0f; // толщина линий (px)
|
||
float z = 0.0f; // 2D слой
|
||
|
||
auto drawLeadRing2D = [&](float lx, float ly)
|
||
{
|
||
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;
|
||
|
||
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);
|
||
|
||
// вернуть цвет HUD обратно
|
||
renderer.RenderUniform4fv("uColor", enemyColor.data());
|
||
};
|
||
|
||
|
||
// Цель в кадре: рамки
|
||
if (onScreen)
|
||
{
|
||
// перевод NDC -> экран (в пикселях)
|
||
float sx = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
|
||
float sy = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;
|
||
|
||
// анимация “снаружи внутрь”
|
||
// 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);
|
||
glClear(GL_DEPTH_BUFFER_BIT);
|
||
|
||
renderer.shaderManager.PushShader("defaultColor");
|
||
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
|
||
renderer.PushMatrix();
|
||
renderer.LoadIdentity();
|
||
|
||
renderer.RenderUniform4fv("uColor", enemyColor.data());
|
||
|
||
// рамки
|
||
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);
|
||
|
||
// LEAD — независимо от рамок: если его точка на экране, рисуем
|
||
if (haveLead && leadOnScreen) {
|
||
float lx = (leadNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
|
||
float ly = (leadNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
|
||
drawLeadRing2D(lx, ly);
|
||
}
|
||
|
||
renderer.PopMatrix();
|
||
renderer.PopProjectionMatrix();
|
||
renderer.shaderManager.PopShader();
|
||
|
||
glDisable(GL_BLEND);
|
||
glEnable(GL_DEPTH_TEST);
|
||
|
||
targetWasVisible = true;
|
||
return;
|
||
}
|
||
|
||
|
||
// Цель вне экрана: стрелка
|
||
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;
|
||
|
||
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::projectionWidth;
|
||
float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
|
||
|
||
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 };
|
||
// 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);
|
||
};
|
||
|
||
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(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
|
||
renderer.PushMatrix();
|
||
renderer.LoadIdentity();
|
||
|
||
renderer.RenderUniform4fv("uColor", enemyColor.data());
|
||
|
||
// стрелка
|
||
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);
|
||
|
||
// LEAD — рисуем даже когда цель вне экрана (если lead точка на экране)
|
||
if (haveLead && leadOnScreen) {
|
||
float lx = (leadNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
|
||
float ly = (leadNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
|
||
drawLeadRing2D(lx, ly);
|
||
}
|
||
|
||
renderer.PopMatrix();
|
||
renderer.PopProjectionMatrix();
|
||
renderer.shaderManager.PopShader();
|
||
|
||
// дистанция около стрелки
|
||
{
|
||
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::updateSparkEmitters(float deltaMs)
|
||
{
|
||
// Local ship
|
||
SparkEmitter* sparkEmitterPtr;
|
||
if (Environment::shipState.shipType == 1) {
|
||
sparkEmitterPtr = &sparkEmitterCargo;
|
||
static std::vector<Vector3f> emissionPoints = { Vector3f(0, 0, 0), Vector3f(0, 0, 0) };
|
||
emissionPoints[0] = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0, 2.8, -6.5 + 16.0 };
|
||
emissionPoints[1] = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0, 1.5, -6.5 + 16.0 };
|
||
sparkEmitterPtr->setEmissionPoints(emissionPoints);
|
||
}
|
||
else {
|
||
sparkEmitterPtr = &sparkEmitter;
|
||
static std::vector<Vector3f> emissionPoints = { Vector3f(0, 0, 0), Vector3f(0, 0, 0) };
|
||
emissionPoints[0] = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ -0.9, 1.4 - 1.0, -8.5 + 16.0 };
|
||
emissionPoints[1] = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.9, 1.4 - 1.0, -8.5 + 16.0 };
|
||
sparkEmitterPtr->setEmissionPoints(emissionPoints);
|
||
}
|
||
sparkEmitterPtr->setIsActive(Environment::shipState.velocity > 0.1f);
|
||
sparkEmitterPtr->update(deltaMs);
|
||
|
||
// Remote ships
|
||
for (auto const& [id, playerState] : remotePlayerStates) {
|
||
if (deadRemotePlayers.count(id)) continue;
|
||
if (!remoteShipSparkEmitters.count(id)) {
|
||
remoteShipSparkEmitters.emplace(id, playerState.shipType == 1 ? sparkEmitterCargo : sparkEmitter);
|
||
}
|
||
auto& remEmitter = remoteShipSparkEmitters.at(id);
|
||
std::vector<Vector3f> remEmitPts(2);
|
||
if (playerState.shipType == 1) {
|
||
remEmitPts[0] = playerState.position + playerState.rotation * Vector3f{ 0.0f, -0.4f+2.8f, 8.4f };
|
||
remEmitPts[1] = playerState.position + playerState.rotation * Vector3f{ 0.0f, -0.4f+1.5f, 8.4f };
|
||
} else {
|
||
remEmitPts[0] = playerState.position + playerState.rotation * Vector3f{ -0.9f, -0.2,5.6 };
|
||
remEmitPts[1] = playerState.position + playerState.rotation * Vector3f{ 0.9f,-0.2,5.6 };
|
||
}
|
||
remEmitter.setEmissionPoints(remEmitPts);
|
||
remEmitter.setIsActive(playerState.velocity > 0.1f);
|
||
remEmitter.update(deltaMs);
|
||
}
|
||
}
|
||
|
||
void Space::prepareSparkEmittersForDraw()
|
||
{
|
||
sparkEmitter.prepareForDraw(true);
|
||
sparkEmitterCargo.prepareForDraw(true);
|
||
for (auto& [id, emitter] : remoteShipSparkEmitters) {
|
||
if (!deadRemotePlayers.count(id)) emitter.prepareForDraw(true);
|
||
}
|
||
explosionEmitter.prepareForDraw(false);
|
||
for (const auto& p : projectiles) {
|
||
if (p && p->isActive()) {
|
||
p->projectileEmitter.prepareForDraw(true);
|
||
}
|
||
}
|
||
}
|
||
|
||
void Space::drawShipSparkEmitters()
|
||
{
|
||
renderer.PushMatrix();
|
||
renderer.RotateMatrix(Environment::inverseShipMatrix);
|
||
renderer.TranslateMatrix(-Environment::shipState.position);
|
||
if (Environment::shipState.shipType == 1) {
|
||
sparkEmitterCargo.draw(renderer, Environment::zoom, Environment::width, Environment::height);
|
||
} else {
|
||
sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
|
||
}
|
||
|
||
for (auto& [id, emitter] : remoteShipSparkEmitters) {
|
||
if (!deadRemotePlayers.count(id)) {
|
||
|
||
renderer.PushMatrix();
|
||
renderer.LoadIdentity();
|
||
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
|
||
renderer.RotateMatrix(Environment::inverseShipMatrix);
|
||
renderer.TranslateMatrix(-Environment::shipState.position);
|
||
emitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
|
||
renderer.PopMatrix();
|
||
}
|
||
}
|
||
renderer.PopMatrix();
|
||
}
|
||
|
||
void Space::processTickCount(int64_t newTickCount, int64_t delta) {
|
||
|
||
auto now_ms = newTickCount;
|
||
|
||
if (firePressed)
|
||
{
|
||
firePressed = false;
|
||
if (now_ms - lastProjectileFireTime >= static_cast<uint64_t>(projectileCooldownMs)) {
|
||
lastProjectileFireTime = now_ms;
|
||
const float projectileSpeed = PROJECTILE_VELOCITY;
|
||
|
||
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 - 6.0f, 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<int>(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 (networkClient && id == networkClient->GetClientId()) {
|
||
continue;
|
||
}
|
||
|
||
if (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay);
|
||
|
||
remotePlayerStates[id] = playerState;
|
||
}
|
||
|
||
updateSparkEmitters(static_cast<float>(delta));
|
||
|
||
for (auto& p : projectiles) {
|
||
if (p && p->isActive()) {
|
||
p->update(static_cast<float>(delta), renderer);
|
||
}
|
||
}
|
||
|
||
|
||
for (const auto& p : projectiles) {
|
||
if (p && p->isActive()) {
|
||
Vector3f worldPos = p->getPosition();
|
||
p->projectileEmitter.resetEmissionPoints({ worldPos });
|
||
p->projectileEmitter.update(static_cast<float>(delta));
|
||
}
|
||
}
|
||
|
||
|
||
explosionEmitter.update(static_cast<float>(delta));
|
||
if (showExplosion) {
|
||
uint64_t now = SDL_GetTicks64();
|
||
if (lastExplosionTime != 0 && now - lastExplosionTime >= explosionDurationMs) {
|
||
showExplosion = false;
|
||
explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
|
||
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.selectedVelocity = 0;
|
||
Environment::shipState.velocity = 0.0f;
|
||
showExplosion = true;
|
||
|
||
explosionEmitter.setUseWorldSpace(true);
|
||
explosionEmitter.setEmissionPoints(std::vector<Vector3f>{ collisionPoint });
|
||
explosionEmitter.emit();
|
||
lastExplosionTime = SDL_GetTicks64();
|
||
|
||
std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n";
|
||
|
||
menuManager.showGameOver(this->playerScore);
|
||
}
|
||
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<int>(planetObject.planetStones.allInstances.size()))
|
||
continue;
|
||
|
||
if (planetObject.planetStones.statuses.size() <= static_cast<size_t>(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<Vector3f>{ collidedStonePos });
|
||
explosionEmitter.emit();
|
||
lastExplosionTime = SDL_GetTicks64();
|
||
|
||
std::cerr << "GAME OVER: collision with stone on triangle " << collidedTriIdx << std::endl;
|
||
|
||
if (collidedTriIdx >= 0 && collidedTriIdx < static_cast<int>(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<int>(planetObject.planetStones.statuses.size())) {
|
||
planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty;
|
||
}
|
||
|
||
menuManager.showGameOver(this->playerScore);
|
||
}
|
||
}
|
||
}
|
||
|
||
planetObject.update(static_cast<float>(delta));
|
||
|
||
// update velocity text
|
||
|
||
if (shipAlive && !gameOver) {
|
||
auto velocityTv = menuManager.uiManager.findTextView("velocityText");
|
||
if (velocityTv) {
|
||
std::string velocityStr = "Velocity: " + std::to_string(static_cast<int>(Environment::shipState.velocity));
|
||
menuManager.uiManager.setText("velocityText", velocityStr);
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
void Space::fireProjectiles() {
|
||
std::vector<Vector3f> localOffsets = {
|
||
Vector3f{ -1.5f, 0.9f - 6.f, 5.0f },
|
||
Vector3f{ 1.5f, 0.9f - 6.f, 5.0f }
|
||
};
|
||
|
||
const float projectileSpeed = PROJECTILE_VELOCITY;
|
||
const float lifeMs = PROJECTILE_LIFE;
|
||
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);
|
||
p->projectileEmitter = SparkEmitter(projectileEmitter);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
void Space::update() {
|
||
if (networkClient) {
|
||
if (networkClient->IsConnected()) {
|
||
wasConnectedToServer = true;
|
||
}
|
||
else if (wasConnectedToServer && shipAlive && !gameOver) {
|
||
wasConnectedToServer = false;
|
||
shipAlive = false;
|
||
gameOver = true;
|
||
Environment::shipState.velocity = 0.0f;
|
||
std::cout << "Client: Lost connection to server\n";
|
||
menuManager.showConnectionLost();
|
||
}
|
||
|
||
auto pending = networkClient->getPendingProjectiles();
|
||
if (!pending.empty()) {
|
||
const float projectileSpeed = PROJECTILE_VELOCITY;
|
||
const float lifeMs = PROJECTILE_LIFE;
|
||
const float size = 0.5f;
|
||
for (const auto& pi : pending) {
|
||
const std::vector<Vector3f> localOffsets = {
|
||
Vector3f{ -1.5f, 0.9f - 6.0f, 5.0f },
|
||
Vector3f{ 1.5f, 0.9f - 6.0f, 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);
|
||
p->projectileEmitter = SparkEmitter(projectileEmitter);
|
||
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<Vector3f>{ 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(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;
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
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 disconnects = networkClient->getPendingDisconnects();
|
||
for (int pid : disconnects) {
|
||
remotePlayerStates.erase(pid);
|
||
deadRemotePlayers.erase(pid);
|
||
remoteShipSparkEmitters.erase(pid);
|
||
if (trackedTargetId == pid) {
|
||
trackedTargetId = -1;
|
||
targetAcquireAnim = 0.f;
|
||
}
|
||
std::cout << "Client: Remote player " << pid << " left the game, removed from scene\n";
|
||
}
|
||
|
||
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<Vector3f>{ 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
|