Compare commits

...

17 Commits

Author SHA1 Message Date
Vladislav Khorev
b59a10b7e6 Major refactoring for game menu 2026-02-22 19:50:13 +03:00
Vladislav Khorev
5b57696acf Refactoring major 2026-02-22 19:15:25 +03:00
Vladislav Khorev
1265d87bc5 Updated UI 2026-02-22 18:23:27 +03:00
Vladislav Khorev
46d7a2a25d Merge branch 'spark' 2026-02-22 17:02:40 +03:00
Vladislav Khorev
ed6e1bacc7 merge 2026-02-22 16:03:11 +03:00
Vladislav Khorev
9c39782729 Fixing bug with collisions 2026-02-22 15:53:04 +03:00
Vladislav Khorev
efad2dde3e Restore as it was before 2026-02-22 14:08:12 +03:00
Vladislav Khorev
84d8ee7eee Merge branch 'spark' 2026-02-22 13:30:33 +03:00
Vladislav Khorev
7693237aa5 Added websocket 2026-02-22 13:29:06 +03:00
528c94e921 Add enemy target brackets + off-screen arrows 2026-02-19 01:13:08 +06:00
b56fafa0e0 Add mesh caching for unchanging text labels 2026-02-12 13:23:16 +06:00
Vladislav Khorev
22ee99418d Working on web version 2026-02-11 21:48:25 +03:00
4eda57b4e4 single texture atlas for all font glyphs in TextRenderer 2026-02-12 00:08:52 +06:00
Vladislav Khorev
2ffd8124f2 Change the way fire happens 2026-02-11 20:02:15 +03:00
Vladislav Khorev
210c191d41 Added different hash func 2026-02-10 21:06:21 +03:00
Vladislav Khorev
251a59ddbe added hash 2026-02-10 11:42:23 +03:00
Vladislav Khorev
2c1c077611 Added time offset sync, added web version multiplayer 2026-02-09 22:56:48 +03:00
27 changed files with 2925 additions and 2076 deletions

View File

@ -65,6 +65,10 @@ set(SOURCES
../src/network/ClientState.cpp ../src/network/ClientState.cpp
../src/network/WebSocketClient.h ../src/network/WebSocketClient.h
../src/network/WebSocketClient.cpp ../src/network/WebSocketClient.cpp
../src/network/WebSocketClientBase.h
../src/network/WebSocketClientBase.cpp
../src/network/WebSocketClientEmscripten.h
../src/network/WebSocketClientEmscripten.cpp
../src/render/TextRenderer.h ../src/render/TextRenderer.h
../src/render/TextRenderer.cpp ../src/render/TextRenderer.cpp
) )
@ -90,7 +94,7 @@ add_subdirectory("../thirdparty/libzip-1.11.4" libzip-build)
# Линковка: # Линковка:
# 'zip' берется из add_subdirectory # 'zip' берется из add_subdirectory
# 'z' - это системный zlib Emscripten-а (флаг -sUSE_ZLIB=1 добавим ниже) # 'z' - это системный zlib Emscripten-а (флаг -sUSE_ZLIB=1 добавим ниже)
target_link_libraries(space-game001 PRIVATE zip z) target_link_libraries(space-game001 PRIVATE zip z websocket)
# Эмскриптен-флаги # Эмскриптен-флаги
set(EMSCRIPTEN_FLAGS set(EMSCRIPTEN_FLAGS
@ -102,6 +106,7 @@ set(EMSCRIPTEN_FLAGS
"-pthread" "-pthread"
"-sUSE_PTHREADS=1" "-sUSE_PTHREADS=1"
"-fexceptions" "-fexceptions"
"-DNETWORK"
) )
target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2") target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")

View File

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

View File

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

View File

