Compare commits
No commits in common. "b59a10b7e6114bf839b47d59ad542aa81df0aec2" and "0babfff28b3e1f9ace2447648b82e51e58602820" have entirely different histories.
b59a10b7e6
...
0babfff28b
@ -65,10 +65,6 @@ 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
|
||||||
)
|
)
|
||||||
@ -94,7 +90,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 websocket)
|
target_link_libraries(space-game001 PRIVATE zip z)
|
||||||
|
|
||||||
# Эмскриптен-флаги
|
# Эмскриптен-флаги
|
||||||
set(EMSCRIPTEN_FLAGS
|
set(EMSCRIPTEN_FLAGS
|
||||||
@ -106,7 +102,6 @@ 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")
|
||||||
|
|||||||
@ -57,16 +57,8 @@ 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
|
||||||
|
|||||||
@ -94,24 +94,11 @@
|
|||||||
"pressed": "resources/main_menu/multi.png"
|
"pressed": "resources/main_menu/multi.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "Button",
|
|
||||||
"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",
|
"type": "Button",
|
||||||
"name": "exitButton",
|
"name": "exitButton",
|
||||||
"x": 409,
|
"x": 409,
|
||||||
"y": 147,
|
"y": 218,
|
||||||
"width": 382,
|
"width": 382,
|
||||||
"height": 56,
|
"height": 56,
|
||||||
"textures": {
|
"textures": {
|
||||||
@ -124,7 +111,7 @@
|
|||||||
"type": "Button",
|
"type": "Button",
|
||||||
"name": "versionLabel",
|
"name": "versionLabel",
|
||||||
"x": 559.5,
|
"x": 559.5,
|
||||||
"y": 99,
|
"y": 170,
|
||||||
"width": 81,
|
"width": 81,
|
||||||
"height": 9,
|
"height": 9,
|
||||||
"textures": {
|
"textures": {
|
||||||
|
|||||||
@ -141,9 +141,9 @@
|
|||||||
"type": "Slider",
|
"type": "Slider",
|
||||||
"name": "velocitySlider",
|
"name": "velocitySlider",
|
||||||
"x": 1140,
|
"x": 1140,
|
||||||
"y": 300,
|
"y": 100,
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"height": 300,
|
"height": 500,
|
||||||
"value": 0.0,
|
"value": 0.0,
|
||||||
"orientation": "vertical",
|
"orientation": "vertical",
|
||||||
"textures": {
|
"textures": {
|
||||||
@ -164,19 +164,6 @@
|
|||||||
"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)
BIN
resources/sky/space_red.png
(Stored with Git LFS)
Binary file not shown.
@ -129,29 +129,21 @@ public:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsMessageValid(const std::string& fullMessage) {
|
|
||||||
#ifdef ENABLE_NETWORK_CHECKSUM
|
|
||||||
size_t hashPos = fullMessage.find("#hash:");
|
|
||||||
if (hashPos == std::string::npos) {
|
|
||||||
return false; // Хеша нет, хотя он ожидался
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string originalContent = fullMessage.substr(0, hashPos);
|
|
||||||
std::string receivedHashStr = fullMessage.substr(hashPos + 6); // 6 — длина "#hash:"
|
|
||||||
|
|
||||||
// Вычисляем ожидаемый хеш от контента
|
|
||||||
size_t expectedHash = fnv1a_hash(originalContent + NET_SECRET);
|
|
||||||
|
|
||||||
std::stringstream ss;
|
|
||||||
ss << std::hex << expectedHash;
|
|
||||||
|
|
||||||
return ss.str() == receivedHashStr;
|
|
||||||
#else
|
|
||||||
return true; // В режиме отладки пропускаем всё
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/*
|
||||||
|
void init() {
|
||||||
|
sendBoxesToClient();
|
||||||
|
|
||||||
|
auto timer = std::make_shared<net::steady_timer>(ws_.get_executor());
|
||||||
|
timer->expires_after(std::chrono::milliseconds(100));
|
||||||
|
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_));
|
||||||
|
self->do_read();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
void sendBoxesToClient() {
|
void sendBoxesToClient() {
|
||||||
std::lock_guard<std::mutex> lock(g_boxes_mutex);
|
std::lock_guard<std::mutex> lock(g_boxes_mutex);
|
||||||
@ -175,6 +167,10 @@ private:
|
|||||||
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
/*
|
||||||
|
explicit Session(tcp::socket&& socket, int id)
|
||||||
|
: ws_(std::move(socket)), id_(id) {
|
||||||
|
}*/
|
||||||
|
|
||||||
void init()
|
void init()
|
||||||
{
|
{
|
||||||
@ -189,6 +185,31 @@ 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()) {
|
||||||
@ -205,7 +226,31 @@ 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()) {
|
||||||
@ -252,15 +297,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void process_message(const std::string& msg) {
|
void process_message(const std::string& msg) {
|
||||||
if (!IsMessageValid(msg)) {
|
auto parts = split(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;
|
||||||
|
|
||||||
@ -286,7 +323,7 @@ private:
|
|||||||
receivedState.handle_full_sync(parts, 2);
|
receivedState.handle_full_sync(parts, 2);
|
||||||
timedClientStates.add_state(receivedState);
|
timedClientStates.add_state(receivedState);
|
||||||
|
|
||||||
retranslateMessage(cleanMessage);
|
retranslateMessage(msg);
|
||||||
}
|
}
|
||||||
else if (parts[0] == "RESPAWN") {
|
else if (parts[0] == "RESPAWN") {
|
||||||
{
|
{
|
||||||
@ -340,8 +377,8 @@ private:
|
|||||||
|
|
||||||
{
|
{
|
||||||
const std::vector<Eigen::Vector3f> localOffsets = {
|
const std::vector<Eigen::Vector3f> localOffsets = {
|
||||||
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)
|
Eigen::Vector3f(1.5f, 0.9f, 5.0f)
|
||||||
};
|
};
|
||||||
|
|
||||||
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
@ -532,8 +569,7 @@ 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;
|
||||||
@ -541,12 +577,11 @@ 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, 0.0f, 45000.0f);
|
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 6.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) {
|
||||||
@ -716,15 +751,14 @@ 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(), 8081} };
|
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} };
|
||||||
|
|
||||||
int next_id = 1000;
|
int next_id = 1000;
|
||||||
|
|
||||||
std::cout << "Server started on port 8081...\n";
|
std::cout << "Server started on port 8080...\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) {
|
||||||
|
|||||||
1538
src/Game.cpp
1538
src/Game.cpp
File diff suppressed because it is too large
Load Diff
93
src/Game.h
93
src/Game.h
@ -14,13 +14,19 @@
|
|||||||
#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();
|
||||||
@ -35,30 +41,99 @@ 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 drawUnderMainMenu();
|
void drawRemoteShips();
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int64_t newTickCount;
|
size_t newTickCount;
|
||||||
int64_t lastTickCount;
|
size_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;
|
||||||
|
|
||||||
MenuManager menuManager;
|
std::shared_ptr<Texture> sparkTexture;
|
||||||
Space space;
|
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;
|
||||||
|
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,198 +0,0 @@
|
|||||||
#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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
#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
1450
src/Space.cpp
File diff suppressed because it is too large
Load Diff
139
src/Space.h
139
src/Space.h
@ -1,139 +0,0 @@
|
|||||||
#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
|
|
||||||
@ -182,7 +182,60 @@ namespace ZL {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
|
void UiManager::loadFromFile(const std::string& path, 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>();
|
||||||
@ -363,76 +416,6 @@ 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;
|
||||||
@ -570,8 +553,7 @@ namespace ZL {
|
|||||||
return tf->text;
|
return tf->text;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UiManager::pushMenuFromSavedRoot(std::shared_ptr<UiNode> newRoot)
|
bool UiManager::pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
|
||||||
{
|
|
||||||
MenuState prev;
|
MenuState prev;
|
||||||
prev.root = root;
|
prev.root = root;
|
||||||
prev.buttons = buttons;
|
prev.buttons = buttons;
|
||||||
@ -597,22 +579,17 @@ namespace ZL {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceRoot(newRoot);
|
loadFromFile(path, renderer, zipFile);
|
||||||
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 from root : " << e.what() << std::endl;
|
std::cerr << "UiManager: pushMenuFromFile failed to load " << path << " : " << 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;
|
||||||
|
|||||||
@ -136,15 +136,10 @@ 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);
|
||||||
@ -191,7 +186,6 @@ 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();
|
||||||
|
|
||||||
@ -202,6 +196,7 @@ 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);
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,5 @@
|
|||||||
#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)
|
||||||
|
|||||||
@ -9,9 +9,6 @@
|
|||||||
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;
|
||||||
@ -23,11 +20,9 @@ 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 = 500; //ms
|
constexpr long long CLIENT_DELAY = 1000; //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 };
|
||||||
|
|||||||
@ -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, 0.0f, 45000.0f);
|
Eigen::Vector3f boxWorld = serverBoxes[bi].position + Eigen::Vector3f(0.0f, 6.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 - 6.f, 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, 5.0f)
|
||||||
};
|
};
|
||||||
|
|
||||||
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
|||||||
@ -47,7 +47,5 @@ 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; }
|
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,17 @@
|
|||||||
#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) {
|
||||||
@ -44,20 +55,48 @@ 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:
|
||||||
@ -65,36 +104,229 @@ 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;
|
||||||
|
|
||||||
std::string finalMessage = SignMessage(message);
|
auto ss = std::make_shared<std::string>(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);
|
||||||
@ -132,4 +364,4 @@ namespace ZL {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#ifdef NETWORK
|
#ifdef NETWORK
|
||||||
|
|
||||||
#include "WebSocketClientBase.h"
|
#include "NetworkInterface.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 WebSocketClientBase {
|
class WebSocketClient : public INetworkClient {
|
||||||
private:
|
private:
|
||||||
// Переиспользуем io_context из TaskManager
|
// Переиспользуем io_context из TaskManager
|
||||||
boost::asio::io_context& ioc_;
|
boost::asio::io_context& ioc_;
|
||||||
@ -28,7 +28,26 @@ 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);
|
||||||
@ -44,6 +63,22 @@ 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
|
||||||
|
|||||||
@ -1,294 +0,0 @@
|
|||||||
#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
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
#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;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
#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
|
|
||||||
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
#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
|
|
||||||
|
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,21 +6,26 @@
|
|||||||
#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
|
||||||
@ -42,15 +47,19 @@ 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
|
||||||
@ -88,188 +97,38 @@ 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)) {
|
|
||||||
// пропускаем если не удалось загрузить, но сохраняем пустой запись с advance
|
FT_Load_Char(face, c, FT_LOAD_RENDER);
|
||||||
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;
|
TextureDataStruct glyphData;
|
||||||
gb.width = face->glyph->bitmap.width;
|
glyphData.width = face->glyph->bitmap.width;
|
||||||
gb.height = face->glyph->bitmap.rows;
|
glyphData.height = face->glyph->bitmap.rows;
|
||||||
gb.bearing = Eigen::Vector2f((float)face->glyph->bitmap_left, (float)face->glyph->bitmap_top);
|
glyphData.format = TextureDataStruct::R8;
|
||||||
gb.advance = static_cast<unsigned int>(face->glyph->advance.x);
|
glyphData.mipmap = TextureDataStruct::NONE;
|
||||||
|
|
||||||
size_t dataSize = static_cast<size_t>(gb.width) * static_cast<size_t>(gb.height);
|
// Копируем буфер FreeType в вектор данных
|
||||||
if (dataSize > 0) {
|
size_t dataSize = glyphData.width * glyphData.height;
|
||||||
gb.data.assign(face->glyph->bitmap.buffer, face->glyph->bitmap.buffer + dataSize);
|
glyphData.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);
|
||||||
@ -283,81 +142,60 @@ 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() || !atlasTexture) return;
|
if (!r || text.empty()) return;
|
||||||
|
|
||||||
// формируем ключ кеша
|
// 1. Считаем ширину для центрирования
|
||||||
std::string key = text + "|" + std::to_string(scale) + "|" + (centered ? "1" : "0");
|
float totalW = 0.0f;
|
||||||
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;
|
|
||||||
|
|
||||||
for (char ch : text) {
|
|
||||||
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;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем в кеш
|
|
||||||
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) {
|
if (centered) {
|
||||||
tx = x - cached.width * 0.5f;
|
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, но для всей строки)
|
||||||
|
VertexDataStruct textData;
|
||||||
|
float penX = x;
|
||||||
|
float penY = y;
|
||||||
|
|
||||||
|
for (char ch : text) {
|
||||||
|
auto it = glyphs.find(ch);
|
||||||
|
if (it == glyphs.end()) continue;
|
||||||
|
|
||||||
|
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);
|
||||||
@ -368,44 +206,41 @@ 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;
|
||||||
// Сдвигаем проекцию так, чтобы локальные координаты меша (pen-origin=0,0) оказались в (tx,ty)
|
proj(0, 3) = -1.0f;
|
||||||
proj(0, 3) = -1.0f + 2.0f * (tx) / W;
|
proj(1, 3) = -1.0f;
|
||||||
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
|
||||||
@ -12,10 +12,7 @@
|
|||||||
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
|
||||||
@ -29,9 +26,6 @@ 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);
|
||||||
|
|
||||||
@ -43,24 +37,9 @@ 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
|
||||||
Loading…
Reference in New Issue
Block a user