Merge branch 'main' into linux

This commit is contained in:
Vladislav Khorev 2026-02-23 07:04:02 +00:00
commit 03323638e1
26 changed files with 9654 additions and 1962 deletions

View File

@ -71,6 +71,10 @@ set(SOURCES
../src/network/WebSocketClientEmscripten.cpp
../src/render/TextRenderer.h
../src/render/TextRenderer.cpp
../src/MenuManager.h
../src/MenuManager.cpp
../src/Space.h
../src/Space.cpp
)
add_executable(space-game001 ${SOURCES})

View File

@ -63,6 +63,10 @@ add_executable(space-game001
../src/network/WebSocketClientEmscripten.cpp
../src/render/TextRenderer.h
../src/render/TextRenderer.cpp
../src/MenuManager.h
../src/MenuManager.cpp
../src/Space.h
../src/Space.cpp
)
# Установка проекта по умолчанию для Visual Studio

BIN
resources/Cargo_Base_color_sRGB.png (Stored with Git LFS) Normal file

Binary file not shown.

6175
resources/cargoship001.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -96,11 +96,24 @@
},
{
"type": "Button",
"name": "exitButton",
"name": "multiplayerButton2",
"x": 409,
"y": 218,
"width": 382,
"height": 56,
"textures": {
"normal": "resources/main_menu/multi.png",
"hover": "resources/main_menu/multi.png",
"pressed": "resources/main_menu/multi.png"
}
},
{
"type": "Button",
"name": "exitButton",
"x": 409,
"y": 147,
"width": 382,
"height": 56,
"textures": {
"normal": "resources/main_menu/exit.png",
"hover": "resources/main_menu/exit.png",
@ -111,7 +124,7 @@
"type": "Button",
"name": "versionLabel",
"x": 559.5,
"y": 170,
"y": 99,
"width": 81,
"height": 9,
"textures": {

View File

@ -0,0 +1,105 @@
{
"root": {
"type": "LinearLayout",
"x": 0,
"y": 0,
"width": 1920,
"height": 1080,
"orientation": "vertical",
"spacing": 20,
"children": [
{
"type": "TextView",
"name": "titleText",
"x": 300,
"y": 100,
"width": 1320,
"height": 100,
"text": "Multiplayer",
"fontPath": "resources/fonts/DroidSans.ttf",
"fontSize": 72,
"color": [1, 1, 1, 1],
"centered": true
},
{
"type": "TextView",
"name": "serverLabel",
"x": 400,
"y": 250,
"width": 1120,
"height": 50,
"text": "Enter server name or IP:",
"fontPath": "resources/fonts/DroidSans.ttf",
"fontSize": 32,
"color": [1, 1, 1, 1],
"centered": false
},
{
"type": "TextField",
"name": "serverInputField",
"x": 400,
"y": 320,
"width": 1120,
"height": 60,
"placeholder": "Enter server name or IP",
"fontPath": "resources/fonts/DroidSans.ttf",
"fontSize": 28,
"maxLength": 256,
"color": [1, 1, 1, 1],
"placeholderColor": [0.6, 0.6, 0.6, 1],
"backgroundColor": [0.15, 0.15, 0.15, 1],
"borderColor": [0.7, 0.7, 0.7, 1]
},
{
"type": "LinearLayout",
"x": 400,
"y": 450,
"width": 1120,
"height": 80,
"orientation": "horizontal",
"spacing": 30,
"children": [
{
"type": "Button",
"name": "connectButton",
"x": 0,
"y": 0,
"width": 530,
"height": 80,
"textures": {
"normal": "resources/main_menu/single.png",
"hover": "resources/main_menu/single.png",
"pressed": "resources/main_menu/single.png"
}
},
{
"type": "Button",
"name": "backButton",
"x": 590,
"y": 0,
"width": 530,
"height": 80,
"textures": {
"normal": "resources/main_menu/exit.png",
"hover": "resources/main_menu/exit.png",
"pressed": "resources/main_menu/exit.png"
}
}
]
},
{
"type": "TextView",
"name": "statusText",
"x": 400,
"y": 580,
"width": 1120,
"height": 50,
"text": "Ready to connect",
"fontPath": "resources/fonts/DroidSans.ttf",
"fontSize": 24,
"color": [0.8, 0.8, 0.8, 1],
"centered": false
}
]
}
}

BIN
resources/gameover.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/loading.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -41,42 +41,6 @@ std::vector<std::string> split(const std::string& s, char delimiter) {
return tokens;
}
// Вспомогательная функция для проверки столкновения снаряда с объектом-сферой
bool checkSegmentSphereCollision(
int x,
const Eigen::Vector3f& pStart,
const Eigen::Vector3f& pEnd,
const Eigen::Vector3f& targetCenter,
float combinedRadius)
{
Eigen::Vector3f segment = pEnd - pStart;
Eigen::Vector3f toTarget = targetCenter - pStart;
float segmentLenSq = segment.squaredNorm();
if (segmentLenSq < 1e-6f) {
return toTarget.norm() <= combinedRadius;
}
// Находим проекцию точки targetCenter на прямую, содержащую отрезок
// t — это нормализованный параметр вдоль отрезка (от 0 до 1)
float t = toTarget.dot(segment) / segmentLenSq;
// Ограничиваем t, чтобы найти ближайшую точку именно на ОТРЕЗКЕ
t = std::max(0.0f, std::min(1.0f, t));
// Ближайшая точка на отрезке к центру цели
Eigen::Vector3f closestPoint = pStart + t * segment;
/*
std::cout << "Collision for box: " << x << " pStart: " << pStart
<< " pEnd: " << pEnd
<< " targetCenter: " << targetCenter
<< " closestPoint: " << closestPoint
<< " t: " << t << std::endl;
*/
// Проверяем расстояние от ближайшей точки до центра цели
return (targetCenter - closestPoint).squaredNorm() <= (combinedRadius * combinedRadius);
}
struct ServerBox {
Eigen::Vector3f position;
Eigen::Matrix3f rotation;
@ -99,6 +63,9 @@ struct BoxDestroyedInfo {
int destroyedBy = -1;
};
std::vector<BoxDestroyedInfo> g_boxDestructions;
std::mutex g_boxDestructions_mutex;
std::vector<ServerBox> g_serverBoxes;
std::mutex g_boxes_mutex;
@ -217,15 +184,12 @@ public:
timer->expires_after(std::chrono::milliseconds(100));
timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) {
if (!ec) {
int64_t serverNow = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
self->send_message("ID:" + std::to_string(self->id_) + ":" + std::to_string(serverNow));
self->send_message("ID:" + std::to_string(self->id_));
self->do_read();
}
});
}
ClientState get_latest_state(std::chrono::system_clock::time_point now) {
if (timedClientStates.timedStates.empty()) {
return {};
@ -295,8 +259,6 @@ private:
}
std::string cleanMessage = msg.substr(0, msg.find("#hash:"));
std::cout << "Received from player " << id_ << ": " << cleanMessage << std::endl;
auto parts = split(cleanMessage, ':');
@ -378,8 +340,8 @@ private:
{
const std::vector<Eigen::Vector3f> localOffsets = {
Eigen::Vector3f(-1.5f, 0.9f, 5.0f),
Eigen::Vector3f(1.5f, 0.9f, 5.0f)
Eigen::Vector3f(-1.5f, 0.9f - 6.f, 5.0f),
Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
};
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
@ -427,61 +389,242 @@ void broadcastToAll(const std::string& message) {
}
}
void checkShipBoxCollisions(std::chrono::system_clock::time_point now, uint64_t now_ms, std::vector<BoxDestroyedInfo>& boxDestructions) {
// Внимание: Мьютексы g_boxes_mutex и g_sessions_mutex должны быть захвачены
// внешним кодом в update_world перед вызовом этой функции.
void update_world(net::steady_timer& timer, net::io_context& ioc) {
const float shipCollisionRadius = 15.0f;
const float boxCollisionRadius = 2.0f;
const float thresh = shipCollisionRadius + boxCollisionRadius;
const float threshSq = thresh * thresh;
static auto last_snapshot_time = std::chrono::steady_clock::now();
auto now = std::chrono::steady_clock::now();
/*static uint64_t lastTickCount = 0;
for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
if (g_serverBoxes[bi].destroyed) continue;
if (lastTickCount == 0) {
//lastTickCount = SDL_GetTicks64();
lastTickCount = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
// Центр ящика в мировых координатах
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
lastTickCount = (lastTickCount / 50) * 50;
for (auto& session : g_sessions) {
int playerId = session->get_id();
// Пропускаем мертвых игроков
{
// Если g_dead_mutex не захвачен глобально в update_world, раскомментируйте:
// std::lock_guard<std::mutex> gd(g_dead_mutex);
if (g_dead_players.count(playerId)) continue;
return;
}
ClientState shipState;
// Получаем состояние игрока на текущий момент времени сервера
if (!session->fetchStateAtTime(now, shipState)) continue;
Eigen::Vector3f diff = shipState.position - boxWorld;
auto newTickCount = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
// Проверка столкновения сфер
if (diff.squaredNorm() <= threshSq) {
g_serverBoxes[bi].destroyed = true;
newTickCount = (newTickCount / 50) * 50;
// Регистрируем уничтожение ящика
BoxDestroyedInfo destruction;
destruction.boxIndex = static_cast<int>(bi);
destruction.serverTime = now_ms;
destruction.position = boxWorld;
destruction.destroyedBy = playerId;
int64_t deltaMs = static_cast<int64_t>(newTickCount - lastTickCount);
boxDestructions.push_back(destruction);
std::chrono::system_clock::time_point nowRounded = std::chrono::system_clock::time_point(std::chrono::milliseconds(newTickCount));
*/
// For each player
// Get letest state + add time (until newTickCount)
// Calculate if collisions with boxes
std::cout << "Server: Box " << bi << " smashed by player " << playerId << std::endl;
// Один ящик не может быть уничтожен дважды за один проход
// Рассылка Snapshot раз в 1000мс
/*
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_snapshot_time).count() >= 1000) {
last_snapshot_time = now;
auto system_now = std::chrono::system_clock::now();
std::string snapshot_msg = "SNAPSHOT:" + std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(
system_now.time_since_epoch()).count()
);
std::lock_guard<std::mutex> lock(g_sessions_mutex);
// Формируем общую строку состояний всех игроков
for (auto& session : g_sessions) {
ClientState st = session->get_latest_state(system_now);
snapshot_msg += "|" + std::to_string(session->get_id()) + ":" + st.formPingMessageContent();
}
for (auto& session : g_sessions) {
session->send_message(snapshot_msg);
}
}*/
const std::chrono::milliseconds interval(50);
timer.expires_after(interval);
timer.async_wait([&](const boost::system::error_code& ec) {
if (ec) return;
auto now = std::chrono::system_clock::now();
uint64_t now_ms = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());
std::vector<DeathInfo> deathEvents;
{
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
std::vector<int> indicesToRemove;
float dt = 50.0f / 1000.0f;
for (size_t i = 0; i < g_projectiles.size(); ++i) {
auto& pr = g_projectiles[i];
pr.pos += pr.vel * dt;
if (now_ms > pr.spawnMs + static_cast<uint64_t>(pr.lifeMs)) {
indicesToRemove.push_back(static_cast<int>(i));
continue;
}
bool hitDetected = false;
{
std::lock_guard<std::mutex> lm(g_sessions_mutex);
std::lock_guard<std::mutex> gd(g_dead_mutex);
for (auto& session : g_sessions) {
int targetId = session->get_id();
if (targetId == pr.shooterId) continue;
if (g_dead_players.find(targetId) != g_dead_players.end()) continue;
ClientState targetState;
if (!session->fetchStateAtTime(now, targetState)) continue;
Eigen::Vector3f diff = pr.pos - targetState.position;
const float shipRadius = 15.0f;
const float projectileRadius = 1.5f;
float combinedRadius = shipRadius + projectileRadius;
if (diff.squaredNorm() <= combinedRadius * combinedRadius) {
DeathInfo death;
death.targetId = targetId;
death.serverTime = now_ms;
death.position = pr.pos;
death.killerId = pr.shooterId;
deathEvents.push_back(death);
g_dead_players.insert(targetId);
indicesToRemove.push_back(static_cast<int>(i));
hitDetected = true;
std::cout << "Server: *** HIT DETECTED! ***" << std::endl;
std::cout << "Server: Projectile at ("
<< pr.pos.x() << ", " << pr.pos.y() << ", " << pr.pos.z()
<< ") hit player " << targetId << std::endl;
break;
}
}
}
}
void dispatchEvents(const std::vector<DeathInfo>& deathEvents, const std::vector<BoxDestroyedInfo>& boxDestructions) {
// 1. Рассылка событий смерти игроков
if (hitDetected) continue;
}
if (!indicesToRemove.empty()) {
std::sort(indicesToRemove.rbegin(), indicesToRemove.rend());
for (int idx : indicesToRemove) {
if (idx >= 0 && idx < (int)g_projectiles.size()) {
g_projectiles.erase(g_projectiles.begin() + idx);
}
}
}
}
{
std::lock_guard<std::mutex> bm(g_boxes_mutex);
//const float projectileHitRadius = 1.5f;
const float projectileHitRadius = 5.0f;
const float boxCollisionRadius = 2.0f;
std::vector<std::pair<size_t, size_t>> boxProjectileCollisions;
for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
if (g_serverBoxes[bi].destroyed) continue;
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
for (size_t pi = 0; pi < g_projectiles.size(); ++pi) {
const auto& pr = g_projectiles[pi];
Eigen::Vector3f diff = pr.pos - boxWorld;
//std::cout << "diff norm is " << diff.norm() << std::endl;
float thresh = boxCollisionRadius + projectileHitRadius;
if (diff.squaredNorm() <= thresh * thresh) {
boxProjectileCollisions.push_back({ bi, pi });
}
}
}
for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) {
g_serverBoxes[boxIdx].destroyed = true;
Eigen::Vector3f boxWorld = g_serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
BoxDestroyedInfo destruction;
destruction.boxIndex = static_cast<int>(boxIdx);
destruction.serverTime = now_ms;
destruction.position = boxWorld;
destruction.destroyedBy = g_projectiles[projIdx].shooterId;
{
std::lock_guard<std::mutex> dm(g_boxDestructions_mutex);
g_boxDestructions.push_back(destruction);
}
std::cout << "Server: Box " << boxIdx << " destroyed by projectile from player "
<< g_projectiles[projIdx].shooterId << std::endl;
}
}
{
std::lock_guard<std::mutex> bm(g_boxes_mutex);
std::lock_guard<std::mutex> lm(g_sessions_mutex);
const float shipCollisionRadius = 15.0f;
const float boxCollisionRadius = 2.0f;
for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
if (g_serverBoxes[bi].destroyed) continue;
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
for (auto& session : g_sessions) {
{
std::lock_guard<std::mutex> gd(g_dead_mutex);
if (g_dead_players.find(session->get_id()) != g_dead_players.end()) {
continue;
}
}
ClientState shipState;
if (!session->fetchStateAtTime(now, shipState)) continue;
Eigen::Vector3f diff = shipState.position - boxWorld;
float thresh = shipCollisionRadius + boxCollisionRadius;
if (diff.squaredNorm() <= thresh * thresh) {
g_serverBoxes[bi].destroyed = true;
BoxDestroyedInfo destruction;
destruction.boxIndex = static_cast<int>(bi);
destruction.serverTime = now_ms;
destruction.position = boxWorld;
destruction.destroyedBy = session->get_id();
{
std::lock_guard<std::mutex> dm(g_boxDestructions_mutex);
g_boxDestructions.push_back(destruction);
}
std::cout << "Server: Box " << bi << " destroyed by ship collision with player "
<< session->get_id() << std::endl;
break;
}
}
}
}
if (!deathEvents.empty()) {
for (const auto& death : deathEvents) {
std::string deadMsg = "DEAD:" +
std::to_string(death.serverTime) + ":" +
@ -496,10 +639,11 @@ void dispatchEvents(const std::vector<DeathInfo>& deathEvents, const std::vector
std::cout << "Server: Sent DEAD event - Player " << death.targetId
<< " killed by " << death.killerId << std::endl;
}
}
// 2. Рассылка событий разрушения ящиков
for (const auto& destruction : boxDestructions) {
{
std::lock_guard<std::mutex> dm(g_boxDestructions_mutex);
for (const auto& destruction : g_boxDestructions) {
std::string boxMsg = "BOX_DESTROYED:" +
std::to_string(destruction.boxIndex) + ":" +
std::to_string(destruction.serverTime) + ":" +
@ -509,105 +653,11 @@ void dispatchEvents(const std::vector<DeathInfo>& deathEvents, const std::vector
std::to_string(destruction.destroyedBy);
broadcastToAll(boxMsg);
std::cout << "Server: Broadcasted BOX_DESTROYED for box "
<< destruction.boxIndex << std::endl;
std::cout << "Server: Broadcasted BOX_DESTROYED for box " << destruction.boxIndex << std::endl;
}
}
void update_world(net::steady_timer& timer, net::io_context& ioc) {
const std::chrono::milliseconds interval(50);
timer.expires_after(interval);
timer.async_wait([&](const boost::system::error_code& ec) {
if (ec) return;
auto now = std::chrono::system_clock::now();
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
float dt = 50.0f / 1000.0f;
std::vector<DeathInfo> deathEvents;
std::vector<BoxDestroyedInfo> boxDestructions;
std::vector<int> projectilesToRemove;
{
// Захватываем необходимые данные под мьютексами один раз
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
std::lock_guard<std::mutex> bm(g_boxes_mutex);
std::lock_guard<std::mutex> sm(g_sessions_mutex);
std::lock_guard<std::mutex> gm(g_dead_mutex);
for (size_t i = 0; i < g_projectiles.size(); ++i) {
auto& pr = g_projectiles[i];
Eigen::Vector3f oldPos = pr.pos;
pr.pos += pr.vel * dt;
Eigen::Vector3f newPos = pr.pos;
// 1. Проверка времени жизни снаряда
if (now_ms > pr.spawnMs + static_cast<uint64_t>(pr.lifeMs)) {
projectilesToRemove.push_back(static_cast<int>(i));
continue;
g_boxDestructions.clear();
}
bool hitDetected = false;
// 2. Проверка коллизий снаряда с игроками (Ray-cast)
for (auto& session : g_sessions) {
int targetId = session->get_id();
if (targetId == pr.shooterId || g_dead_players.count(targetId)) continue;
ClientState targetState;
if (!session->fetchStateAtTime(now, targetState)) continue;
if (checkSegmentSphereCollision(0, oldPos, newPos, targetState.position, 15.0f + 1.5f)) {
deathEvents.push_back({ targetId, now_ms, newPos, pr.shooterId });
g_dead_players.insert(targetId);
hitDetected = true;
break;
}
}
if (hitDetected) {
projectilesToRemove.push_back(static_cast<int>(i));
continue;
}
// 3. Проверка коллизий снаряда с ящиками (Ray-cast)
for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
if (g_serverBoxes[bi].destroyed) continue;
// Центр ящика с учетом смещения мира
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
if (checkSegmentSphereCollision(bi, oldPos, newPos, boxWorld, 2.0f + 1.5f)) {
g_serverBoxes[bi].destroyed = true;
boxDestructions.push_back({ static_cast<int>(bi), now_ms, boxWorld, pr.shooterId });
hitDetected = true;
break;
}
}
if (hitDetected) {
projectilesToRemove.push_back(static_cast<int>(i));
}
}
// Удаляем отработавшие снаряды (с конца)
std::sort(projectilesToRemove.rbegin(), projectilesToRemove.rend());
for (int idx : projectilesToRemove) {
g_projectiles.erase(g_projectiles.begin() + idx);
}
}
// 4. Отдельная проверка столкновения кораблей с ящиками (Point-Sphere)
// Эту логику оставляем отдельно, так как она не привязана к снарядам
checkShipBoxCollisions(now, now_ms, boxDestructions);
// Рассылка событий
dispatchEvents(deathEvents, boxDestructions);
update_world(timer, ioc);
});
}
@ -666,6 +716,7 @@ int main() {
{
std::lock_guard<std::mutex> lock(g_boxes_mutex);
g_serverBoxes = generateServerBoxes(50);
//g_serverBoxes = generateServerBoxes(1);
std::cout << "Generated " << g_serverBoxes.size() << " boxes on server\n";
}
net::io_context ioc;

File diff suppressed because it is too large Load Diff

View File

@ -14,25 +14,20 @@
#include <string>
#include <memory>
#include <render/TextRenderer.h>
#include "MenuManager.h"
#include "Space.h"
#include <unordered_set>
namespace ZL {
struct BoxCoords
{
Vector3f pos;
Matrix3f m;
};
class Game {
public:
Game();
~Game();
void setup();
void setupPart2();
void update();
void render();
@ -41,23 +36,20 @@ namespace ZL {
Renderer renderer;
TaskManager taskManager;
MainThreadHandler mainThreadHandler;
std::unique_ptr<INetworkClient> networkClient;
std::shared_ptr<Texture> loadingTexture;
VertexRenderStruct loadingMesh;
bool loadingCompleted = false;
private:
int64_t getSyncTimeMs();
void processTickCount();
void drawScene();
void drawCubemap(float skyPercent);
void drawShip();
void drawBoxes();
void drawBoxesLabels();
void drawUI();
void drawRemoteShips();
void drawRemoteShipsLabels();
void fireProjectiles();
bool worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth) const;
void drawUnderMainMenu();
void drawLoading();
void handleDown(int mx, int my);
void handleUp(int mx, int my);
void handleMotion(int mx, int my);
@ -65,77 +57,16 @@ namespace ZL {
SDL_Window* window;
SDL_GLContext glContext;
int64_t newTickCount;
int64_t lastTickCount;
std::vector<BoxCoords> boxCoordsArr;
std::vector<VertexRenderStruct> boxRenderArr;
std::vector<std::string> boxLabels;
std::unique_ptr<TextRenderer> textRenderer;
//std::unordered_map<int, ClientStateInterval> latestRemotePlayers;
std::unordered_map<int, ClientState> remotePlayerStates;
float newShipVelocity = 0;
static const size_t CONST_TIMER_INTERVAL = 10;
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
std::shared_ptr<Texture> sparkTexture;
std::shared_ptr<Texture> spaceshipTexture;
std::shared_ptr<Texture> cubemapTexture;
VertexDataStruct spaceshipBase;
VertexRenderStruct spaceship;
VertexRenderStruct cubemap;
std::shared_ptr<Texture> boxTexture;
VertexDataStruct boxBase;
SparkEmitter sparkEmitter;
SparkEmitter projectileEmitter;
SparkEmitter explosionEmitter;
PlanetObject planetObject;
UiManager uiManager;
std::vector<std::unique_ptr<Projectile>> projectiles;
std::shared_ptr<Texture> projectileTexture;
float projectileCooldownMs = 500.0f;
int64_t lastProjectileFireTime = 0;
int maxProjectiles = 32;
std::vector<Vector3f> shipLocalEmissionPoints;
bool shipAlive = true;
bool gameOver = false;
std::vector<bool> boxAlive;
float shipCollisionRadius = 15.0f;
float boxCollisionRadius = 2.0f;
bool uiGameOverShown = false;
bool showExplosion = false;
uint64_t lastExplosionTime = 0;
const uint64_t explosionDurationMs = 500;
bool firePressed = false;
bool serverBoxesApplied = false;
static constexpr float MAX_DIST_SQ = 10000.f * 10000.f;
static constexpr float FADE_START = 6000.f;
static constexpr float FADE_RANGE = 4000.f;
static constexpr float BASE_SCALE = 140.f;
static constexpr float PERSPECTIVE_K = 0.05f; // Tune
static constexpr float MIN_SCALE = 0.4f;
static constexpr float MAX_SCALE = 0.8f;
static constexpr float CLOSE_DIST = 600.0f;
std::unordered_set<int> deadRemotePlayers;
MenuManager menuManager;
Space space;
int spaceGameStarted = 0;
};

198
src/MenuManager.cpp Normal file
View File

@ -0,0 +1,198 @@
#include "MenuManager.h"
namespace ZL {
MenuManager::MenuManager(Renderer& iRenderer) :
renderer(iRenderer)
{
}
void MenuManager::setupMenu()
{
uiManager.loadFromFile("resources/config/main_menu.json", renderer, CONST_ZIP_FILE);
uiSavedRoot = loadUiFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE);
settingsSavedRoot = loadUiFromFile("resources/config/settings.json", renderer, CONST_ZIP_FILE);
multiplayerSavedRoot = loadUiFromFile("resources/config/multiplayer_menu.json", renderer, CONST_ZIP_FILE);
gameOverSavedRoot = loadUiFromFile("resources/config/game_over.json", renderer, CONST_ZIP_FILE);
std::function<void()> loadGameplayUI;
loadGameplayUI = [this]() {
uiManager.replaceRoot(uiSavedRoot);
auto velocityTv = uiManager.findTextView("velocityText");
if (velocityTv) {
velocityTv->rect.x = 10.0f;
velocityTv->rect.y = static_cast<float>(Environment::height) - velocityTv->rect.h - 10.0f;
}
else {
std::cerr << "Failed to find velocityText in UI" << std::endl;
}
uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
static bool isExitButtonAnimating = false;
uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() {
std::cerr << "Settings button animation finished -> переход в настройки" << std::endl;
if (uiManager.pushMenuFromSavedRoot(settingsSavedRoot)) {
uiManager.setButtonCallback("Opt1", [this](const std::string& n) {
std::cerr << "Opt1 pressed: " << n << std::endl;
});
uiManager.setButtonCallback("Opt2", [this](const std::string& n) {
std::cerr << "Opt2 pressed: " << n << std::endl;
});
uiManager.setButtonCallback("backButton", [this](const std::string& n) {
uiManager.stopAllAnimations();
uiManager.popMenu();
});
}
else {
std::cerr << "Failed to open settings menu after animations" << std::endl;
}
});
uiManager.setAnimationCallback("exitButton", "bgScroll", [this]() {
std::cerr << "Exit button bgScroll animation finished" << std::endl;
g_exitBgAnimating = false;
});
uiManager.setButtonCallback("playButton", [this](const std::string& name) {
std::cerr << "Play button pressed: " << name << std::endl;
});
uiManager.setButtonCallback("settingsButton", [this](const std::string& name) {
std::cerr << "Settings button pressed: " << name << std::endl;
uiManager.startAnimationOnNode("playButton", "buttonsExit");
uiManager.startAnimationOnNode("settingsButton", "buttonsExit");
uiManager.startAnimationOnNode("exitButton", "buttonsExit");
});
uiManager.setButtonCallback("exitButton", [this](const std::string& name) {
std::cerr << "Exit button pressed: " << name << std::endl;
if (!g_exitBgAnimating) {
std::cerr << "start repeat anim bgScroll on exitButton" << std::endl;
g_exitBgAnimating = true;
uiManager.startAnimationOnNode("exitButton", "bgScroll");
}
else {
std::cerr << "stop repeat anim bgScroll on exitButton" << std::endl;
g_exitBgAnimating = false;
uiManager.stopAnimationOnNode("exitButton", "bgScroll");
auto exitButton = uiManager.findButton("exitButton");
if (exitButton) {
exitButton->animOffsetX = 0.0f;
exitButton->animOffsetY = 0.0f;
exitButton->animScaleX = 1.0f;
exitButton->animScaleY = 1.0f;
exitButton->buildMesh();
}
}
});
uiManager.setButtonCallback("shootButton", [this](const std::string& name) {
onFirePressed();
});
uiManager.setButtonCallback("shootButton2", [this](const std::string& name) {
onFirePressed();
});
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
int newVel = roundf(value * 10);
/*if (newVel > 2)
{
newVel = 2;
}*/
if (newVel != Environment::shipState.selectedVelocity) {
onVelocityChanged(newVel);
}
});
};
uiManager.setButtonCallback("singleButton", [loadGameplayUI, this](const std::string& name) {
std::cerr << "Single button pressed: " << name << " -> load gameplay UI\n";
loadGameplayUI();
onSingleplayerPressed();
});
uiManager.setButtonCallback("multiplayerButton", [loadGameplayUI, this](const std::string& name) {
std::cerr << "Multiplayer button pressed: " << name << " -> load gameplay UI\n";
loadGameplayUI();
onMultiplayerPressed();
});
uiManager.setButtonCallback("multiplayerButton2", [this](const std::string& name) {
std::cerr << "Multiplayer button pressed → opening multiplayer menu\n";
uiManager.startAnimationOnNode("playButton", "buttonsExit");
uiManager.startAnimationOnNode("settingsButton", "buttonsExit");
uiManager.startAnimationOnNode("multiplayerButton", "buttonsExit");
uiManager.startAnimationOnNode("exitButton", "buttonsExit");
if (uiManager.pushMenuFromSavedRoot(multiplayerSavedRoot)) {
// Callback для кнопки подключения
uiManager.setButtonCallback("connectButton", [this](const std::string& buttonName) {
std::string serverAddress = uiManager.getTextFieldValue("serverInputField");
if (serverAddress.empty()) {
uiManager.setText("statusText", "Please enter server address");
return;
}
uiManager.setText("statusText", "Connecting to " + serverAddress + "...");
std::cerr << "Connecting to server: " << serverAddress << std::endl;
// Здесь добавить вашу логику подключения к серверу
// connectToServer(serverAddress);
});
// Callback для кнопки назад
uiManager.setButtonCallback("backButton", [this](const std::string& buttonName) {
uiManager.popMenu();
});
// Callback для отслеживания ввода текста
uiManager.setTextFieldCallback("serverInputField",
[this](const std::string& fieldName, const std::string& newText) {
std::cout << "Server input field changed to: " << newText << std::endl;
});
std::cerr << "Multiplayer menu loaded successfully\n";
}
else {
std::cerr << "Failed to load multiplayer menu\n";
}
});
uiManager.setButtonCallback("exitButton", [](const std::string& name) {
std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n";
Environment::exitGameLoop = true;
});
}
void MenuManager::showGameOver()
{
if (!uiGameOverShown) {
if (uiManager.pushMenuFromSavedRoot(gameOverSavedRoot)) {
uiManager.setButtonCallback("restartButton", [this](const std::string& name) {
uiGameOverShown = false;
uiManager.popMenu();
onRestartPressed();
});
uiManager.setButtonCallback("gameOverExitButton", [this](const std::string& name) {
Environment::exitGameLoop = true;
});
uiGameOverShown = true;
}
else {
std::cerr << "Failed to load game_over.json\n";
}
}
}
}

42
src/MenuManager.h Normal file
View File

@ -0,0 +1,42 @@
#pragma once
#include "render/Renderer.h"
#include "Environment.h"
#include "render/TextureManager.h"
#include "UiManager.h"
namespace ZL {
extern const char* CONST_ZIP_FILE;
//extern bool g_exitBgAnimating;
class MenuManager
{
protected:
Renderer& renderer;
std::shared_ptr<UiNode> uiSavedRoot;
std::shared_ptr<UiNode> gameOverSavedRoot;
std::shared_ptr<UiNode> settingsSavedRoot;
std::shared_ptr<UiNode> multiplayerSavedRoot;
public:
bool uiGameOverShown = false;
bool g_exitBgAnimating = false;
UiManager uiManager;
MenuManager(Renderer& iRenderer);
void setupMenu();
void showGameOver();
std::function<void()> onRestartPressed;
std::function<void(float)> onVelocityChanged;
std::function<void()> onFirePressed;
std::function<void()> onSingleplayerPressed;
std::function<void()> onMultiplayerPressed;
};
};

1450
src/Space.cpp Normal file

File diff suppressed because it is too large Load Diff

139
src/Space.h Normal file
View File

@ -0,0 +1,139 @@
#pragma once
#include "render/Renderer.h"
#include "Environment.h"
#include "render/TextureManager.h"
#include "SparkEmitter.h"
#include "planet/PlanetObject.h"
#include "UiManager.h"
#include "Projectile.h"
#include "utils/TaskManager.h"
#include "network/NetworkInterface.h"
#include <queue>
#include <vector>
#include <string>
#include <memory>
#include <render/TextRenderer.h>
#include "MenuManager.h"
#include <unordered_set>
namespace ZL {
struct BoxCoords
{
Vector3f pos;
Matrix3f m;
};
class Space {
public:
Space(Renderer& iRenderer, TaskManager& iTaskManager, MainThreadHandler& iMainThreadHandler, std::unique_ptr<INetworkClient>& iNetworkClient, MenuManager& iMenuManager);
~Space();
void setup();
void update();
Renderer& renderer;
TaskManager& taskManager;
MainThreadHandler& mainThreadHandler;
std::unique_ptr<INetworkClient>& networkClient;
MenuManager& menuManager;
public:
void processTickCount(int64_t newTickCount, int64_t delta);
void drawScene();
void drawCubemap(float skyPercent);
void drawShip();
void drawBoxes();
void drawBoxesLabels();
void drawRemoteShips();
void drawRemoteShipsLabels();
void fireProjectiles();
void handleDown(int mx, int my);
void handleUp(int mx, int my);
void handleMotion(int mx, int my);
std::vector<BoxCoords> boxCoordsArr;
std::vector<VertexRenderStruct> boxRenderArr;
std::vector<std::string> boxLabels;
std::unique_ptr<TextRenderer> textRenderer;
std::unordered_map<int, ClientState> remotePlayerStates;
float newShipVelocity = 0;
static const size_t CONST_TIMER_INTERVAL = 10;
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
std::shared_ptr<Texture> sparkTexture;
std::shared_ptr<Texture> spaceshipTexture;
std::shared_ptr<Texture> cubemapTexture;
VertexDataStruct spaceshipBase;
VertexRenderStruct spaceship;
VertexRenderStruct cubemap;
std::shared_ptr<Texture> boxTexture;
VertexDataStruct boxBase;
SparkEmitter sparkEmitter;
SparkEmitter projectileEmitter;
SparkEmitter explosionEmitter;
PlanetObject planetObject;
std::vector<std::unique_ptr<Projectile>> projectiles;
std::shared_ptr<Texture> projectileTexture;
float projectileCooldownMs = 500.0f;
int64_t lastProjectileFireTime = 0;
int maxProjectiles = 32;
std::vector<Vector3f> shipLocalEmissionPoints;
bool shipAlive = true;
bool gameOver = false;
bool firePressed = false;
std::vector<bool> boxAlive;
float shipCollisionRadius = 15.0f;
float boxCollisionRadius = 2.0f;
bool showExplosion = false;
uint64_t lastExplosionTime = 0;
const uint64_t explosionDurationMs = 500;
bool serverBoxesApplied = false;
static constexpr float MAX_DIST_SQ = 10000.f * 10000.f;
static constexpr float FADE_START = 6000.f;
static constexpr float FADE_RANGE = 4000.f;
static constexpr float BASE_SCALE = 140.f;
static constexpr float PERSPECTIVE_K = 0.05f; // Tune
static constexpr float MIN_SCALE = 0.4f;
static constexpr float MAX_SCALE = 0.8f;
static constexpr float CLOSE_DIST = 600.0f;
std::unordered_set<int> deadRemotePlayers;
// --- Target HUD (brackets + offscreen arrow) ---
int trackedTargetId = -1;
bool targetWasVisible = false;
float targetAcquireAnim = 0.0f; // 0..1 схлопывание (0 = далеко, 1 = на месте)
// временный меш для HUD (будем перезаливать VBO маленькими порциями)
VertexRenderStruct hudTempMesh;
// helpers
void drawTargetHud(); // рисует рамку или стрелку
int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок)
void clearTextRendererCache();
};
} // namespace ZL

View File

@ -168,59 +168,21 @@ namespace ZL {
renderer.DisableVertexAttribArray(vTexCoordName);
}
void UiManager::loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
std::string content;
try {
if (zipFile.empty()) {
content = readTextFile(path);
void UiTextField::draw(Renderer& renderer) const {
if (textRenderer) {
float textX = rect.x + 10.0f;
float textY = rect.y + rect.h / 2.0f;
if (text.empty()) {
textRenderer->drawText(placeholder, textX, textY, 1.0f, false, placeholderColor);
}
else {
auto buf = readFileFromZIP(path, zipFile);
if (buf.empty()) {
std::cerr << "UiManager: failed to read " << path << " from zip " << zipFile << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
textRenderer->drawText(text, textX, textY, 1.0f, false, color);
}
content.assign(buf.begin(), buf.end());
}
}
catch (const std::exception& e) {
std::cerr << "UiManager: failed to open " << path << " : " << e.what() << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
json j;
try {
j = json::parse(content);
}
catch (const std::exception& e) {
std::cerr << "UiManager: json parse error: " << e.what() << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
if (!j.contains("root") || !j["root"].is_object()) {
std::cerr << "UiManager: root node missing or invalid" << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
root = parseNode(j["root"], renderer, zipFile);
layoutNode(root);
buttons.clear();
sliders.clear();
textViews.clear();
collectButtonsAndSliders(root);
nodeActiveAnims.clear();
for (auto& b : buttons) {
b->buildMesh();
}
for (auto& s : sliders) {
s->buildTrackMesh();
s->buildKnobMesh();
}
}
std::shared_ptr<UiNode> UiManager::parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
auto node = std::make_shared<UiNode>();
if (j.contains("type") && j["type"].is_string()) node->type = j["type"].get<std::string>();
if (j.contains("name") && j["name"].is_string()) node->name = j["name"].get<std::string>();
@ -277,7 +239,7 @@ namespace ZL {
if (!t.contains(key) || !t[key].is_string()) return nullptr;
std::string path = t[key].get<std::string>();
try {
std::cout << "UiManager: --loading texture for button '" << "' : " << path << " Zip file: " << zipFile << std::endl;
std::cout << "UiManager: --loading texture for slider '" << s->name << "' : " << path << " Zip file: " << zipFile << std::endl;
auto data = CreateTextureDataFromPng(path.c_str(), zipFile.c_str());
return std::make_shared<Texture>(data);
}
@ -299,6 +261,44 @@ namespace ZL {
node->slider = s;
}
else if (node->type == "TextField") {
auto tf = std::make_shared<UiTextField>();
tf->name = node->name;
tf->rect = node->rect;
if (j.contains("placeholder")) tf->placeholder = j["placeholder"].get<std::string>();
if (j.contains("fontPath")) tf->fontPath = j["fontPath"].get<std::string>();
if (j.contains("fontSize")) tf->fontSize = j["fontSize"].get<int>();
if (j.contains("maxLength")) tf->maxLength = j["maxLength"].get<int>();
if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) {
for (int i = 0; i < 4; ++i) {
tf->color[i] = j["color"][i].get<float>();
}
}
if (j.contains("placeholderColor") && j["placeholderColor"].is_array() && j["placeholderColor"].size() == 4) {
for (int i = 0; i < 4; ++i) {
tf->placeholderColor[i] = j["placeholderColor"][i].get<float>();
}
}
if (j.contains("backgroundColor") && j["backgroundColor"].is_array() && j["backgroundColor"].size() == 4) {
for (int i = 0; i < 4; ++i) {
tf->backgroundColor[i] = j["backgroundColor"][i].get<float>();
}
}
if (j.contains("borderColor") && j["borderColor"].is_array() && j["borderColor"].size() == 4) {
for (int i = 0; i < 4; ++i) {
tf->borderColor[i] = j["borderColor"][i].get<float>();
}
}
tf->textRenderer = std::make_unique<TextRenderer>();
if (!tf->textRenderer->init(renderer, tf->fontPath, tf->fontSize, zipFile)) {
std::cerr << "Failed to init TextRenderer for TextField: " << tf->name << std::endl;
}
node->textField = tf;
}
if (j.contains("animations") && j["animations"].is_object()) {
for (auto it = j["animations"].begin(); it != j["animations"].end(); ++it) {
@ -363,6 +363,76 @@ namespace ZL {
return node;
}
std::shared_ptr<UiNode> loadUiFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile)
{
std::shared_ptr<UiNode> root;
std::string content;
try {
if (zipFile.empty()) {
content = readTextFile(path);
}
else {
auto buf = readFileFromZIP(path, zipFile);
if (buf.empty()) {
std::cerr << "UiManager: failed to read " << path << " from zip " << zipFile << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
content.assign(buf.begin(), buf.end());
}
}
catch (const std::exception& e) {
std::cerr << "UiManager: failed to open " << path << " : " << e.what() << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
json j;
try {
j = json::parse(content);
}
catch (const std::exception& e) {
std::cerr << "UiManager: json parse error: " << e.what() << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
if (!j.contains("root") || !j["root"].is_object()) {
std::cerr << "UiManager: root node missing or invalid" << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
root = parseNode(j["root"], renderer, zipFile);
return root;
}
void UiManager::replaceRoot(std::shared_ptr<UiNode> newRoot) {
root = newRoot;
layoutNode(root);
buttons.clear();
sliders.clear();
textViews.clear();
textFields.clear();
collectButtonsAndSliders(root);
nodeActiveAnims.clear();
for (auto& b : buttons) {
b->buildMesh();
}
for (auto& s : sliders) {
s->buildTrackMesh();
s->buildKnobMesh();
}
}
void UiManager::loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
std::shared_ptr<UiNode> newRoot = loadUiFromFile(path, renderer, zipFile);
replaceRoot(newRoot);
}
void UiManager::layoutNode(const std::shared_ptr<UiNode>& node) {
for (auto& child : node->children) {
child->rect.x += node->rect.x;
@ -406,7 +476,10 @@ namespace ZL {
if (node->textView) {
textViews.push_back(node->textView);
}
for (auto& c : node->children) collectButtonsAndSliders(c); // ìîæíî ïåðåèìåíîâàòü â collectControls
if (node->textField) {
textFields.push_back(node->textField);
}
for (auto& c : node->children) collectButtonsAndSliders(c);
}
bool UiManager::setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb) {
@ -476,13 +549,37 @@ namespace ZL {
return true;
}
bool UiManager::pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
std::shared_ptr<UiTextField> UiManager::findTextField(const std::string& name) {
for (auto& tf : textFields) if (tf->name == name) return tf;
return nullptr;
}
bool UiManager::setTextFieldCallback(const std::string& name, std::function<void(const std::string&, const std::string&)> cb) {
auto tf = findTextField(name);
if (!tf) {
std::cerr << "UiManager: setTextFieldCallback failed, textfield not found: " << name << std::endl;
return false;
}
tf->onTextChanged = std::move(cb);
return true;
}
std::string UiManager::getTextFieldValue(const std::string& name) {
auto tf = findTextField(name);
if (!tf) return "";
return tf->text;
}
bool UiManager::pushMenuFromSavedRoot(std::shared_ptr<UiNode> newRoot)
{
MenuState prev;
prev.root = root;
prev.buttons = buttons;
prev.sliders = sliders;
prev.textFields = textFields;
prev.pressedButton = pressedButton;
prev.pressedSlider = pressedSlider;
prev.focusedTextField = focusedTextField;
prev.path = "";
prev.animCallbacks = animCallbacks;
@ -490,6 +587,7 @@ namespace ZL {
try {
nodeActiveAnims.clear();
animCallbacks.clear();
focusedTextField = nullptr;
for (auto& b : buttons) {
if (b) {
b->animOffsetX = 0.0f;
@ -499,17 +597,22 @@ namespace ZL {
}
}
loadFromFile(path, renderer, zipFile);
replaceRoot(newRoot);
menuStack.push_back(std::move(prev));
return true;
}
catch (const std::exception& e) {
std::cerr << "UiManager: pushMenuFromFile failed to load " << path << " : " << e.what() << std::endl;
std::cerr << "UiManager: pushMenuFromFile failed to load from root : " << e.what() << std::endl;
animCallbacks = prev.animCallbacks;
return false;
}
}
bool UiManager::pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
auto newRoot = loadUiFromFile(path, renderer, zipFile);
return pushMenuFromSavedRoot(newRoot);
}
bool UiManager::popMenu() {
if (menuStack.empty()) {
std::cerr << "UiManager: popMenu called but menu stack is empty" << std::endl;
@ -523,8 +626,10 @@ namespace ZL {
root = s.root;
buttons = s.buttons;
sliders = s.sliders;
textFields = s.textFields;
pressedButton = s.pressedButton;
pressedSlider = s.pressedSlider;
focusedTextField = s.focusedTextField;
animCallbacks = s.animCallbacks;
@ -565,6 +670,9 @@ namespace ZL {
for (const auto& tv : textViews) {
tv->draw(renderer);
}
for (const auto& tf : textFields) {
tf->draw(renderer);
}
renderer.PopMatrix();
renderer.PopProjectionMatrix();
@ -785,6 +893,16 @@ namespace ZL {
break;
}
}
for (auto& tf : textFields) {
if (tf->rect.contains((float)x, (float)y)) {
focusedTextField = tf;
tf->focused = true;
}
else {
tf->focused = false;
}
}
}
void UiManager::onMouseUp(int x, int y) {
@ -806,6 +924,30 @@ namespace ZL {
}
}
void UiManager::onKeyPress(unsigned char key) {
if (!focusedTextField) return;
if (key >= 32 && key <= 126) {
if (focusedTextField->text.length() < (size_t)focusedTextField->maxLength) {
focusedTextField->text += key;
if (focusedTextField->onTextChanged) {
focusedTextField->onTextChanged(focusedTextField->name, focusedTextField->text);
}
}
}
}
void UiManager::onKeyBackspace() {
if (!focusedTextField) return;
if (!focusedTextField->text.empty()) {
focusedTextField->text.pop_back();
if (focusedTextField->onTextChanged) {
focusedTextField->onTextChanged(focusedTextField->name, focusedTextField->text);
}
}
}
std::shared_ptr<UiButton> UiManager::findButton(const std::string& name) {
for (auto& b : buttons) if (b->name == name) return b;
return nullptr;

View File

@ -90,6 +90,26 @@ namespace ZL {
}
};
struct UiTextField {
std::string name;
UiRect rect;
std::string text = "";
std::string placeholder = "";
std::string fontPath = "resources/fonts/DroidSans.ttf";
int fontSize = 32;
std::array<float, 4> color = { 1.f, 1.f, 1.f, 1.f };
std::array<float, 4> placeholderColor = { 0.5f, 0.5f, 0.5f, 1.f };
std::array<float, 4> backgroundColor = { 0.2f, 0.2f, 0.2f, 1.f };
std::array<float, 4> borderColor = { 0.5f, 0.5f, 0.5f, 1.f };
int maxLength = 256;
bool focused = false;
std::unique_ptr<TextRenderer> textRenderer;
std::function<void(const std::string&, const std::string&)> onTextChanged;
void draw(Renderer& renderer) const;
};
struct UiNode {
std::string type;
UiRect rect;
@ -98,6 +118,7 @@ namespace ZL {
std::shared_ptr<UiButton> button;
std::shared_ptr<UiSlider> slider;
std::shared_ptr<UiTextView> textView;
std::shared_ptr<UiTextField> textField;
std::string orientation = "vertical";
float spacing = 0.0f;
@ -115,10 +136,15 @@ namespace ZL {
std::map<std::string, AnimSequence> animations;
};
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile);
std::shared_ptr<UiNode> loadUiFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
class UiManager {
public:
UiManager() = default;
void replaceRoot(std::shared_ptr<UiNode> newRoot);
void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
void draw(Renderer& renderer);
@ -126,9 +152,11 @@ namespace ZL {
void onMouseMove(int x, int y);
void onMouseDown(int x, int y);
void onMouseUp(int x, int y);
void onKeyPress(unsigned char key);
void onKeyBackspace();
bool isUiInteraction() const {
return pressedButton != nullptr || pressedSlider != nullptr;
return pressedButton != nullptr || pressedSlider != nullptr || focusedTextField != nullptr;
}
void stopAllAnimations() {
@ -158,7 +186,12 @@ namespace ZL {
std::shared_ptr<UiTextView> findTextView(const std::string& name);
bool setText(const std::string& name, const std::string& newText);
std::shared_ptr<UiTextField> findTextField(const std::string& name);
bool setTextFieldCallback(const std::string& name, std::function<void(const std::string&, const std::string&)> cb);
std::string getTextFieldValue(const std::string& name);
bool pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
bool pushMenuFromSavedRoot(std::shared_ptr<UiNode> newRoot);
bool popMenu();
void clearMenuStack();
@ -169,7 +202,6 @@ namespace ZL {
bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function<void()> cb);
private:
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile);
void layoutNode(const std::shared_ptr<UiNode>& node);
void collectButtonsAndSliders(const std::shared_ptr<UiNode>& node);
@ -200,19 +232,23 @@ namespace ZL {
std::vector<std::shared_ptr<UiButton>> buttons;
std::vector<std::shared_ptr<UiSlider>> sliders;
std::vector<std::shared_ptr<UiTextView>> textViews;
std::vector<std::shared_ptr<UiTextField>> textFields;
std::map<std::shared_ptr<UiNode>, std::vector<ActiveAnim>> nodeActiveAnims;
std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks; // key: (nodeName, animName)
std::shared_ptr<UiButton> pressedButton;
std::shared_ptr<UiSlider> pressedSlider;
std::shared_ptr<UiTextField> focusedTextField;
struct MenuState {
std::shared_ptr<UiNode> root;
std::vector<std::shared_ptr<UiButton>> buttons;
std::vector<std::shared_ptr<UiSlider>> sliders;
std::vector<std::shared_ptr<UiTextField>> textFields;
std::shared_ptr<UiButton> pressedButton;
std::shared_ptr<UiSlider> pressedSlider;
std::shared_ptr<UiTextField> focusedTextField;
std::string path;
std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks;
};

View File

@ -1,20 +1,461 @@
#include "LocalClient.h"
#include <iostream>
#include <sstream>
#include <algorithm>
#include <cmath>
#define _USE_MATH_DEFINES
#include <math.h>
namespace ZL {
void LocalClient::Connect(const std::string& host, uint16_t port) {
generateBoxes();
initializeNPCs();
lastUpdateMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}
void LocalClient::generateBoxes() {
serverBoxes.clear();
std::random_device rd;
std::mt19937 gen(rd());
const float MIN_COORD = -100.0f;
const float MAX_COORD = 100.0f;
const float MIN_DISTANCE = 3.0f;
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
const int MAX_ATTEMPTS = 1000;
std::uniform_real_distribution<> posDistrib(MIN_COORD, MAX_COORD);
std::uniform_real_distribution<> angleDistrib(0.0, M_PI * 2.0);
for (int i = 0; i < 50; i++) {
bool accepted = false;
int attempts = 0;
while (!accepted && attempts < MAX_ATTEMPTS) {
LocalServerBox box;
box.position = Eigen::Vector3f(
(float)posDistrib(gen),
(float)posDistrib(gen),
(float)posDistrib(gen)
);
accepted = true;
for (const auto& existingBox : serverBoxes) {
Eigen::Vector3f diff = box.position - existingBox.position;
if (diff.squaredNorm() < MIN_DISTANCE_SQUARED) {
accepted = false;
break;
}
}
if (accepted) {
float randomAngle = (float)angleDistrib(gen);
Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized();
box.rotation = Eigen::AngleAxisf(randomAngle, axis).toRotationMatrix();
serverBoxes.push_back(box);
}
attempts++;
}
}
std::cout << "LocalClient: Generated " << serverBoxes.size() << " boxes\n";
}
Eigen::Vector3f LocalClient::generateRandomPosition() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> distrib(-500.0, 500.0);
return Eigen::Vector3f(
(float)distrib(gen),
(float)distrib(gen),
(float)distrib(gen) + 45000.0f
);
}
void LocalClient::initializeNPCs() {
npcs.clear();
for (int i = 0; i < 3; ++i) {
LocalNPC npc;
npc.id = 100 + i;
npc.currentState.id = npc.id;
npc.currentState.position = generateRandomPosition();
npc.currentState.rotation = Eigen::Matrix3f::Identity();
npc.currentState.velocity = 0.0f;
npc.currentState.selectedVelocity = 0;
npc.currentState.discreteMag = 0.0f;
npc.currentState.discreteAngle = -1;
npc.currentState.currentAngularVelocity = Eigen::Vector3f::Zero();
npc.targetPosition = generateRandomPosition();
npc.lastStateUpdateMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
npc.destroyed = false;
npc.stateHistory.add_state(npc.currentState);
npcs.push_back(npc);
}
}
void LocalClient::updateNPCs() {
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
for (auto& npc : npcs) {
if (npc.destroyed) continue;
uint64_t deltaMs = now_ms - npc.lastStateUpdateMs;
if (deltaMs == 0) {
npc.lastStateUpdateMs = now_ms;
continue;
}
npc.lastStateUpdateMs = now_ms;
Eigen::Vector3f toTarget = npc.targetPosition - npc.currentState.position;
float distance = toTarget.norm();
const float ARRIVAL_THRESHOLD = 100.0f;
if (distance < ARRIVAL_THRESHOLD) {
npc.targetPosition = generateRandomPosition();
toTarget = npc.targetPosition - npc.currentState.position;
distance = toTarget.norm();
}
Eigen::Vector3f forwardWorld = -npc.currentState.rotation.col(2);
forwardWorld.normalize();
Eigen::Vector3f desiredDir = (distance > 0.001f) ? toTarget.normalized() : Eigen::Vector3f::UnitZ();
float dot = forwardWorld.dot(desiredDir);
float angleErrorRad = std::acos(std::clamp(dot, -1.0f, 1.0f));
const float ALIGN_TOLERANCE = 0.15f;
const float HYSTERESIS_FACTOR = 1.35f;
const float SOFT_THRUST_ANGLE = ALIGN_TOLERANCE * HYSTERESIS_FACTOR;
if (angleErrorRad < ALIGN_TOLERANCE) {
npc.currentState.selectedVelocity = 1;
npc.currentState.discreteMag = 0.0f;
}
else if (angleErrorRad < SOFT_THRUST_ANGLE) {
npc.currentState.selectedVelocity = 1;
npc.currentState.discreteMag = std::min(0.50f, (angleErrorRad - ALIGN_TOLERANCE) * 10.0f);
}
else {
npc.currentState.selectedVelocity = 0;
Eigen::Vector3f localDesired = npc.currentState.rotation.transpose() * desiredDir;
float dx = localDesired.x();
float dy = localDesired.y();
float dz = localDesired.z();
float turnX = dy;
float turnY = -dx;
float turnLen = std::sqrt(turnX * turnX + turnY * turnY);
if (turnLen > 0.0001f) {
turnX /= turnLen;
turnY /= turnLen;
float rad = std::atan2(turnX, turnY);
int angleDeg = static_cast<int>(std::round(rad * 180.0f / M_PI));
if (angleDeg < 0) angleDeg += 360;
npc.currentState.discreteAngle = angleDeg;
npc.currentState.discreteMag = std::min(1.0f, angleErrorRad * 2.2f);
}
else if (angleErrorRad > 0.1f) {
npc.currentState.discreteAngle = 0;
npc.currentState.discreteMag = 1.0f;
}
else {
npc.currentState.discreteMag = 0.0f;
}
}
npc.currentState.simulate_physics(static_cast<size_t>(deltaMs));
npc.currentState.lastUpdateServerTime = std::chrono::system_clock::time_point(
std::chrono::milliseconds(now_ms));
npc.stateHistory.add_state(npc.currentState);
}
}
void LocalClient::Poll() {
updatePhysics();
updateNPCs();
checkCollisions();
}
void LocalClient::updatePhysics() {
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
if (lastUpdateMs == 0) {
lastUpdateMs = now_ms;
return;
}
uint64_t deltaMs = now_ms - lastUpdateMs;
float dt = deltaMs / 1000.0f;
lastUpdateMs = now_ms;
std::vector<int> indicesToRemove;
for (size_t i = 0; i < projectiles.size(); ++i) {
auto& pr = projectiles[i];
pr.pos += pr.vel * dt;
if (now_ms > pr.spawnMs + static_cast<uint64_t>(pr.lifeMs)) {
indicesToRemove.push_back(static_cast<int>(i));
}
}
if (!indicesToRemove.empty()) {
std::sort(indicesToRemove.rbegin(), indicesToRemove.rend());
for (int idx : indicesToRemove) {
if (idx >= 0 && idx < (int)projectiles.size()) {
projectiles.erase(projectiles.begin() + idx);
}
}
}
}
void LocalClient::checkCollisions() {
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
const float projectileHitRadius = 1.5f;
const float boxCollisionRadius = 2.0f;
const float shipCollisionRadius = 15.0f;
const float npcCollisionRadius = 5.0f;
std::vector<std::pair<size_t, size_t>> boxProjectileCollisions;
for (size_t bi = 0; bi < serverBoxes.size(); ++bi) {
if (serverBoxes[bi].destroyed) continue;
Eigen::Vector3f boxWorld = serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
for (size_t pi = 0; pi < projectiles.size(); ++pi) {
const auto& pr = projectiles[pi];
Eigen::Vector3f diff = pr.pos - boxWorld;
float thresh = boxCollisionRadius + projectileHitRadius;
if (diff.squaredNorm() <= thresh * thresh) {
boxProjectileCollisions.push_back({ bi, pi });
}
}
}
std::vector<int> projIndicesToRemove;
for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) {
if (!serverBoxes[boxIdx].destroyed) {
serverBoxes[boxIdx].destroyed = true;
Eigen::Vector3f boxWorld = serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
BoxDestroyedInfo destruction;
destruction.boxIndex = static_cast<int>(boxIdx);
destruction.serverTime = now_ms;
destruction.position = boxWorld;
destruction.destroyedBy = projectiles[projIdx].shooterId;
pendingBoxDestructions.push_back(destruction);
std::cout << "LocalClient: Box " << boxIdx << " destroyed by projectile from player "
<< projectiles[projIdx].shooterId << std::endl;
if (std::find(projIndicesToRemove.begin(), projIndicesToRemove.end(), (int)projIdx)
== projIndicesToRemove.end()) {
projIndicesToRemove.push_back(static_cast<int>(projIdx));
}
}
}
std::vector<std::pair<size_t, size_t>> npcProjectileCollisions;
for (size_t ni = 0; ni < npcs.size(); ++ni) {
if (npcs[ni].destroyed) continue;
for (size_t pi = 0; pi < projectiles.size(); ++pi) {
const auto& pr = projectiles[pi];
Eigen::Vector3f diff = pr.pos - npcs[ni].currentState.position;
float thresh = npcCollisionRadius + projectileHitRadius;
if (diff.squaredNorm() <= thresh * thresh) {
npcProjectileCollisions.push_back({ ni, pi });
}
}
}
for (const auto& [npcIdx, projIdx] : npcProjectileCollisions) {
if (!npcs[npcIdx].destroyed) {
npcs[npcIdx].destroyed = true;
DeathInfo death;
death.targetId = npcs[npcIdx].id;
death.serverTime = now_ms;
death.position = npcs[npcIdx].currentState.position;
death.killerId = projectiles[projIdx].shooterId;
pendingDeaths.push_back(death);
std::cout << "LocalClient: NPC " << npcs[npcIdx].id << " destroyed by projectile from player "
<< projectiles[projIdx].shooterId << " at position ("
<< npcs[npcIdx].currentState.position.x() << ", "
<< npcs[npcIdx].currentState.position.y() << ", "
<< npcs[npcIdx].currentState.position.z() << ")" << std::endl;
if (std::find(projIndicesToRemove.begin(), projIndicesToRemove.end(), (int)projIdx)
== projIndicesToRemove.end()) {
projIndicesToRemove.push_back(static_cast<int>(projIdx));
}
}
}
if (!projIndicesToRemove.empty()) {
std::sort(projIndicesToRemove.rbegin(), projIndicesToRemove.rend());
for (int idx : projIndicesToRemove) {
if (idx >= 0 && idx < (int)projectiles.size()) {
projectiles.erase(projectiles.begin() + idx);
}
}
}
if (hasLocalPlayerState) {
for (size_t bi = 0; bi < serverBoxes.size(); ++bi) {
if (serverBoxes[bi].destroyed) continue;
Eigen::Vector3f boxWorld = serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
Eigen::Vector3f diff = localPlayerState.position - boxWorld;
float thresh = shipCollisionRadius + boxCollisionRadius;
if (diff.squaredNorm() <= thresh * thresh) {
serverBoxes[bi].destroyed = true;
BoxDestroyedInfo destruction;
destruction.boxIndex = static_cast<int>(bi);
destruction.serverTime = now_ms;
destruction.position = boxWorld;
destruction.destroyedBy = GetClientId();
pendingBoxDestructions.push_back(destruction);
std::cout << "LocalClient: Box " << bi << " destroyed by ship collision with player "
<< GetClientId() << std::endl;
}
}
}
}
void LocalClient::Send(const std::string& message) {
auto parts = [](const std::string& s, char delimiter) {
std::vector<std::string> tokens;
std::string token;
std::istringstream tokenStream(s);
while (std::getline(tokenStream, token, delimiter)) {
tokens.push_back(token);
}
return tokens;
}(message, ':');
if (parts.empty()) return;
std::string type = parts[0];
if (type == "FIRE") {
if (parts.size() < 10) return;
uint64_t clientTime = std::stoull(parts[1]);
Eigen::Vector3f pos{
std::stof(parts[2]), std::stof(parts[3]), std::stof(parts[4])
};
Eigen::Quaternionf dir(
std::stof(parts[5]), std::stof(parts[6]), std::stof(parts[7]), std::stof(parts[8])
);
float velocity = std::stof(parts[9]);
int shotCount = 2;
if (parts.size() >= 11) {
try { shotCount = std::stoi(parts[10]); }
catch (...) { shotCount = 2; }
}
const std::vector<Eigen::Vector3f> localOffsets = {
Eigen::Vector3f(-1.5f, 0.9f - 6.f, 5.0f),
Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
};
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
for (int i = 0; i < std::min(shotCount, (int)localOffsets.size()); ++i) {
LocalProjectile pr;
pr.shooterId = GetClientId();
pr.spawnMs = now_ms;
Eigen::Vector3f shotPos = pos + dir.toRotationMatrix() * localOffsets[i];
pr.pos = shotPos;
Eigen::Vector3f localForward(0.0f, 0.0f, -1.0f);
Eigen::Vector3f worldForward = dir.toRotationMatrix() * localForward;
float len = worldForward.norm();
if (len > 1e-6f) worldForward /= len;
pr.vel = worldForward * velocity;
pr.lifeMs = 5000.0f;
projectiles.push_back(pr);
ProjectileInfo pinfo;
pinfo.shooterId = pr.shooterId;
pinfo.clientTime = clientTime;
pinfo.position = pr.pos;
pinfo.rotation = dir.toRotationMatrix();
pinfo.velocity = velocity;
pendingProjectiles.push_back(pinfo);
std::cout << "LocalClient: Created projectile at pos (" << shotPos.x() << ", "
<< shotPos.y() << ", " << shotPos.z() << ") vel (" << pr.vel.x() << ", "
<< pr.vel.y() << ", " << pr.vel.z() << ")" << std::endl;
}
}
}
std::vector<ProjectileInfo> LocalClient::getPendingProjectiles() {
return {};
auto result = pendingProjectiles;
pendingProjectiles.clear();
return result;
}
std::vector<DeathInfo> LocalClient::getPendingDeaths() {
auto result = pendingDeaths;
pendingDeaths.clear();
return result;
}
std::unordered_map<int, ClientStateInterval> LocalClient::getRemotePlayers() {
std::unordered_map<int, ClientStateInterval> result;
for (const auto& npc : npcs) {
if (!npc.destroyed) {
result[npc.id] = npc.stateHistory;
}
}
return result;
}
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> LocalClient::getServerBoxes() {
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> result;
for (const auto& box : serverBoxes) {
result.push_back({ box.position, box.rotation });
}
return result;
}
std::vector<BoxDestroyedInfo> LocalClient::getPendingBoxDestructions() {
auto result = pendingBoxDestructions;
pendingBoxDestructions.clear();
return result;
}
}

View File

@ -2,11 +2,60 @@
#include "NetworkInterface.h"
#include <queue>
#include <vector>
#include <Eigen/Dense>
#include <chrono>
#include <random>
namespace ZL {
struct LocalServerBox {
Eigen::Vector3f position;
Eigen::Matrix3f rotation;
float collisionRadius = 2.0f;
bool destroyed = false;
};
struct LocalProjectile {
int shooterId = -1;
uint64_t spawnMs = 0;
Eigen::Vector3f pos;
Eigen::Vector3f vel;
float lifeMs = 5000.0f;
};
struct LocalNPC {
int id = -1;
ClientState currentState;
ClientStateInterval stateHistory;
Eigen::Vector3f targetPosition;
uint64_t lastStateUpdateMs = 0;
bool destroyed = false;
};
class LocalClient : public INetworkClient {
private:
std::queue<std::string> messageQueue;
std::vector<LocalServerBox> serverBoxes;
std::vector<LocalProjectile> projectiles;
std::vector<ProjectileInfo> pendingProjectiles;
std::vector<DeathInfo> pendingDeaths;
std::vector<BoxDestroyedInfo> pendingBoxDestructions;
std::vector<int> pendingRespawns;
uint64_t lastUpdateMs = 0;
ClientState localPlayerState;
bool hasLocalPlayerState = false;
std::vector<LocalNPC> npcs;
void updatePhysics();
void checkCollisions();
void generateBoxes();
void initializeNPCs();
void updateNPCs();
Eigen::Vector3f generateRandomPosition();
public:
void Connect(const std::string& host, uint16_t port) override;
@ -18,25 +67,21 @@ namespace ZL {
int GetClientId() const override { return 1; }
std::vector<ProjectileInfo> getPendingProjectiles() override;
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override {
return std::unordered_map<int, ClientStateInterval>();
}
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override;
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override {
return {};
}
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override;
std::vector<DeathInfo> getPendingDeaths() override {
return {};
}
std::vector<DeathInfo> getPendingDeaths() override;
std::vector<int> getPendingRespawns() override {
return {};
}
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override
{
return {};
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
void setLocalPlayerState(const ClientState& state) {
localPlayerState = state;
hasLocalPlayerState = true;
}
};
}

View File

@ -1,4 +1,4 @@
#ifdef NETWORK
#ifdef NETWORK
#include "WebSocketClient.h"
#include <iostream>
@ -43,34 +43,22 @@ namespace ZL {
}
void WebSocketClient::processIncomingMessage(const std::string& msg) {
// Логика парсинга...
/*if (msg.rfind("ID:", 0) == 0) {
clientId = std::stoi(msg.substr(3));
}*/
// Безопасно кладем в очередь для главного потока
std::lock_guard<std::mutex> lock(queueMutex);
messageQueue.push(msg);
// Lock-free push: producer (I/O thread) pushes to its buffer
readProducerBuf_.load(std::memory_order_relaxed)->push_back(msg);
}
void WebSocketClient::Poll() {
std::lock_guard<std::mutex> lock(queueMutex);
while (!messageQueue.empty()) {
/*
auto nowTime = std::chrono::system_clock::now();
//Apply server delay:
nowTime -= std::chrono::milliseconds(CLIENT_DELAY);
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
nowTime.time_since_epoch()
).count();*/
std::string msg = messageQueue.front();
messageQueue.pop();
// Lock-free drain: swap consumer buffer with producer if ours is empty, then process all
MessageBuf* c = readConsumerBuf_.load(std::memory_order_acquire);
if (c->empty()) {
MessageBuf* p = readProducerBuf_.exchange(c, std::memory_order_acq_rel);
readConsumerBuf_.store(p, std::memory_order_release);
c = p;
}
for (std::string& msg : *c) {
HandlePollMessage(msg);
}
c->clear();
}
@ -79,54 +67,49 @@ namespace ZL {
if (!connected) return;
std::string finalMessage = SignMessage(message);
/*
#ifdef ENABLE_NETWORK_CHECKSUM
// Вычисляем хеш. Для примера используем std::hash,
// но в продакшене лучше взять быструю реализацию типа MurmurHash3.
size_t hashValue = std::hash<std::string>{}(message + NET_SECRET);
auto ss = std::make_shared<std::string>(std::move(finalMessage));
// Преобразуем хеш в hex-строку для передачи
std::stringstream ss_hash;
ss_hash << std::hex << hashValue;
// Lock-free push to write queue
writeProducerBuf_.load(std::memory_order_relaxed)->push_back(ss);
// Добавляем хеш в конец сообщения через разделитель
// Например: "UPD:12345:pos...#hash:a1b2c3d4"
finalMessage += "#hash:" + ss_hash.str();
#endif
*/
auto ss = std::make_shared<std::string>(finalMessage);
std::lock_guard<std::mutex> lock(writeMutex_);
writeQueue_.push(ss);
// Если сейчас ничего не записывается, инициируем первую запись
if (!isWriting_) {
// Start write chain if not already writing
bool expected = false;
if (isWriting_.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
doWrite();
}
}
void WebSocketClient::doWrite() {
// Эта функция всегда вызывается под мьютексом или из колбэка
if (writeQueue_.empty()) {
isWriting_ = false;
// Lock-free: take next message from consumer buffer; swap buffers if drained
WriteBuf* c = writeConsumerBuf_.load(std::memory_order_acquire);
if (currentWriteBuf_ == nullptr || currentWriteIndex_ >= currentWriteBuf_->size()) {
if (currentWriteBuf_) {
currentWriteBuf_->clear();
}
currentWriteBuf_ = c;
if (currentWriteBuf_->empty()) {
WriteBuf* p = writeProducerBuf_.exchange(currentWriteBuf_, std::memory_order_acq_rel);
writeConsumerBuf_.store(p, std::memory_order_release);
currentWriteBuf_ = p;
}
currentWriteIndex_ = 0;
}
if (currentWriteIndex_ >= currentWriteBuf_->size()) {
isWriting_.store(false, std::memory_order_release);
return;
}
isWriting_ = true;
auto message = writeQueue_.front();
std::shared_ptr<std::string> message = (*currentWriteBuf_)[currentWriteIndex_++];
// Захватываем self (shared_from_this), чтобы объект не удалился во время записи
ws_->async_write(
boost::asio::buffer(*message),
[this, message](boost::beast::error_code ec, std::size_t) {
if (ec) {
connected = false;
isWriting_.store(false, std::memory_order_release);
return;
}
std::lock_guard<std::mutex> lock(writeMutex_);
writeQueue_.pop(); // Удаляем отправленное сообщение
doWrite(); // Проверяем следующее
doWrite();
}
);
}

View File

@ -1,9 +1,11 @@
#pragma once
#pragma once
#ifdef NETWORK
#include "WebSocketClientBase.h"
#include <queue>
#include <vector>
#include <atomic>
#include <memory>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/asio/connect.hpp>
@ -11,21 +13,30 @@
namespace ZL {
// Lock-free SPSC double-buffer: producer pushes to one buffer, consumer swaps and drains the other.
// No mutexes; avoids contention under high message load.
class WebSocketClient : public WebSocketClientBase {
private:
// Переиспользуем io_context из TaskManager
boost::asio::io_context& ioc_;
// Объекты переехали в члены класса
std::unique_ptr<boost::beast::websocket::stream<boost::beast::tcp_stream>> ws_;
boost::beast::flat_buffer buffer_;
std::queue<std::string> messageQueue;
std::mutex queueMutex; // Защита для messageQueue
// Incoming messages: I/O thread pushes, main thread drains in Poll()
using MessageBuf = std::vector<std::string>;
MessageBuf readBuffer0_;
MessageBuf readBuffer1_;
std::atomic<MessageBuf*> readProducerBuf_;
std::atomic<MessageBuf*> readConsumerBuf_;
std::queue<std::shared_ptr<std::string>> writeQueue_;
bool isWriting_ = false;
std::mutex writeMutex_; // Отдельный мьютекс для очереди записи
// Outgoing messages: main thread pushes in Send(), doWrite()/completion drains
using WriteBuf = std::vector<std::shared_ptr<std::string>>;
WriteBuf writeBuffer0_;
WriteBuf writeBuffer1_;
std::atomic<WriteBuf*> writeProducerBuf_;
std::atomic<WriteBuf*> writeConsumerBuf_;
WriteBuf* currentWriteBuf_ = nullptr;
size_t currentWriteIndex_ = 0;
std::atomic<bool> isWriting_{ false };
bool connected = false;
@ -34,7 +45,13 @@ namespace ZL {
void processIncomingMessage(const std::string& msg);
public:
explicit WebSocketClient(boost::asio::io_context& ioc) : ioc_(ioc) {}
explicit WebSocketClient(boost::asio::io_context& ioc)
: ioc_(ioc)
, readProducerBuf_(&readBuffer0_)
, readConsumerBuf_(&readBuffer1_)
, writeProducerBuf_(&writeBuffer0_)
, writeConsumerBuf_(&writeBuffer1_)
{}
void Connect(const std::string& host, uint16_t port) override;

View File

@ -1,4 +1,4 @@
#ifdef NETWORK
#ifdef NETWORK
#include "WebSocketClientBase.h"
#include <iostream>
@ -70,10 +70,7 @@ namespace ZL {
}
}
}
{
std::lock_guard<std::mutex> bLock(boxesMutex);
serverBoxes_ = std::move(parsedBoxes);
}
return;
}
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
@ -81,14 +78,8 @@ namespace ZL {
if (parts.size() >= 2) {
try {
int respawnedPlayerId = std::stoi(parts[1]);
{
std::lock_guard<std::mutex> rLock(respawnMutex_);
pendingRespawns_.push_back(respawnedPlayerId);
}
{
std::lock_guard<std::mutex> pLock(playersMutex);
remotePlayers.erase(respawnedPlayerId);
}
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
}
catch (...) {}
@ -110,10 +101,7 @@ namespace ZL {
);
destruction.destroyedBy = std::stoi(parts[6]);
{
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
pendingBoxDestructions_.push_back(destruction);
}
std::cout << "Client: Received BOX_DESTROYED for box " << destruction.boxIndex
<< " destroyed by player " << destruction.destroyedBy << std::endl;
@ -146,7 +134,6 @@ namespace ZL {
pi.rotation = q.toRotationMatrix();
pi.velocity = std::stof(parts[10]);
std::lock_guard<std::mutex> pl(projMutex_);
pendingProjectiles_.push_back(pi);
}
catch (...) {}
@ -168,7 +155,6 @@ namespace ZL {
);
di.killerId = std::stoi(parts[6]);
std::lock_guard<std::mutex> dl(deathsMutex_);
pendingDeaths_.push_back(di);
}
catch (...) {}
@ -215,9 +201,7 @@ namespace ZL {
}
{
std::lock_guard<std::mutex> pLock(playersMutex);
auto& rp = remotePlayers[remoteId];
rp.add_state(remoteState);
}
}
@ -243,13 +227,10 @@ namespace ZL {
// Используем твой handle_full_sync, начиная со 2-го индекса (пропускаем ID в playerParts)
remoteState.handle_full_sync(playerParts, 1);
{
std::lock_guard<std::mutex> pLock(playersMutex);
remotePlayers[rId].add_state(remoteState);
}
}
}
}
std::string WebSocketClientBase::SignMessage(const std::string& msg) {
#ifdef ENABLE_NETWORK_CHECKSUM
@ -263,30 +244,26 @@ namespace ZL {
}
std::vector<ProjectileInfo> WebSocketClientBase::getPendingProjectiles() {
std::lock_guard<std::mutex> lock(projMutex_);
auto copy = pendingProjectiles_;
pendingProjectiles_.clear();
std::vector<ProjectileInfo> copy;
copy.swap(pendingProjectiles_);
return copy;
}
std::vector<DeathInfo> WebSocketClientBase::getPendingDeaths() {
std::lock_guard<std::mutex> lock(deathsMutex_);
auto copy = pendingDeaths_;
pendingDeaths_.clear();
std::vector<DeathInfo> copy;
copy.swap(pendingDeaths_);
return copy;
}
std::vector<int> WebSocketClientBase::getPendingRespawns() {
std::lock_guard<std::mutex> lock(respawnMutex_);
auto copy = pendingRespawns_;
pendingRespawns_.clear();
std::vector<int> copy;
copy.swap(pendingRespawns_);
return copy;
}
std::vector<BoxDestroyedInfo> WebSocketClientBase::getPendingBoxDestructions() {
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
auto copy = pendingBoxDestructions_;
pendingBoxDestructions_.clear();
std::vector<BoxDestroyedInfo> copy;
copy.swap(pendingBoxDestructions_);
return copy;
}
}

View File

@ -1,34 +1,25 @@
#pragma once
#pragma once
#include "NetworkInterface.h"
#include <queue>
#include <mutex>
#include <vector>
#include <unordered_map>
namespace ZL {
// All state in WebSocketClientBase is only accessed from the main thread:
// HandlePollMessage() runs from Poll(), and get*() are called from Game/Space on the main thread.
// No mutexes needed.
class WebSocketClientBase : public INetworkClient {
protected:
std::unordered_map<int, ClientStateInterval> remotePlayers;
std::mutex playersMutex;
// Серверные коробки
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_;
std::mutex boxesMutex;
std::vector<ProjectileInfo> pendingProjectiles_;
std::mutex projMutex_;
std::vector<DeathInfo> pendingDeaths_;
std::mutex deathsMutex_;
std::vector<int> pendingRespawns_;
std::mutex respawnMutex_;
std::vector<BoxDestroyedInfo> pendingBoxDestructions_;
std::mutex boxDestructionsMutex_;
int clientId = -1;
int64_t timeOffset = 0;
@ -42,12 +33,10 @@ namespace ZL {
std::string SignMessage(const std::string& msg);
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override {
std::lock_guard<std::mutex> lock(playersMutex);
return remotePlayers;
}
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override {
std::lock_guard<std::mutex> lock(boxesMutex);
return serverBoxes_;
}

View File

@ -8,6 +8,7 @@ namespace ZL {
void WebSocketClientEmscripten::Connect(const std::string& host, uint16_t port) {
// Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost
std::string url = "ws://" + host + ":" + std::to_string(port);
//std::string url = "wss://api.spacegame.fishrungames.com";
EmscriptenWebSocketCreateAttributes attr = {
url.c_str(),

View File

@ -6,13 +6,15 @@
#include "render/OpenGlExtensions.h"
#include <iostream>
#include <array>
#include <algorithm>
#include <cmath>
namespace ZL {
TextRenderer::~TextRenderer()
{
glyphs.clear();
atlasTexture.reset();
textMesh.positionVBO.reset();
}
@ -44,6 +46,11 @@ bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixe
return true;
}
void TextRenderer::ClearCache()
{
cache.clear();
}
bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename)
{
// 1. Загружаем сырые данные из ZIP
@ -81,38 +88,188 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
}
FT_Set_Pixel_Sizes(face, 0, pixelSize);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glyphs.clear();
// Проходим по стандартным ASCII символам
// glyphs.clear();
// Сначала собираем все глифы в память
struct GlyphBitmap {
int width = 0;
int height = 0;
std::vector<char> data; // R8 байты (ширина*height)
Eigen::Vector2f bearing;
unsigned int advance = 0;
};
std::vector<std::pair<char, GlyphBitmap>> glyphList;
glyphList.reserve(128 - 32);
int maxGlyphHeight = 0;
for (unsigned char c = 32; c < 128; ++c) {
FT_Load_Char(face, c, FT_LOAD_RENDER);
TextureDataStruct glyphData;
glyphData.width = face->glyph->bitmap.width;
glyphData.height = face->glyph->bitmap.rows;
glyphData.format = TextureDataStruct::R8;
glyphData.mipmap = TextureDataStruct::NONE;
// Копируем буфер FreeType в вектор данных
size_t dataSize = glyphData.width * glyphData.height;
glyphData.data.assign(face->glyph->bitmap.buffer, face->glyph->bitmap.buffer + dataSize);
// Теперь создание текстуры — это одна строка!
auto tex = std::make_shared<Texture>(glyphData);
GlyphInfo g;
g.texture = tex;
g.size = Eigen::Vector2f((float)face->glyph->bitmap.width, (float)face->glyph->bitmap.rows);
g.bearing = Eigen::Vector2f((float)face->glyph->bitmap_left, (float)face->glyph->bitmap_top);
// Advance во FreeType измеряется в 1/64 пикселя
g.advance = (unsigned int)face->glyph->advance.x;
glyphs.emplace((char)c, g);
if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
// пропускаем если не удалось загрузить, но сохраняем пустой запись с advance
GlyphBitmap gb;
gb.width = 0;
gb.height = 0;
gb.bearing = { 0.f, 0.f };
gb.advance = 0;
glyphList.emplace_back((char)c, std::move(gb));
continue;
}
GlyphBitmap gb;
gb.width = face->glyph->bitmap.width;
gb.height = face->glyph->bitmap.rows;
gb.bearing = Eigen::Vector2f((float)face->glyph->bitmap_left, (float)face->glyph->bitmap_top);
gb.advance = static_cast<unsigned int>(face->glyph->advance.x);
size_t dataSize = static_cast<size_t>(gb.width) * static_cast<size_t>(gb.height);
if (dataSize > 0) {
gb.data.assign(face->glyph->bitmap.buffer, face->glyph->bitmap.buffer + dataSize);
maxGlyphHeight = max(maxGlyphHeight, gb.height);
}
glyphList.emplace_back((char)c, std::move(gb));
}
// Пакуем глифы в один атлас (упрощённый алгоритм строковой укладки)
const int padding = 1;
const int maxAtlasWidth = 1024; // безопасное значение для большинства устройств
int curX = padding;
int curY = padding;
int rowHeight = 0;
int neededWidth = 0;
int neededHeight = 0;
// Предварительно вычислим требуемый размер, укладывая в maxAtlasWidth
for (auto& p : glyphList) {
const GlyphBitmap& gb = p.second;
int w = gb.width;
int h = gb.height;
if (curX + w + padding > maxAtlasWidth) {
// новая строка
neededWidth = max(neededWidth, curX);
curX = padding;
curY += rowHeight + padding;
rowHeight = 0;
}
curX += w + padding;
rowHeight = max(rowHeight, h);
}
neededWidth = max(neededWidth, curX);
neededHeight = curY + rowHeight + padding;
// Подгоняем к степеням двух (необязательно, но часто удобно)
auto nextPow2 = [](int v) {
int p = 1;
while (p < v) p <<= 1;
return p;
};
atlasWidth = static_cast<size_t>(nextPow2(max(16, neededWidth)));
atlasHeight = static_cast<size_t>(nextPow2(max(16, neededHeight)));
// Ограничение - если получилось слишком большое, попробуем без power-of-two
if (atlasWidth > 4096) atlasWidth = static_cast<size_t>(neededWidth);
if (atlasHeight > 4096) atlasHeight = static_cast<size_t>(neededHeight);
// Создаём буфер атласа, инициализируем нулями (прозрачность)
std::vector<char> atlasData(atlasWidth * atlasHeight, 0);
// Второй проход - размещаем глифы и заполняем atlasData
curX = padding;
curY = padding;
rowHeight = 0;
for (auto &p : glyphList) {
char ch = p.first;
GlyphBitmap &gb = p.second;
if (gb.width == 0 || gb.height == 0) {
// пустой глиф — записываем UV с нулевым размером и метрики
GlyphInfo gi;
gi.size = Eigen::Vector2f(0.f, 0.f);
gi.bearing = gb.bearing;
gi.advance = gb.advance;
gi.uv = Eigen::Vector2f(0.f, 0.f);
gi.uvSize = Eigen::Vector2f(0.f, 0.f);
glyphs.emplace(ch, gi);
continue;
}
if (curX + gb.width + padding > static_cast<int>(atlasWidth)) {
// новая строка
curX = padding;
curY += rowHeight + padding;
rowHeight = 0;
}
// Копируем строки глифа в atlasData
for (int row = 0; row < gb.height; ++row) {
// FreeType буфер, как мы ранее использовали, хранит строки подряд.
// Копируем gb.width байт из gb.data на позицию (curX, curY + row)
int destY = curY + row;
int destX = curX;
char* destPtr = atlasData.data() + destY * atlasWidth + destX;
const char* srcPtr = gb.data.data() + row * gb.width;
std::memcpy(destPtr, srcPtr, static_cast<size_t>(gb.width));
}
// Сохраняем информацию о глифе (в пикселях и UV)
GlyphInfo gi;
gi.size = Eigen::Vector2f((float)gb.width, (float)gb.height);
gi.bearing = gb.bearing;
gi.advance = gb.advance;
// UV: нормализуем относительно размера атласа. Здесь uv указывает на верх-лево.
gi.uv = Eigen::Vector2f((float)curX / (float)atlasWidth, (float)curY / (float)atlasHeight);
gi.uvSize = Eigen::Vector2f((float)gb.width / (float)atlasWidth, (float)gb.height / (float)atlasHeight);
glyphs.emplace(ch, gi);
curX += gb.width + padding;
rowHeight = max(rowHeight, gb.height);
}
// // Проходим по стандартным ASCII символам
// for (unsigned char c = 32; c < 128; ++c) {
//
// FT_Load_Char(face, c, FT_LOAD_RENDER);
// TextureDataStruct glyphData;
// glyphData.width = face->glyph->bitmap.width;
// glyphData.height = face->glyph->bitmap.rows;
// glyphData.format = TextureDataStruct::R8;
// glyphData.mipmap = TextureDataStruct::NONE;
// // Копируем буфер FreeType в вектор данных
// size_t dataSize = glyphData.width * glyphData.height;
// glyphData.data.assign(face->glyph->bitmap.buffer, face->glyph->bitmap.buffer + dataSize);
// // Теперь создание текстуры — это одна строка!
// auto tex = std::make_shared<Texture>(glyphData);
//GlyphInfo g;
// g.texture = tex;
// g.size = Eigen::Vector2f((float)face->glyph->bitmap.width, (float)face->glyph->bitmap.rows);
// g.bearing = Eigen::Vector2f((float)face->glyph->bitmap_left, (float)face->glyph->bitmap_top);
// // Advance во FreeType измеряется в 1/64 пикселя
// g.advance = (unsigned int)face->glyph->advance.x;
// glyphs.emplace((char)c, g);
// }
// Создаём Texture из atlasData (R8)
TextureDataStruct atlasTex;
atlasTex.width = atlasWidth;
atlasTex.height = atlasHeight;
atlasTex.format = TextureDataStruct::R8;
atlasTex.mipmap = TextureDataStruct::NONE;
atlasTex.data = std::move(atlasData);
atlasTexture = std::make_shared<Texture>(atlasTex);
// Очистка
FT_Done_Face(face);
FT_Done_FreeType(ft);
@ -126,30 +283,24 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color)
{
if (!r || text.empty() || !atlasTexture) return;
if (!r || text.empty()) return;
// формируем ключ кеша
std::string key = text + "|" + std::to_string(scale) + "|" + (centered ? "1" : "0");
auto itCache = cache.find(key);
// 1. Считаем ширину для центрирования
float totalW = 0.0f;
if (centered) {
for (char ch : text) {
auto it = glyphs.find(ch);
if (it == glyphs.end()) continue;
totalW += (it->second.advance >> 6) * scale;
}
x -= totalW * 0.5f;
}
// 2. Подготовка данных (аналог CreateRect2D, но для всей строки)
if (itCache == cache.end()) {
VertexDataStruct textData;
float penX = x;
float penY = y;
float penX = 0.0f;
float penY = 0.0f;
float totalW = 0.0f;
float maxH = 0.0f;
for (char ch : text) {
auto it = glyphs.find(ch);
if (it == glyphs.end()) continue;
const GlyphInfo& g = it->second;
auto git = glyphs.find(ch);
if (git == glyphs.end()) continue;
const GlyphInfo& g = git->second;
float xpos = penX + g.bearing.x() * scale;
float ypos = penY - (g.size.y() - g.bearing.y()) * scale;
@ -157,7 +308,6 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
float h = g.size.y() * scale;
// Добавляем 2 треугольника (6 вершин) для текущего символа
// Координаты Z ставим 0.0f, так как это 2D
textData.PositionData.push_back({ xpos, ypos + h, 0.0f });
textData.PositionData.push_back({ xpos, ypos, 0.0f });
textData.PositionData.push_back({ xpos + w, ypos, 0.0f });
@ -165,22 +315,49 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
textData.PositionData.push_back({ xpos + w, ypos, 0.0f });
textData.PositionData.push_back({ xpos + w, ypos + h, 0.0f });
// UV-координаты (здесь есть нюанс с атласом, ниже поясню)
textData.TexCoordData.push_back({ 0.0f, 0.0f });
textData.TexCoordData.push_back({ 0.0f, 1.0f });
textData.TexCoordData.push_back({ 1.0f, 1.0f });
textData.TexCoordData.push_back({ 0.0f, 0.0f });
textData.TexCoordData.push_back({ 1.0f, 1.0f });
textData.TexCoordData.push_back({ 1.0f, 0.0f });
// TexCoords — на основе UV позиции и размера в атласе (uv указывает на верх-лево)
float u0 = g.uv.x();
float v0 = g.uv.y();
float u1 = u0 + g.uvSize.x();
float v1 = v0 + g.uvSize.y();
textData.TexCoordData.push_back({ u0, v0 });
textData.TexCoordData.push_back({ u0, v1 });
textData.TexCoordData.push_back({ u1, v1 });
textData.TexCoordData.push_back({ u0, v0 });
textData.TexCoordData.push_back({ u1, v1 });
textData.TexCoordData.push_back({ u1, v0 });
penX += (g.advance >> 6) * scale;
totalW = penX;
maxH = max(maxH, h);
}
// Сохраняем в кеш
CachedText ct;
ct.width = totalW;
ct.height = maxH;
ct.mesh.AssignFrom(textData);
auto res = cache.emplace(key, std::move(ct));
itCache = res.first;
}
// Используем кешированный меш
CachedText& cached = itCache->second;
// Вычисляем смещение для проекции (оставляем Y как есть)
float tx = x;
if (centered) {
tx = x - cached.width * 0.5f;
}
float ty = y;
// 3. Обновляем VBO через наш стандартный механизм
// Примечание: для текста лучше использовать GL_DYNAMIC_DRAW,
// но RefreshVBO сейчас жестко зашит на GL_STATIC_DRAW.
// Для UI это обычно не критично, если строк не тысячи.
textMesh.AssignFrom(textData);
// textMesh.AssignFrom(textData);
// 4. Рендеринг
r->shaderManager.PushShader(shaderName);
@ -191,41 +368,44 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
Eigen::Matrix4f proj = Eigen::Matrix4f::Identity();
proj(0, 0) = 2.0f / W;
proj(1, 1) = 2.0f / H;
proj(0, 3) = -1.0f;
proj(1, 3) = -1.0f;
// Сдвигаем проекцию так, чтобы локальные координаты меша (pen-origin=0,0) оказались в (tx,ty)
proj(0, 3) = -1.0f + 2.0f * (tx) / W;
proj(1, 3) = -1.0f + 2.0f * (ty) / H;
r->RenderUniformMatrix4fv("uProjection", false, proj.data());
r->RenderUniform1i("uText", 0);
r->RenderUniform4fv("uColor", color.data());
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, atlasTexture->getTexID());
// ВНИМАНИЕ: Так как у тебя каждый символ — это отдельная текстура,
// нам всё равно придется делать glDrawArrays в цикле, ЛИБО использовать атлас.
// Если оставляем текущую систему с разными текстурами:
r->EnableVertexAttribArray("vPosition");
r->EnableVertexAttribArray("vTexCoord");
for (size_t i = 0; i < text.length(); ++i) {
auto it = glyphs.find(text[i]);
if (it == glyphs.end()) continue;
//for (size_t i = 0; i < text.length(); ++i) {
// auto it = glyphs.find(text[i]);
// if (it == glyphs.end()) continue;
glBindTexture(GL_TEXTURE_2D, it->second.texture->getTexID());
// glBindTexture(GL_TEXTURE_2D, it->second.texture->getTexID());
// Отрисовываем по 6 вершин за раз
// Нам нужно вручную биндить VBO, так как DrawVertexRenderStruct рисует всё сразу
glBindBuffer(GL_ARRAY_BUFFER, textMesh.positionVBO->getBuffer());
r->VertexAttribPointer3fv("vPosition", 0, (const char*)(i * 6 * sizeof(Vector3f)));
// // Отрисовываем по 6 вершин за раз
// // Нам нужно вручную биндить VBO, так как DrawVertexRenderStruct рисует всё сразу
// glBindBuffer(GL_ARRAY_BUFFER, textMesh.positionVBO->getBuffer());
// r->VertexAttribPointer3fv("vPosition", 0, (const char*)(i * 6 * sizeof(Vector3f)));
glBindBuffer(GL_ARRAY_BUFFER, textMesh.texCoordVBO->getBuffer());
r->VertexAttribPointer2fv("vTexCoord", 0, (const char*)(i * 6 * sizeof(Vector2f)));
// glBindBuffer(GL_ARRAY_BUFFER, textMesh.texCoordVBO->getBuffer());
// r->VertexAttribPointer2fv("vTexCoord", 0, (const char*)(i * 6 * sizeof(Vector2f)));
glDrawArrays(GL_TRIANGLES, 0, 6);
}
// glDrawArrays(GL_TRIANGLES, 0, 6);
//}
r->DrawVertexRenderStruct(cached.mesh);
r->DisableVertexAttribArray("vPosition");
r->DisableVertexAttribArray("vTexCoord");
r->shaderManager.PopShader();
// Сброс бинда текстуры не обязателен, но можно для чистоты
glBindTexture(GL_TEXTURE_2D, 0);
}
} // namespace ZL

View File

@ -12,7 +12,10 @@
namespace ZL {
struct GlyphInfo {
std::shared_ptr<Texture> texture; // Texture for glyph
// std::shared_ptr<Texture> texture; // Texture for glyph
Eigen::Vector2f uv; // u,v координата левого верхнего угла в атласе (0..1)
Eigen::Vector2f uvSize; // ширина/высота в UV (0..1)
Eigen::Vector2f size; // glyph size in pixels
Eigen::Vector2f bearing; // offset from baseline
unsigned int advance = 0; // advance.x in 1/64 pixels
@ -26,6 +29,9 @@ public:
bool init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename);
void drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color = { 1.f,1.f,1.f,1.f });
// Clear cached meshes (call on window resize / DPI change)
void ClearCache();
private:
bool loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename);
@ -37,9 +43,24 @@ private:
//unsigned int vao = 0;
//unsigned int vbo = 0;
// единый атлас для всех глифов
std::shared_ptr<Texture> atlasTexture;
size_t atlasWidth = 0;
size_t atlasHeight = 0;
VertexRenderStruct textMesh;
std::string shaderName = "text2d";
// caching for static texts
struct CachedText {
VertexRenderStruct mesh;
float width = 0.f; // in pixels, total advance
float height = 0.f; // optional, not currently used
};
// key: text + "|" + scale + "|" + centered
std::unordered_map<std::string, CachedText> cache;
};
} // namespace ZL