@ -141,9 +141,9 @@
"type": "Slider", "type": "Slider",
"name": "velocitySlider", "name": "velocitySlider",
"x": 1140, "x": 1140,
"y": 100, "y": 300,
"width": 50, "width": 50,
"height": 500, "height": 300,
"value": 0.0, "value": 0.0,
"orientation": "vertical", "orientation": "vertical",
"textures": { "textures": {
@ -164,6 +164,19 @@
"pressed": "resources/shoot_pressed.png" "pressed": "resources/shoot_pressed.png"
} }
}, },
{
"type": "Button",
"name": "shootButton2",
"x": 1000,
"y": 100,
"width": 100,
"height": 100,
"textures": {
"normal": "resources/shoot_normal.png",
"hover": "resources/shoot_hover.png",
"pressed": "resources/shoot_pressed.png"
}
},
{ {
"type": "TextView", "type": "TextView",
"name": "velocityText", "name": "velocityText",

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

Binary file not shown.

View File

@ -129,21 +129,29 @@ public:
}); });
} }
private: bool IsMessageValid(const std::string& fullMessage) {
/* #ifdef ENABLE_NETWORK_CHECKSUM
void init() { size_t hashPos = fullMessage.find("#hash:");
sendBoxesToClient(); if (hashPos == std::string::npos) {
return false; // Хеша нет, хотя он ожидался
}
auto timer = std::make_shared<net::steady_timer>(ws_.get_executor()); std::string originalContent = fullMessage.substr(0, hashPos);
timer->expires_after(std::chrono::milliseconds(100)); std::string receivedHashStr = fullMessage.substr(hashPos + 6); // 6 — длина "#hash:"
timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) {
if (!ec) { // Вычисляем ожидаемый хеш от контента
self->send_message("ID:" + std::to_string(self->id_)); size_t expectedHash = fnv1a_hash(originalContent + NET_SECRET);
self->do_read();
} std::stringstream ss;
}); ss << std::hex << expectedHash;
return ss.str() == receivedHashStr;
#else
return true; // В режиме отладки пропускаем всё
#endif
} }
*/
private:
void sendBoxesToClient() { void sendBoxesToClient() {
std::lock_guard<std::mutex> lock(g_boxes_mutex); std::lock_guard<std::mutex> lock(g_boxes_mutex);
@ -167,10 +175,6 @@ private:
public: public:
/*
explicit Session(tcp::socket&& socket, int id)
: ws_(std::move(socket)), id_(id) {
}*/
void init() void init()
{ {
@ -185,31 +189,6 @@ public:
} }
}); });
} }
/*
void run() {
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
g_sessions.push_back(shared_from_this());
}
ws_.async_accept([self = shared_from_this()](beast::error_code ec) {
if (ec) return;
std::cout << "Client " << self->id_ << " connected\n";
self->init();
// self->send_message("ID:" + std::to_string(self->id_));
// self->do_read();
});
}*/
/*void send_message(std::string msg) {
auto ss = std::make_shared<std::string>(std::move(msg));
ws_.async_write(net::buffer(*ss), [ss](beast::error_code, std::size_t) {});
}
int get_id() const {
return id_;
}*/
ClientState get_latest_state(std::chrono::system_clock::time_point now) { ClientState get_latest_state(std::chrono::system_clock::time_point now) {
if (timedClientStates.timedStates.empty()) { if (timedClientStates.timedStates.empty()) {
@ -226,31 +205,7 @@ public:
return latest; return latest;
} }
/*
void send_message(std::string msg) {
auto ss = std::make_shared<std::string>(std::move(msg));
if (is_writing_) {
ws_.async_write(net::buffer(*ss),
[self = shared_from_this(), ss](beast::error_code ec, std::size_t) {
if (ec) {
std::cerr << "Write error: " << ec.message() << std::endl;
}
});
}
else {
is_writing_ = true;
ws_.async_write(net::buffer(*ss),
[self = shared_from_this(), ss](beast::error_code ec, std::size_t) {
self->is_writing_ = false;
if (ec) {
std::cerr << "Write error: " << ec.message() << std::endl;
}
});
}
}*/
void doWrite() { void doWrite() {
std::lock_guard<std::mutex> lock(writeMutex_); std::lock_guard<std::mutex> lock(writeMutex_);
if (is_writing_ || writeQueue_.empty()) { if (is_writing_ || writeQueue_.empty()) {
@ -297,7 +252,15 @@ private:
} }
void process_message(const std::string& msg) { void process_message(const std::string& msg) {
auto parts = split(msg, ':'); if (!IsMessageValid(msg)) {
// Логируем попытку подмены и просто выходим из обработки
std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl;
return;
}
std::string cleanMessage = msg.substr(0, msg.find("#hash:"));
std::cout << "Received from player " << id_ << ": " << cleanMessage << std::endl;
auto parts = split(cleanMessage, ':');
if (parts.empty()) return; if (parts.empty()) return;
@ -323,7 +286,7 @@ private:
receivedState.handle_full_sync(parts, 2); receivedState.handle_full_sync(parts, 2);
timedClientStates.add_state(receivedState); timedClientStates.add_state(receivedState);
retranslateMessage(msg); retranslateMessage(cleanMessage);
} }
else if (parts[0] == "RESPAWN") { else if (parts[0] == "RESPAWN") {
{ {
@ -377,8 +340,8 @@ private:
{ {
const std::vector<Eigen::Vector3f> localOffsets = { const std::vector<Eigen::Vector3f> localOffsets = {
Eigen::Vector3f(-1.5f, 0.9f, 5.0f), Eigen::Vector3f(-1.5f, 0.9f - 6.f, 5.0f),
Eigen::Vector3f(1.5f, 0.9f, 5.0f) Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
}; };
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>( uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
@ -569,7 +532,8 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
{ {
std::lock_guard<std::mutex> bm(g_boxes_mutex); std::lock_guard<std::mutex> bm(g_boxes_mutex);
const float projectileHitRadius = 1.5f; //const float projectileHitRadius = 1.5f;
const float projectileHitRadius = 5.0f;
const float boxCollisionRadius = 2.0f; const float boxCollisionRadius = 2.0f;
std::vector<std::pair<size_t, size_t>> boxProjectileCollisions; std::vector<std::pair<size_t, size_t>> boxProjectileCollisions;
@ -577,11 +541,12 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) { for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
if (g_serverBoxes[bi].destroyed) continue; if (g_serverBoxes[bi].destroyed) continue;
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 6.0f, 45000.0f); 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) { for (size_t pi = 0; pi < g_projectiles.size(); ++pi) {
const auto& pr = g_projectiles[pi]; const auto& pr = g_projectiles[pi];
Eigen::Vector3f diff = pr.pos - boxWorld; Eigen::Vector3f diff = pr.pos - boxWorld;
//std::cout << "diff norm is " << diff.norm() << std::endl;
float thresh = boxCollisionRadius + projectileHitRadius; float thresh = boxCollisionRadius + projectileHitRadius;
if (diff.squaredNorm() <= thresh * thresh) { if (diff.squaredNorm() <= thresh * thresh) {
@ -751,14 +716,15 @@ int main() {
{ {
std::lock_guard<std::mutex> lock(g_boxes_mutex); std::lock_guard<std::mutex> lock(g_boxes_mutex);
g_serverBoxes = generateServerBoxes(50); g_serverBoxes = generateServerBoxes(50);
//g_serverBoxes = generateServerBoxes(1);
std::cout << "Generated " << g_serverBoxes.size() << " boxes on server\n"; std::cout << "Generated " << g_serverBoxes.size() << " boxes on server\n";
} }
net::io_context ioc; net::io_context ioc;
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} }; tcp::acceptor acceptor{ ioc, {tcp::v4(), 8081} };
int next_id = 1000; int next_id = 1000;
std::cout << "Server started on port 8080...\n"; std::cout << "Server started on port 8081...\n";
auto do_accept = [&](auto& self_fn) -> void { auto do_accept = [&](auto& self_fn) -> void {
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) { acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {

File diff suppressed because it is too large Load Diff

View File

@ -14,19 +14,13 @@
#include <string> #include <string>
#include <memory> #include <memory>
#include <render/TextRenderer.h> #include <render/TextRenderer.h>
#include "MenuManager.h"
#include "Space.h"
#include <unordered_set> #include <unordered_set>
namespace ZL { namespace ZL {
struct BoxCoords
{
Vector3f pos;
Matrix3f m;
};
class Game { class Game {
public: public:
Game(); Game();
@ -41,99 +35,30 @@ namespace ZL {
Renderer renderer; Renderer renderer;
TaskManager taskManager; TaskManager taskManager;
MainThreadHandler mainThreadHandler; MainThreadHandler mainThreadHandler;
std::unique_ptr<INetworkClient> networkClient; std::unique_ptr<INetworkClient> networkClient;
private: private:
int64_t getSyncTimeMs();
void processTickCount(); void processTickCount();
void drawScene(); void drawScene();
void drawCubemap(float skyPercent);
void drawShip();
void drawBoxes();
void drawBoxesLabels();
void drawUI(); void drawUI();
void drawRemoteShips(); void drawUnderMainMenu();
void drawRemoteShipsLabels();
void fireProjectiles();
bool worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth) const;
void handleDown(int mx, int my); void handleDown(int mx, int my);
void handleUp(int mx, int my); void handleUp(int mx, int my);
void handleMotion(int mx, int my); void handleMotion(int mx, int my);
SDL_Window* window; SDL_Window* window;
SDL_GLContext glContext; SDL_GLContext glContext;
size_t newTickCount; int64_t newTickCount;
size_t lastTickCount; 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_TIMER_INTERVAL = 10;
static const size_t CONST_MAX_TIME_INTERVAL = 1000; static const size_t CONST_MAX_TIME_INTERVAL = 1000;
std::shared_ptr<Texture> sparkTexture; MenuManager menuManager;
std::shared_ptr<Texture> spaceshipTexture; Space space;
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;
uint64_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 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;
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

@ -182,60 +182,7 @@ namespace ZL {
} }
} }
void UiManager::loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) { std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
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);
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();
}
}
std::shared_ptr<UiNode> UiManager::parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
auto node = std::make_shared<UiNode>(); auto node = std::make_shared<UiNode>();
if (j.contains("type") && j["type"].is_string()) node->type = j["type"].get<std::string>(); 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>(); if (j.contains("name") && j["name"].is_string()) node->name = j["name"].get<std::string>();
@ -416,6 +363,76 @@ namespace ZL {
return node; 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) { void UiManager::layoutNode(const std::shared_ptr<UiNode>& node) {
for (auto& child : node->children) { for (auto& child : node->children) {
child->rect.x += node->rect.x; child->rect.x += node->rect.x;
@ -553,7 +570,8 @@ namespace ZL {
return tf->text; return tf->text;
} }
bool UiManager::pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) { bool UiManager::pushMenuFromSavedRoot(std::shared_ptr<UiNode> newRoot)
{
MenuState prev; MenuState prev;
prev.root = root; prev.root = root;
prev.buttons = buttons; prev.buttons = buttons;
@ -579,17 +597,22 @@ namespace ZL {
} }
} }
loadFromFile(path, renderer, zipFile); replaceRoot(newRoot);
menuStack.push_back(std::move(prev)); menuStack.push_back(std::move(prev));
return true; return true;
} }
catch (const std::exception& e) { 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; animCallbacks = prev.animCallbacks;
return false; 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() { bool UiManager::popMenu() {
if (menuStack.empty()) { if (menuStack.empty()) {
std::cerr << "UiManager: popMenu called but menu stack is empty" << std::endl; std::cerr << "UiManager: popMenu called but menu stack is empty" << std::endl;

View File

@ -136,10 +136,15 @@ namespace ZL {
std::map<std::string, AnimSequence> animations; 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 { class UiManager {
public: public:
UiManager() = default; UiManager() = default;
void replaceRoot(std::shared_ptr<UiNode> newRoot);
void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = ""); void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
void draw(Renderer& renderer); void draw(Renderer& renderer);
@ -186,6 +191,7 @@ namespace ZL {
std::string getTextFieldValue(const std::string& name); std::string getTextFieldValue(const std::string& name);
bool pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = ""); bool pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
bool pushMenuFromSavedRoot(std::shared_ptr<UiNode> newRoot);
bool popMenu(); bool popMenu();
void clearMenuStack(); void clearMenuStack();
@ -196,7 +202,6 @@ namespace ZL {
bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function<void()> cb); bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function<void()> cb);
private: private:
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile);
void layoutNode(const std::shared_ptr<UiNode>& node); void layoutNode(const std::shared_ptr<UiNode>& node);
void collectButtonsAndSliders(const std::shared_ptr<UiNode>& node); void collectButtonsAndSliders(const std::shared_ptr<UiNode>& node);

View File

@ -1,5 +1,13 @@
#include "ClientState.h" #include "ClientState.h"
uint32_t fnv1a_hash(const std::string& data) {
uint32_t hash = 0x811c9dc5;
for (unsigned char c : data) {
hash ^= c;
hash *= 0x01000193;
}
return hash;
}
void ClientState::simulate_physics(size_t delta) { void ClientState::simulate_physics(size_t delta) {
if (discreteMag > 0.01f) if (discreteMag > 0.01f)

View File

@ -9,6 +9,9 @@
using std::min; using std::min;
using std::max; using std::max;
constexpr auto NET_SECRET = "880b3713b9ff3e7a94b2712d54679e1f";
#define ENABLE_NETWORK_CHECKSUM
constexpr float ANGULAR_ACCEL = 0.005f * 1000.0f; constexpr float ANGULAR_ACCEL = 0.005f * 1000.0f;
constexpr float SHIP_ACCEL = 1.0f * 1000.0f; constexpr float SHIP_ACCEL = 1.0f * 1000.0f;
constexpr float ROTATION_SENSITIVITY = 0.002f; constexpr float ROTATION_SENSITIVITY = 0.002f;
@ -20,9 +23,11 @@ constexpr float PLANET_MAX_ANGULAR_VELOCITY = 10.f;
constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f; constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f;
constexpr long long SERVER_DELAY = 0; //ms constexpr long long SERVER_DELAY = 0; //ms
constexpr long long CLIENT_DELAY = 1000; //ms constexpr long long CLIENT_DELAY = 500; //ms
constexpr long long CUTOFF_TIME = 5000; //ms constexpr long long CUTOFF_TIME = 5000; //ms
uint32_t fnv1a_hash(const std::string& data);
struct ClientState { struct ClientState {
int id = 0; int id = 0;
Eigen::Vector3f position = { 0, 0, 45000.0f }; Eigen::Vector3f position = { 0, 0, 45000.0f };

View File

@ -239,7 +239,7 @@ namespace ZL {
for (size_t bi = 0; bi < serverBoxes.size(); ++bi) { for (size_t bi = 0; bi < serverBoxes.size(); ++bi) {
if (serverBoxes[bi].destroyed) continue; if (serverBoxes[bi].destroyed) continue;
Eigen::Vector3f boxWorld = serverBoxes[bi].position + Eigen::Vector3f(0.0f, 6.0f, 45000.0f); Eigen::Vector3f boxWorld = serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
for (size_t pi = 0; pi < projectiles.size(); ++pi) { for (size_t pi = 0; pi < projectiles.size(); ++pi) {
const auto& pr = projectiles[pi]; const auto& pr = projectiles[pi];
@ -387,8 +387,8 @@ namespace ZL {
} }
const std::vector<Eigen::Vector3f> localOffsets = { const std::vector<Eigen::Vector3f> localOffsets = {
Eigen::Vector3f(-1.5f, 0.9f, 5.0f), Eigen::Vector3f(-1.5f, 0.9f - 6.f, 5.0f),
Eigen::Vector3f(1.5f, 0.9f, 5.0f) Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
}; };
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>( uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(

View File

@ -47,5 +47,7 @@ namespace ZL {
virtual std::vector<int> getPendingRespawns() = 0; virtual std::vector<int> getPendingRespawns() = 0;
virtual int GetClientId() const { return -1; } virtual int GetClientId() const { return -1; }
virtual std::vector<BoxDestroyedInfo> getPendingBoxDestructions() = 0; virtual std::vector<BoxDestroyedInfo> getPendingBoxDestructions() = 0;
virtual int64_t getTimeOffset() const { return 0; }
}; };
} }

View File

@ -4,17 +4,6 @@
#include <iostream> #include <iostream>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
// Вспомогательный split
std::vector<std::string> split(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;
}
namespace ZL { namespace ZL {
void WebSocketClient::Connect(const std::string& host, uint16_t port) { void WebSocketClient::Connect(const std::string& host, uint16_t port) {
@ -55,48 +44,20 @@ namespace ZL {
void WebSocketClient::processIncomingMessage(const std::string& msg) { void WebSocketClient::processIncomingMessage(const std::string& msg) {
// Логика парсинга... // Логика парсинга...
if (msg.rfind("ID:", 0) == 0) { /*if (msg.rfind("ID:", 0) == 0) {
clientId = std::stoi(msg.substr(3)); clientId = std::stoi(msg.substr(3));
} }*/
// Безопасно кладем в очередь для главного потока // Безопасно кладем в очередь для главного потока
std::lock_guard<std::mutex> lock(queueMutex); std::lock_guard<std::mutex> lock(queueMutex);
messageQueue.push(msg); messageQueue.push(msg);
} }
std::vector<ProjectileInfo> WebSocketClient::getPendingProjectiles() {
std::lock_guard<std::mutex> lock(projMutex_);
auto copy = pendingProjectiles_;
pendingProjectiles_.clear();
return copy;
}
std::vector<DeathInfo> WebSocketClient::getPendingDeaths() {
std::lock_guard<std::mutex> lock(deathsMutex_);
auto copy = pendingDeaths_;
pendingDeaths_.clear();
return copy;
}
std::vector<int> WebSocketClient::getPendingRespawns() {
std::lock_guard<std::mutex> lock(respawnMutex_);
auto copy = pendingRespawns_;
pendingRespawns_.clear();
return copy;
}
std::vector<BoxDestroyedInfo> WebSocketClient::getPendingBoxDestructions() {
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
auto copy = pendingBoxDestructions_;
pendingBoxDestructions_.clear();
return copy;
}
void WebSocketClient::Poll() { void WebSocketClient::Poll() {
std::lock_guard<std::mutex> lock(queueMutex); std::lock_guard<std::mutex> lock(queueMutex);
while (!messageQueue.empty()) { while (!messageQueue.empty()) {
/*
auto nowTime = std::chrono::system_clock::now(); auto nowTime = std::chrono::system_clock::now();
//Apply server delay: //Apply server delay:
@ -104,229 +65,36 @@ namespace ZL {
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>( auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
nowTime.time_since_epoch() nowTime.time_since_epoch()
).count(); ).count();*/
std::string msg = messageQueue.front(); std::string msg = messageQueue.front();
messageQueue.pop(); messageQueue.pop();
// Обработка списка коробок от сервера HandlePollMessage(msg);
if (msg.rfind("BOXES:", 0) == 0) {
std::string payload = msg.substr(6); // после "BOXES:"
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> parsedBoxes;
if (!payload.empty()) {
auto items = split(payload, '|');
for (auto& item : items) {
if (item.empty()) continue;
auto parts = split(item, ':');
if (parts.size() < 7) continue;
try {
float px = std::stof(parts[0]);
float py = std::stof(parts[1]);
float pz = std::stof(parts[2]);
Eigen::Quaternionf q(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5]),
std::stof(parts[6])
);
Eigen::Matrix3f rot = q.toRotationMatrix();
parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot);
}
catch (...) {
// пропускаем некорректную запись
continue;
}
}
}
{
std::lock_guard<std::mutex> bLock(boxesMutex);
serverBoxes_ = std::move(parsedBoxes);
}
continue;
}
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
auto parts = split(msg, ':');
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 (...) {}
}
continue;
}
if (msg.rfind("BOX_DESTROYED:", 0) == 0) {
auto parts = split(msg, ':');
if (parts.size() >= 7) {
try {
BoxDestroyedInfo destruction;
destruction.boxIndex = std::stoi(parts[1]);
destruction.serverTime = std::stoull(parts[2]);
destruction.position = Eigen::Vector3f(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5])
);
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;
}
catch (const std::exception& e) {
std::cerr << "Client: Error parsing BOX_DESTROYED: " << e.what() << std::endl;
}
}
continue;
}
if (msg.rfind("PROJECTILE:", 0) == 0) {
auto parts = split(msg, ':');
if (parts.size() >= 10) {
try {
ProjectileInfo pi;
pi.shooterId = std::stoi(parts[1]);
pi.clientTime = std::stoull(parts[2]);
pi.position = Eigen::Vector3f(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5])
);
Eigen::Quaternionf q(
std::stof(parts[6]),
std::stof(parts[7]),
std::stof(parts[8]),
std::stof(parts[9])
);
pi.rotation = q.toRotationMatrix();
pi.velocity = std::stof(parts[10]);
std::lock_guard<std::mutex> pl(projMutex_);
pendingProjectiles_.push_back(pi);
}
catch (...) {}
}
continue;
}
if (msg.rfind("DEAD:", 0) == 0) {
auto parts = split(msg, ':');
if (parts.size() >= 7) {
try {
DeathInfo di;
di.serverTime = std::stoull(parts[1]);
di.targetId = std::stoi(parts[2]);
di.position = Eigen::Vector3f(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5])
);
di.killerId = std::stoi(parts[6]);
std::lock_guard<std::mutex> dl(deathsMutex_);
pendingDeaths_.push_back(di);
}
catch (...) {}
}
continue;
}
if (msg.rfind("EVENT:", 0) == 0) {
auto parts = split(msg, ':');
if (parts.size() < 5) continue; // EVENT:ID:TYPE:TIME:DATA...
int remoteId = std::stoi(parts[1]);
std::string subType = parts[2];
uint64_t sentTime = std::stoull(parts[3]);
ClientState remoteState;
remoteState.id = remoteId;
std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast<std::chrono::system_clock::time_point::duration>(std::chrono::milliseconds(sentTime)) };
remoteState.lastUpdateServerTime = uptime_timepoint;
if (subType == "UPD") {
int startFrom = 4;
remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) };
Eigen::Quaternionf q(
std::stof(parts[startFrom + 3]),
std::stof(parts[startFrom + 4]),
std::stof(parts[startFrom + 5]),
std::stof(parts[startFrom + 6]));
remoteState.rotation = q.toRotationMatrix();
remoteState.currentAngularVelocity = Eigen::Vector3f{
std::stof(parts[startFrom + 7]),
std::stof(parts[startFrom + 8]),
std::stof(parts[startFrom + 9]) };
remoteState.velocity = std::stof(parts[startFrom + 10]);
remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]);
remoteState.discreteMag = std::stof(parts[startFrom + 12]);
remoteState.discreteAngle = std::stoi(parts[startFrom + 13]);
}
else
{
throw std::runtime_error("Unknown EVENT subtype: " + subType);
}
{
std::lock_guard<std::mutex> pLock(playersMutex);
auto& rp = remotePlayers[remoteId];
rp.add_state(remoteState);
}
}
if (msg.rfind("SNAPSHOT:", 0) == 0) {
auto mainParts = split(msg.substr(9), '|'); // Отсекаем "SNAPSHOT:" и делим по игрокам
if (mainParts.empty()) continue;
uint64_t serverTimestamp = std::stoull(mainParts[0]);
std::chrono::system_clock::time_point serverTime{ std::chrono::milliseconds(serverTimestamp) };
for (size_t i = 1; i < mainParts.size(); ++i) {
auto playerParts = split(mainParts[i], ':');
if (playerParts.size() < 15) continue; // ID + 14 полей ClientState
int rId = std::stoi(playerParts[0]);
if (rId == clientId) continue; // Свое состояние игрок знает лучше всех (Client Side Prediction)
ClientState remoteState;
remoteState.id = rId;
remoteState.lastUpdateServerTime = serverTime;
// Используем твой handle_full_sync, начиная со 2-го индекса (пропускаем ID в playerParts)
remoteState.handle_full_sync(playerParts, 1);
{
std::lock_guard<std::mutex> pLock(playersMutex);
remotePlayers[rId].add_state(remoteState);
}
}
continue;
}
} }
} }
void WebSocketClient::Send(const std::string& message) { void WebSocketClient::Send(const std::string& message) {
if (!connected) return; if (!connected) return;
auto ss = std::make_shared<std::string>(message); std::string finalMessage = SignMessage(message);
/*
#ifdef ENABLE_NETWORK_CHECKSUM
// Вычисляем хеш. Для примера используем std::hash,
// но в продакшене лучше взять быструю реализацию типа MurmurHash3.
size_t hashValue = std::hash<std::string>{}(message + NET_SECRET);
// Преобразуем хеш в hex-строку для передачи
std::stringstream ss_hash;
ss_hash << std::hex << hashValue;
// Добавляем хеш в конец сообщения через разделитель
// Например: "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_); std::lock_guard<std::mutex> lock(writeMutex_);
writeQueue_.push(ss); writeQueue_.push(ss);
@ -364,4 +132,4 @@ namespace ZL {
} }
} }
#endif #endif

View File

@ -2,7 +2,7 @@
#ifdef NETWORK #ifdef NETWORK
#include "NetworkInterface.h" #include "WebSocketClientBase.h"
#include <queue> #include <queue>
#include <boost/beast/core.hpp> #include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp> #include <boost/beast/websocket.hpp>
@ -11,7 +11,7 @@
namespace ZL { namespace ZL {
class WebSocketClient : public INetworkClient { class WebSocketClient : public WebSocketClientBase {
private: private:
// Переиспользуем io_context из TaskManager // Переиспользуем io_context из TaskManager
boost::asio::io_context& ioc_; boost::asio::io_context& ioc_;
@ -28,26 +28,7 @@ namespace ZL {
std::mutex writeMutex_; // Отдельный мьютекс для очереди записи std::mutex writeMutex_; // Отдельный мьютекс для очереди записи
bool connected = false; bool connected = false;
int clientId = -1;
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_;
void startAsyncRead(); void startAsyncRead();
void processIncomingMessage(const std::string& msg); void processIncomingMessage(const std::string& msg);
@ -63,22 +44,6 @@ namespace ZL {
void doWrite(); void doWrite();
bool IsConnected() const override { return connected; } bool IsConnected() const override { return connected; }
int GetClientId() const override { return clientId; }
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_;
}
std::vector<ProjectileInfo> getPendingProjectiles() override;
std::vector<DeathInfo> getPendingDeaths() override;
std::vector<int> getPendingRespawns() override;
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
}; };
} }
#endif #endif

View File

@ -0,0 +1,294 @@
#ifdef NETWORK
#include "WebSocketClientBase.h"
#include <iostream>
#include <SDL2/SDL.h>
// Вспомогательный split
std::vector<std::string> split(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;
}
namespace ZL {
void WebSocketClientBase::HandlePollMessage(const std::string& msg) {
auto parts = split(msg, ':');
if (parts.empty()) return;
if (parts[0] == "ID") {
std::cout << "ID Message Received:" << msg << std::endl;
clientId = std::stoi(parts[1]);
if (parts.size() >= 3) {
std::cout << "ID Message Received step 2" << std::endl;
uint64_t serverTime = std::stoull(parts[2]);
uint64_t localTime = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
std::cout << "ID Message Received localTime = " << localTime << std::endl;
std::cout << "ID Message Received serverTime = " << serverTime << std::endl;
// Вычисляем смещение
timeOffset = static_cast<int64_t>(serverTime) - static_cast<int64_t>(localTime);
std::cout << "Time synchronized. Offset: " << timeOffset << " ms" << std::endl;
}
return;
}
// Обработка списка коробок от сервера
if (msg.rfind("BOXES:", 0) == 0) {
std::string payload = msg.substr(6); // после "BOXES:"
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> parsedBoxes;
if (!payload.empty()) {
auto items = split(payload, '|');
for (auto& item : items) {
if (item.empty()) return;
auto parts = split(item, ':');
if (parts.size() < 7) return;
try {
float px = std::stof(parts[0]);
float py = std::stof(parts[1]);
float pz = std::stof(parts[2]);
Eigen::Quaternionf q(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5]),
std::stof(parts[6])
);
Eigen::Matrix3f rot = q.toRotationMatrix();
parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot);
}
catch (...) {
// пропускаем некорректную запись
return;
}
}
}
{
std::lock_guard<std::mutex> bLock(boxesMutex);
serverBoxes_ = std::move(parsedBoxes);
}
return;
}
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
//auto parts = split(msg, ':');
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 (...) {}
}
return;
}
if (msg.rfind("BOX_DESTROYED:", 0) == 0) {
//auto parts = split(msg, ':');
if (parts.size() >= 7) {
try {
BoxDestroyedInfo destruction;
destruction.boxIndex = std::stoi(parts[1]);
destruction.serverTime = std::stoull(parts[2]);
destruction.position = Eigen::Vector3f(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5])
);
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;
}
catch (const std::exception& e) {
std::cerr << "Client: Error parsing BOX_DESTROYED: " << e.what() << std::endl;
}
}
return;
}
if (msg.rfind("PROJECTILE:", 0) == 0) {
//auto parts = split(msg, ':');
if (parts.size() >= 10) {
try {
ProjectileInfo pi;
pi.shooterId = std::stoi(parts[1]);
pi.clientTime = std::stoull(parts[2]);
pi.position = Eigen::Vector3f(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5])
);
Eigen::Quaternionf q(
std::stof(parts[6]),
std::stof(parts[7]),
std::stof(parts[8]),
std::stof(parts[9])
);
pi.rotation = q.toRotationMatrix();
pi.velocity = std::stof(parts[10]);
std::lock_guard<std::mutex> pl(projMutex_);
pendingProjectiles_.push_back(pi);
}
catch (...) {}
}
return;
}
if (msg.rfind("DEAD:", 0) == 0) {
//auto parts = split(msg, ':');
if (parts.size() >= 7) {
try {
DeathInfo di;
di.serverTime = std::stoull(parts[1]);
di.targetId = std::stoi(parts[2]);
di.position = Eigen::Vector3f(
std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5])
);
di.killerId = std::stoi(parts[6]);
std::lock_guard<std::mutex> dl(deathsMutex_);
pendingDeaths_.push_back(di);
}
catch (...) {}
}
return;
}
if (msg.rfind("EVENT:", 0) == 0) {
//auto parts = split(msg, ':');
if (parts.size() < 5) return; // EVENT:ID:TYPE:TIME:DATA...
int remoteId = std::stoi(parts[1]);
std::string subType = parts[2];
uint64_t sentTime = std::stoull(parts[3]);
ClientState remoteState;
remoteState.id = remoteId;
std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast<std::chrono::system_clock::time_point::duration>(std::chrono::milliseconds(sentTime)) };
remoteState.lastUpdateServerTime = uptime_timepoint;
if (subType == "UPD") {
int startFrom = 4;
remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) };
Eigen::Quaternionf q(
std::stof(parts[startFrom + 3]),
std::stof(parts[startFrom + 4]),
std::stof(parts[startFrom + 5]),
std::stof(parts[startFrom + 6]));
remoteState.rotation = q.toRotationMatrix();
remoteState.currentAngularVelocity = Eigen::Vector3f{
std::stof(parts[startFrom + 7]),
std::stof(parts[startFrom + 8]),
std::stof(parts[startFrom + 9]) };
remoteState.velocity = std::stof(parts[startFrom + 10]);
remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]);
remoteState.discreteMag = std::stof(parts[startFrom + 12]);
remoteState.discreteAngle = std::stoi(parts[startFrom + 13]);
}
else
{
throw std::runtime_error("Unknown EVENT subtype: " + subType);
}
{
std::lock_guard<std::mutex> pLock(playersMutex);
auto& rp = remotePlayers[remoteId];
rp.add_state(remoteState);
}
}
if (msg.rfind("SNAPSHOT:", 0) == 0) {
auto mainParts = split(msg.substr(9), '|'); // Отсекаем "SNAPSHOT:" и делим по игрокам
if (mainParts.empty()) return;
uint64_t serverTimestamp = std::stoull(mainParts[0]);
std::chrono::system_clock::time_point serverTime{ std::chrono::milliseconds(serverTimestamp) };
for (size_t i = 1; i < mainParts.size(); ++i) {
auto playerParts = split(mainParts[i], ':');
if (playerParts.size() < 15) return; // ID + 14 полей ClientState
int rId = std::stoi(playerParts[0]);
if (rId == clientId) return; // Свое состояние игрок знает лучше всех (Client Side Prediction)
ClientState remoteState;
remoteState.id = rId;
remoteState.lastUpdateServerTime = serverTime;
// Используем твой 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
size_t hashValue = fnv1a_hash(msg + NET_SECRET);
std::stringstream ss;
ss << msg << "#hash:" << std::hex << hashValue;
return ss.str();
#else
return msg;
#endif
}
std::vector<ProjectileInfo> WebSocketClientBase::getPendingProjectiles() {
std::lock_guard<std::mutex> lock(projMutex_);
auto copy = pendingProjectiles_;
pendingProjectiles_.clear();
return copy;
}
std::vector<DeathInfo> WebSocketClientBase::getPendingDeaths() {
std::lock_guard<std::mutex> lock(deathsMutex_);
auto copy = pendingDeaths_;
pendingDeaths_.clear();
return copy;
}
std::vector<int> WebSocketClientBase::getPendingRespawns() {
std::lock_guard<std::mutex> lock(respawnMutex_);
auto copy = pendingRespawns_;
pendingRespawns_.clear();
return copy;
}
std::vector<BoxDestroyedInfo> WebSocketClientBase::getPendingBoxDestructions() {
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
auto copy = pendingBoxDestructions_;
pendingBoxDestructions_.clear();
return copy;
}
}
#endif

View File

@ -0,0 +1,61 @@
#pragma once
#include "NetworkInterface.h"
#include <queue>
#include <mutex>
namespace ZL {
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;
public:
int GetClientId() const override { return clientId; }
int64_t getTimeOffset() const override { return timeOffset; }
void HandlePollMessage(const std::string& msg);
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_;
}
std::vector<ProjectileInfo> getPendingProjectiles() override;
std::vector<DeathInfo> getPendingDeaths() override;
std::vector<int> getPendingRespawns() override;
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
};
}

View File

@ -0,0 +1,94 @@
#ifdef NETWORK
#ifdef EMSCRIPTEN
#include "WebSocketClientEmscripten.h"
#include <iostream>
#include <SDL2/SDL.h>
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(),
nullptr,
EM_TRUE // create_on_main_thread
};
socket_ = emscripten_websocket_new(&attr);
emscripten_websocket_set_onopen_callback(socket_, this, onOpen);
emscripten_websocket_set_onmessage_callback(socket_, this, onMessage);
emscripten_websocket_set_onerror_callback(socket_, this, onError);
emscripten_websocket_set_onclose_callback(socket_, this, onClose);
}
void WebSocketClientEmscripten::Send(const std::string& message) {
if (connected && socket_ > 0) {
auto signedMsg = SignMessage(message);
std::cout << "[WebWS] Sending message: " << signedMsg << std::endl;
emscripten_websocket_send_utf8_text(socket_, signedMsg.c_str());
}
}
void WebSocketClientEmscripten::Poll() {
// Локальная очередь для минимизации времени блокировки мьютекса
std::queue<std::string> localQueue;
{
std::lock_guard<std::mutex> lock(queueMutex);
if (messageQueue.empty()) return;
std::swap(localQueue, messageQueue);
}
while (!localQueue.empty()) {
const std::string& msg = localQueue.front();
std::cout << "[WebWS] Processing message: " << msg << std::endl;
// Передаем в базовый класс для парсинга игровых событий (BOXES, UPD, и т.д.)
HandlePollMessage(msg);
localQueue.pop();
}
}
// --- Колбэки ---
EM_BOOL WebSocketClientEmscripten::onOpen(int eventType, const EmscriptenWebSocketOpenEvent* e, void* userData) {
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
self->connected = true;
std::cout << "[WebWS] Connection opened" << std::endl;
return EM_TRUE;
}
EM_BOOL WebSocketClientEmscripten::onMessage(int eventType, const EmscriptenWebSocketMessageEvent* e, void* userData) {
std::cout << "[WebWS] onMessage " << std::endl;
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
if (e->isText && e->data) {
std::string msg(reinterpret_cast<const char*>(e->data), e->numBytes);
std::lock_guard<std::mutex> lock(self->queueMutex);
self->messageQueue.push(msg);
}
return EM_TRUE;
}
EM_BOOL WebSocketClientEmscripten::onError(int eventType, const EmscriptenWebSocketErrorEvent* e, void* userData) {
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
self->connected = false;
std::cerr << "[WebWS] Error detected" << std::endl;
return EM_TRUE;
}
EM_BOOL WebSocketClientEmscripten::onClose(int eventType, const EmscriptenWebSocketCloseEvent* e, void* userData) {
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
self->connected = false;
std::cout << "[WebWS] Connection closed" << std::endl;
return EM_TRUE;
}
}
#endif
#endif

View File

@ -0,0 +1,42 @@
#pragma once
#ifdef NETWORK
#ifdef EMSCRIPTEN
#include <emscripten/websocket.h>
#include "WebSocketClientBase.h"
#include <queue>
#include <mutex>
#include <string>
namespace ZL {
class WebSocketClientEmscripten : public WebSocketClientBase {
private:
EMSCRIPTEN_WEBSOCKET_T socket_ = 0;
bool connected = false;
// Очередь для хранения сырых строк от браузера
std::queue<std::string> messageQueue;
std::mutex queueMutex;
public:
WebSocketClientEmscripten() = default;
virtual ~WebSocketClientEmscripten() = default;
void Connect(const std::string& host, uint16_t port) override;
void Send(const std::string& message) override;
void Poll() override;
bool IsConnected() const override { return connected; }
// Статические методы-переходники для C-API Emscripten
static EM_BOOL onOpen(int eventType, const EmscriptenWebSocketOpenEvent* e, void* userData);
static EM_BOOL onMessage(int eventType, const EmscriptenWebSocketMessageEvent* e, void* userData);
static EM_BOOL onError(int eventType, const EmscriptenWebSocketErrorEvent* e, void* userData);
static EM_BOOL onClose(int eventType, const EmscriptenWebSocketCloseEvent* e, void* userData);
};
}
#endif
#endif

View File

@ -291,7 +291,7 @@ namespace ZL {
drawPlanet(renderer); drawPlanet(renderer);
drawStones(renderer); drawStones(renderer);
drawCamp(renderer); //drawCamp(renderer);
glClear(GL_DEPTH_BUFFER_BIT); glClear(GL_DEPTH_BUFFER_BIT);
drawAtmosphere(renderer); drawAtmosphere(renderer);
} }

View File

@ -6,26 +6,21 @@
#include "render/OpenGlExtensions.h" #include "render/OpenGlExtensions.h"
#include <iostream> #include <iostream>
#include <array> #include <array>
#include <algorithm>
#include <cmath>
namespace ZL { namespace ZL {
TextRenderer::~TextRenderer() TextRenderer::~TextRenderer()
{ {
/*for (auto& kv : glyphs) {
if (kv.second.texID) glDeleteTextures(1, &kv.second.texID);
}*/
glyphs.clear(); glyphs.clear();
atlasTexture.reset();
textMesh.positionVBO.reset(); textMesh.positionVBO.reset();
/*
if (vbo) glDeleteBuffers(1, &vbo);
// if (vao) glDeleteVertexArrays(1, &vao);
vao = 0;
vbo = 0;*/
} }
bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename) bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename)
{ {
r = &renderer; r = &renderer;
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
@ -47,19 +42,15 @@ bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixe
textMesh.data.PositionData.resize(6, Eigen::Vector3f(0, 0, 0)); textMesh.data.PositionData.resize(6, Eigen::Vector3f(0, 0, 0));
textMesh.RefreshVBO(); textMesh.RefreshVBO();
// VAO/VBO для 6 вершин (2 треугольника) * (pos.xy + uv.xy) = 4 float
// glGenVertexArrays(1, &vao);
/* glGenBuffers(1, &vbo);
//glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, nullptr, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
*/
ZL::CheckGlError(); ZL::CheckGlError();
return true; return true;
} }
void TextRenderer::ClearCache()
{
cache.clear();
}
bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename) bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename)
{ {
// 1. Загружаем сырые данные из ZIP // 1. Загружаем сырые данные из ZIP
@ -97,38 +88,188 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
} }
FT_Set_Pixel_Sizes(face, 0, pixelSize); FT_Set_Pixel_Sizes(face, 0, pixelSize);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glyphs.clear(); // glyphs.clear();
// Проходим по стандартным ASCII символам
// Сначала собираем все глифы в память
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) { for (unsigned char c = 32; c < 128; ++c) {
if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
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;
}
TextureDataStruct glyphData; GlyphBitmap gb;
glyphData.width = face->glyph->bitmap.width; gb.width = face->glyph->bitmap.width;
glyphData.height = face->glyph->bitmap.rows; gb.height = face->glyph->bitmap.rows;
glyphData.format = TextureDataStruct::R8; gb.bearing = Eigen::Vector2f((float)face->glyph->bitmap_left, (float)face->glyph->bitmap_top);
glyphData.mipmap = TextureDataStruct::NONE; gb.advance = static_cast<unsigned int>(face->glyph->advance.x);
// Копируем буфер FreeType в вектор данных size_t dataSize = static_cast<size_t>(gb.width) * static_cast<size_t>(gb.height);
size_t dataSize = glyphData.width * glyphData.height; if (dataSize > 0) {
glyphData.data.assign(face->glyph->bitmap.buffer, face->glyph->bitmap.buffer + dataSize); 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));
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);
} }
// Пакуем глифы в один атлас (упрощённый алгоритм строковой укладки)
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_Face(face);
FT_Done_FreeType(ft); FT_Done_FreeType(ft);
@ -142,60 +283,81 @@ 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) void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color)
{ {
if (!r || text.empty()) return; if (!r || text.empty() || !atlasTexture) return;
// формируем ключ кеша
std::string key = text + "|" + std::to_string(scale) + "|" + (centered ? "1" : "0");
auto itCache = cache.find(key);
if (itCache == cache.end()) {
VertexDataStruct textData;
float penX = 0.0f;
float penY = 0.0f;
float totalW = 0.0f;
float maxH = 0.0f;
// 1. Считаем ширину для центрирования
float totalW = 0.0f;
if (centered) {
for (char ch : text) { for (char ch : text) {
auto it = glyphs.find(ch); auto git = glyphs.find(ch);
if (it == glyphs.end()) continue; if (git == glyphs.end()) continue;
totalW += (it->second.advance >> 6) * scale; const GlyphInfo& g = git->second;
float xpos = penX + g.bearing.x() * scale;
float ypos = penY - (g.size.y() - g.bearing.y()) * scale;
float w = g.size.x() * scale;
float h = g.size.y() * scale;
// Добавляем 2 треугольника (6 вершин) для текущего символа
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 });
textData.PositionData.push_back({ xpos, ypos + h, 0.0f });
textData.PositionData.push_back({ xpos + w, ypos, 0.0f });
textData.PositionData.push_back({ xpos + w, ypos + h, 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);
} }
x -= totalW * 0.5f;
// Сохраняем в кеш
CachedText ct;
ct.width = totalW;
ct.height = maxH;
ct.mesh.AssignFrom(textData);
auto res = cache.emplace(key, std::move(ct));
itCache = res.first;
} }
// 2. Подготовка данных (аналог CreateRect2D, но для всей строки) // Используем кешированный меш
VertexDataStruct textData; CachedText& cached = itCache->second;
float penX = x;
float penY = y;
for (char ch : text) { // Вычисляем смещение для проекции (оставляем Y как есть)
auto it = glyphs.find(ch); float tx = x;
if (it == glyphs.end()) continue; if (centered) {
tx = x - cached.width * 0.5f;
const GlyphInfo& g = it->second;
float xpos = penX + g.bearing.x() * scale;
float ypos = penY - (g.size.y() - g.bearing.y()) * scale;
float w = g.size.x() * scale;
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 });
textData.PositionData.push_back({ xpos, ypos + h, 0.0f });
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 });
penX += (g.advance >> 6) * scale;
} }
float ty = y;
// 3. Обновляем VBO через наш стандартный механизм // 3. Обновляем VBO через наш стандартный механизм
// Примечание: для текста лучше использовать GL_DYNAMIC_DRAW, // Примечание: для текста лучше использовать GL_DYNAMIC_DRAW,
// но RefreshVBO сейчас жестко зашит на GL_STATIC_DRAW. // но RefreshVBO сейчас жестко зашит на GL_STATIC_DRAW.
// Для UI это обычно не критично, если строк не тысячи. // Для UI это обычно не критично, если строк не тысячи.
textMesh.AssignFrom(textData); // textMesh.AssignFrom(textData);
// 4. Рендеринг // 4. Рендеринг
r->shaderManager.PushShader(shaderName); r->shaderManager.PushShader(shaderName);
@ -206,41 +368,44 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
Eigen::Matrix4f proj = Eigen::Matrix4f::Identity(); Eigen::Matrix4f proj = Eigen::Matrix4f::Identity();
proj(0, 0) = 2.0f / W; proj(0, 0) = 2.0f / W;
proj(1, 1) = 2.0f / H; proj(1, 1) = 2.0f / H;
proj(0, 3) = -1.0f; // Сдвигаем проекцию так, чтобы локальные координаты меша (pen-origin=0,0) оказались в (tx,ty)
proj(1, 3) = -1.0f; 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->RenderUniformMatrix4fv("uProjection", false, proj.data());
r->RenderUniform1i("uText", 0); r->RenderUniform1i("uText", 0);
r->RenderUniform4fv("uColor", color.data()); r->RenderUniform4fv("uColor", color.data());
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, atlasTexture->getTexID());
// ВНИМАНИЕ: Так как у тебя каждый символ — это отдельная текстура,
// нам всё равно придется делать glDrawArrays в цикле, ЛИБО использовать атлас.
// Если оставляем текущую систему с разными текстурами:
r->EnableVertexAttribArray("vPosition"); r->EnableVertexAttribArray("vPosition");
r->EnableVertexAttribArray("vTexCoord"); r->EnableVertexAttribArray("vTexCoord");
for (size_t i = 0; i < text.length(); ++i) { //for (size_t i = 0; i < text.length(); ++i) {
auto it = glyphs.find(text[i]); // auto it = glyphs.find(text[i]);
if (it == glyphs.end()) continue; // if (it == glyphs.end()) continue;
glBindTexture(GL_TEXTURE_2D, it->second.texture->getTexID()); // glBindTexture(GL_TEXTURE_2D, it->second.texture->getTexID());
// Отрисовываем по 6 вершин за раз // // Отрисовываем по 6 вершин за раз
// Нам нужно вручную биндить VBO, так как DrawVertexRenderStruct рисует всё сразу // // Нам нужно вручную биндить VBO, так как DrawVertexRenderStruct рисует всё сразу
glBindBuffer(GL_ARRAY_BUFFER, textMesh.positionVBO->getBuffer()); // glBindBuffer(GL_ARRAY_BUFFER, textMesh.positionVBO->getBuffer());
r->VertexAttribPointer3fv("vPosition", 0, (const char*)(i * 6 * sizeof(Vector3f))); // r->VertexAttribPointer3fv("vPosition", 0, (const char*)(i * 6 * sizeof(Vector3f)));
glBindBuffer(GL_ARRAY_BUFFER, textMesh.texCoordVBO->getBuffer()); // glBindBuffer(GL_ARRAY_BUFFER, textMesh.texCoordVBO->getBuffer());
r->VertexAttribPointer2fv("vTexCoord", 0, (const char*)(i * 6 * sizeof(Vector2f))); // 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("vPosition");
r->DisableVertexAttribArray("vTexCoord"); r->DisableVertexAttribArray("vTexCoord");
r->shaderManager.PopShader(); r->shaderManager.PopShader();
// Сброс бинда текстуры не обязателен, но можно для чистоты
glBindTexture(GL_TEXTURE_2D, 0);
} }
} // namespace ZL } // namespace ZL

View File

@ -12,7 +12,10 @@
namespace ZL { namespace ZL {
struct GlyphInfo { 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 size; // glyph size in pixels
Eigen::Vector2f bearing; // offset from baseline Eigen::Vector2f bearing; // offset from baseline
unsigned int advance = 0; // advance.x in 1/64 pixels 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); 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 }); 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: private:
bool loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename); bool loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename);
@ -37,9 +43,24 @@ private:
//unsigned int vao = 0; //unsigned int vao = 0;
//unsigned int vbo = 0; //unsigned int vbo = 0;
// единый атлас для всех глифов
std::shared_ptr<Texture> atlasTexture;
size_t atlasWidth = 0;
size_t atlasHeight = 0;
VertexRenderStruct textMesh; VertexRenderStruct textMesh;
std::string shaderName = "text2d"; 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 } // namespace ZL