Compare commits
19 Commits
528c94e921
...
b59a10b7e6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b59a10b7e6 | ||
|
|
5b57696acf | ||
|
|
1265d87bc5 | ||
|
|
46d7a2a25d | ||
|
|
0babfff28b | ||
|
|
ed6e1bacc7 | ||
|
|
9c39782729 | ||
|
|
efad2dde3e | ||
|
|
84d8ee7eee | ||
|
|
7693237aa5 | ||
|
|
a8700af1d0 | ||
|
|
9793c5bf06 | ||
|
|
22ee99418d | ||
|
|
2ffd8124f2 | ||
|
|
210c191d41 | ||
|
|
251a59ddbe | ||
|
|
5df2216da6 | ||
|
|
2c1c077611 | ||
|
|
629c9ba7b1 |
@ -65,6 +65,10 @@ set(SOURCES
|
||||
../src/network/ClientState.cpp
|
||||
../src/network/WebSocketClient.h
|
||||
../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.cpp
|
||||
)
|
||||
@ -90,7 +94,7 @@ add_subdirectory("../thirdparty/libzip-1.11.4" libzip-build)
|
||||
# Линковка:
|
||||
# 'zip' берется из add_subdirectory
|
||||
# 'z' - это системный zlib Emscripten-а (флаг -sUSE_ZLIB=1 добавим ниже)
|
||||
target_link_libraries(space-game001 PRIVATE zip z)
|
||||
target_link_libraries(space-game001 PRIVATE zip z websocket)
|
||||
|
||||
# Эмскриптен-флаги
|
||||
set(EMSCRIPTEN_FLAGS
|
||||
@ -102,6 +106,7 @@ set(EMSCRIPTEN_FLAGS
|
||||
"-pthread"
|
||||
"-sUSE_PTHREADS=1"
|
||||
"-fexceptions"
|
||||
"-DNETWORK"
|
||||
)
|
||||
|
||||
target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
|
||||
|
||||
@ -57,8 +57,16 @@ add_executable(space-game001
|
||||
../src/network/ClientState.cpp
|
||||
../src/network/WebSocketClient.h
|
||||
../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.cpp
|
||||
../src/MenuManager.h
|
||||
../src/MenuManager.cpp
|
||||
../src/Space.h
|
||||
../src/Space.cpp
|
||||
)
|
||||
|
||||
# Установка проекта по умолчанию для Visual Studio
|
||||
|
||||
BIN
resources/Cargo_Base_color_sRGB.png
(Stored with Git LFS)
Normal file
BIN
resources/Cargo_Base_color_sRGB.png
(Stored with Git LFS)
Normal file
Binary file not shown.
6175
resources/cargoship001.txt
Normal file
6175
resources/cargoship001.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -96,11 +96,24 @@
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "exitButton",
|
||||
"name": "multiplayerButton2",
|
||||
"x": 409,
|
||||
"y": 218,
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/multi.png",
|
||||
"hover": "resources/main_menu/multi.png",
|
||||
"pressed": "resources/main_menu/multi.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "exitButton",
|
||||
"x": 409,
|
||||
"y": 147,
|
||||
"width": 382,
|
||||
"height": 56,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/exit.png",
|
||||
"hover": "resources/main_menu/exit.png",
|
||||
@ -111,7 +124,7 @@
|
||||
"type": "Button",
|
||||
"name": "versionLabel",
|
||||
"x": 559.5,
|
||||
"y": 170,
|
||||
"y": 99,
|
||||
"width": 81,
|
||||
"height": 9,
|
||||
"textures": {
|
||||
|
||||
105
resources/config/multiplayer_menu.json
Normal file
105
resources/config/multiplayer_menu.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"root": {
|
||||
"type": "LinearLayout",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"orientation": "vertical",
|
||||
"spacing": 20,
|
||||
"children": [
|
||||
{
|
||||
"type": "TextView",
|
||||
"name": "titleText",
|
||||
"x": 300,
|
||||
"y": 100,
|
||||
"width": 1320,
|
||||
"height": 100,
|
||||
"text": "Multiplayer",
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"fontSize": 72,
|
||||
"color": [1, 1, 1, 1],
|
||||
"centered": true
|
||||
},
|
||||
{
|
||||
"type": "TextView",
|
||||
"name": "serverLabel",
|
||||
"x": 400,
|
||||
"y": 250,
|
||||
"width": 1120,
|
||||
"height": 50,
|
||||
"text": "Enter server name or IP:",
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"fontSize": 32,
|
||||
"color": [1, 1, 1, 1],
|
||||
"centered": false
|
||||
},
|
||||
{
|
||||
"type": "TextField",
|
||||
"name": "serverInputField",
|
||||
"x": 400,
|
||||
"y": 320,
|
||||
"width": 1120,
|
||||
"height": 60,
|
||||
"placeholder": "Enter server name or IP",
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"fontSize": 28,
|
||||
"maxLength": 256,
|
||||
"color": [1, 1, 1, 1],
|
||||
"placeholderColor": [0.6, 0.6, 0.6, 1],
|
||||
"backgroundColor": [0.15, 0.15, 0.15, 1],
|
||||
"borderColor": [0.7, 0.7, 0.7, 1]
|
||||
},
|
||||
{
|
||||
"type": "LinearLayout",
|
||||
"x": 400,
|
||||
"y": 450,
|
||||
"width": 1120,
|
||||
"height": 80,
|
||||
"orientation": "horizontal",
|
||||
"spacing": 30,
|
||||
"children": [
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "connectButton",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 530,
|
||||
"height": 80,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/single.png",
|
||||
"hover": "resources/main_menu/single.png",
|
||||
"pressed": "resources/main_menu/single.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "backButton",
|
||||
"x": 590,
|
||||
"y": 0,
|
||||
"width": 530,
|
||||
"height": 80,
|
||||
"textures": {
|
||||
"normal": "resources/main_menu/exit.png",
|
||||
"hover": "resources/main_menu/exit.png",
|
||||
"pressed": "resources/main_menu/exit.png"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "TextView",
|
||||
"name": "statusText",
|
||||
"x": 400,
|
||||
"y": 580,
|
||||
"width": 1120,
|
||||
"height": 50,
|
||||
"text": "Ready to connect",
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"fontSize": 24,
|
||||
"color": [0.8, 0.8, 0.8, 1],
|
||||
"centered": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -141,9 +141,9 @@
|
||||
"type": "Slider",
|
||||
"name": "velocitySlider",
|
||||
"x": 1140,
|
||||
"y": 100,
|
||||
"y": 300,
|
||||
"width": 50,
|
||||
"height": 500,
|
||||
"height": 300,
|
||||
"value": 0.0,
|
||||
"orientation": "vertical",
|
||||
"textures": {
|
||||
@ -164,6 +164,19 @@
|
||||
"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",
|
||||
"name": "velocityText",
|
||||
|
||||
BIN
resources/sky/space_red.png
(Stored with Git LFS)
Normal file
BIN
resources/sky/space_red.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -129,21 +129,29 @@ public:
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
/*
|
||||
void init() {
|
||||
sendBoxesToClient();
|
||||
bool IsMessageValid(const std::string& fullMessage) {
|
||||
#ifdef ENABLE_NETWORK_CHECKSUM
|
||||
size_t hashPos = fullMessage.find("#hash:");
|
||||
if (hashPos == std::string::npos) {
|
||||
return false; // Хеша нет, хотя он ожидался
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
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:
|
||||
|
||||
void sendBoxesToClient() {
|
||||
std::lock_guard<std::mutex> lock(g_boxes_mutex);
|
||||
@ -167,10 +175,6 @@ private:
|
||||
|
||||
|
||||
public:
|
||||
/*
|
||||
explicit Session(tcp::socket&& socket, int id)
|
||||
: ws_(std::move(socket)), id_(id) {
|
||||
}*/
|
||||
|
||||
void init()
|
||||
{
|
||||
@ -185,31 +189,6 @@ public:
|
||||
}
|
||||
});
|
||||
}
|
||||
/*
|
||||
void run() {
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_sessions_mutex);
|
||||
g_sessions.push_back(shared_from_this());
|
||||
}
|
||||
|
||||
ws_.async_accept([self = shared_from_this()](beast::error_code ec) {
|
||||
if (ec) return;
|
||||
std::cout << "Client " << self->id_ << " connected\n";
|
||||
self->init();
|
||||
// self->send_message("ID:" + std::to_string(self->id_));
|
||||
// self->do_read();
|
||||
});
|
||||
}*/
|
||||
|
||||
/*void send_message(std::string msg) {
|
||||
auto ss = std::make_shared<std::string>(std::move(msg));
|
||||
ws_.async_write(net::buffer(*ss), [ss](beast::error_code, std::size_t) {});
|
||||
}
|
||||
|
||||
int get_id() const {
|
||||
return id_;
|
||||
}*/
|
||||
|
||||
ClientState get_latest_state(std::chrono::system_clock::time_point now) {
|
||||
if (timedClientStates.timedStates.empty()) {
|
||||
@ -226,31 +205,7 @@ public:
|
||||
|
||||
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() {
|
||||
std::lock_guard<std::mutex> lock(writeMutex_);
|
||||
if (is_writing_ || writeQueue_.empty()) {
|
||||
@ -297,7 +252,15 @@ private:
|
||||
}
|
||||
|
||||
void process_message(const std::string& msg) {
|
||||
auto parts = split(msg, ':');
|
||||
if (!IsMessageValid(msg)) {
|
||||
// Логируем попытку подмены и просто выходим из обработки
|
||||
std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl;
|
||||
return;
|
||||
}
|
||||
std::string cleanMessage = msg.substr(0, msg.find("#hash:"));
|
||||
|
||||
std::cout << "Received from player " << id_ << ": " << cleanMessage << std::endl;
|
||||
auto parts = split(cleanMessage, ':');
|
||||
|
||||
if (parts.empty()) return;
|
||||
|
||||
@ -323,7 +286,7 @@ private:
|
||||
receivedState.handle_full_sync(parts, 2);
|
||||
timedClientStates.add_state(receivedState);
|
||||
|
||||
retranslateMessage(msg);
|
||||
retranslateMessage(cleanMessage);
|
||||
}
|
||||
else if (parts[0] == "RESPAWN") {
|
||||
{
|
||||
@ -377,8 +340,8 @@ private:
|
||||
|
||||
{
|
||||
const std::vector<Eigen::Vector3f> localOffsets = {
|
||||
Eigen::Vector3f(-1.5f, 0.9f, 5.0f),
|
||||
Eigen::Vector3f(1.5f, 0.9f, 5.0f)
|
||||
Eigen::Vector3f(-1.5f, 0.9f - 6.f, 5.0f),
|
||||
Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
|
||||
};
|
||||
|
||||
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
@ -569,7 +532,8 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
||||
|
||||
{
|
||||
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;
|
||||
|
||||
std::vector<std::pair<size_t, size_t>> boxProjectileCollisions;
|
||||
@ -577,11 +541,12 @@ void update_world(net::steady_timer& timer, net::io_context& ioc) {
|
||||
for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
|
||||
if (g_serverBoxes[bi].destroyed) continue;
|
||||
|
||||
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 6.0f, 45000.0f);
|
||||
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
|
||||
|
||||
for (size_t pi = 0; pi < g_projectiles.size(); ++pi) {
|
||||
const auto& pr = g_projectiles[pi];
|
||||
Eigen::Vector3f diff = pr.pos - boxWorld;
|
||||
//std::cout << "diff norm is " << diff.norm() << std::endl;
|
||||
float thresh = boxCollisionRadius + projectileHitRadius;
|
||||
|
||||
if (diff.squaredNorm() <= thresh * thresh) {
|
||||
@ -751,14 +716,15 @@ int main() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_boxes_mutex);
|
||||
g_serverBoxes = generateServerBoxes(50);
|
||||
//g_serverBoxes = generateServerBoxes(1);
|
||||
std::cout << "Generated " << g_serverBoxes.size() << " boxes on server\n";
|
||||
}
|
||||
net::io_context ioc;
|
||||
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} };
|
||||
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8081} };
|
||||
|
||||
int next_id = 1000;
|
||||
|
||||
std::cout << "Server started on port 8080...\n";
|
||||
std::cout << "Server started on port 8081...\n";
|
||||
|
||||
auto do_accept = [&](auto& self_fn) -> void {
|
||||
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {
|
||||
|
||||
1791
src/Game.cpp
1791
src/Game.cpp
File diff suppressed because it is too large
Load Diff
104
src/Game.h
104
src/Game.h
@ -14,19 +14,13 @@
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <render/TextRenderer.h>
|
||||
#include "MenuManager.h"
|
||||
#include "Space.h"
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
namespace ZL {
|
||||
|
||||
|
||||
struct BoxCoords
|
||||
{
|
||||
Vector3f pos;
|
||||
Matrix3f m;
|
||||
};
|
||||
|
||||
|
||||
class Game {
|
||||
public:
|
||||
Game();
|
||||
@ -41,110 +35,30 @@ namespace ZL {
|
||||
Renderer renderer;
|
||||
TaskManager taskManager;
|
||||
MainThreadHandler mainThreadHandler;
|
||||
|
||||
std::unique_ptr<INetworkClient> networkClient;
|
||||
private:
|
||||
int64_t getSyncTimeMs();
|
||||
void processTickCount();
|
||||
void drawScene();
|
||||
void drawCubemap(float skyPercent);
|
||||
void drawShip();
|
||||
void drawBoxes();
|
||||
void drawBoxesLabels();
|
||||
void drawUI();
|
||||
void drawRemoteShips();
|
||||
void drawRemoteShipsLabels();
|
||||
void fireProjectiles();
|
||||
|
||||
bool worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth) const;
|
||||
|
||||
void drawUnderMainMenu();
|
||||
void handleDown(int mx, int my);
|
||||
void handleUp(int mx, int my);
|
||||
void handleMotion(int mx, int my);
|
||||
|
||||
SDL_Window* window;
|
||||
SDL_GLContext glContext;
|
||||
|
||||
|
||||
|
||||
size_t newTickCount;
|
||||
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;
|
||||
int64_t newTickCount;
|
||||
int64_t lastTickCount;
|
||||
|
||||
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;
|
||||
MenuManager menuManager;
|
||||
Space space;
|
||||
|
||||
|
||||
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;
|
||||
|
||||
// --- Target HUD (brackets + offscreen arrow) ---
|
||||
int trackedTargetId = -1;
|
||||
bool targetWasVisible = false;
|
||||
float targetAcquireAnim = 0.0f; // 0..1 схлопывание (0 = далеко, 1 = на месте)
|
||||
|
||||
// временный меш для HUD (будем перезаливать VBO маленькими порциями)
|
||||
VertexRenderStruct hudTempMesh;
|
||||
|
||||
// helpers
|
||||
bool projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW) const;
|
||||
void drawTargetHud(); // рисует рамку или стрелку
|
||||
int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок)
|
||||
int spaceGameStarted = 0;
|
||||
};
|
||||
|
||||
|
||||
|
||||
198
src/MenuManager.cpp
Normal file
198
src/MenuManager.cpp
Normal file
@ -0,0 +1,198 @@
|
||||
#include "MenuManager.h"
|
||||
|
||||
|
||||
namespace ZL {
|
||||
|
||||
MenuManager::MenuManager(Renderer& iRenderer) :
|
||||
renderer(iRenderer)
|
||||
{
|
||||
}
|
||||
|
||||
void MenuManager::setupMenu()
|
||||
{
|
||||
|
||||
uiManager.loadFromFile("resources/config/main_menu.json", renderer, CONST_ZIP_FILE);
|
||||
|
||||
uiSavedRoot = loadUiFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE);
|
||||
|
||||
settingsSavedRoot = loadUiFromFile("resources/config/settings.json", renderer, CONST_ZIP_FILE);
|
||||
|
||||
multiplayerSavedRoot = loadUiFromFile("resources/config/multiplayer_menu.json", renderer, CONST_ZIP_FILE);
|
||||
|
||||
gameOverSavedRoot = loadUiFromFile("resources/config/game_over.json", renderer, CONST_ZIP_FILE);
|
||||
|
||||
std::function<void()> loadGameplayUI;
|
||||
loadGameplayUI = [this]() {
|
||||
uiManager.replaceRoot(uiSavedRoot);
|
||||
|
||||
auto velocityTv = uiManager.findTextView("velocityText");
|
||||
if (velocityTv) {
|
||||
velocityTv->rect.x = 10.0f;
|
||||
velocityTv->rect.y = static_cast<float>(Environment::height) - velocityTv->rect.h - 10.0f;
|
||||
}
|
||||
else {
|
||||
std::cerr << "Failed to find velocityText in UI" << std::endl;
|
||||
}
|
||||
|
||||
uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
|
||||
static bool isExitButtonAnimating = false;
|
||||
uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() {
|
||||
std::cerr << "Settings button animation finished -> переход в настройки" << std::endl;
|
||||
if (uiManager.pushMenuFromSavedRoot(settingsSavedRoot)) {
|
||||
uiManager.setButtonCallback("Opt1", [this](const std::string& n) {
|
||||
std::cerr << "Opt1 pressed: " << n << std::endl;
|
||||
});
|
||||
uiManager.setButtonCallback("Opt2", [this](const std::string& n) {
|
||||
std::cerr << "Opt2 pressed: " << n << std::endl;
|
||||
});
|
||||
uiManager.setButtonCallback("backButton", [this](const std::string& n) {
|
||||
uiManager.stopAllAnimations();
|
||||
uiManager.popMenu();
|
||||
});
|
||||
}
|
||||
else {
|
||||
std::cerr << "Failed to open settings menu after animations" << std::endl;
|
||||
}
|
||||
});
|
||||
|
||||
uiManager.setAnimationCallback("exitButton", "bgScroll", [this]() {
|
||||
std::cerr << "Exit button bgScroll animation finished" << std::endl;
|
||||
g_exitBgAnimating = false;
|
||||
});
|
||||
|
||||
uiManager.setButtonCallback("playButton", [this](const std::string& name) {
|
||||
std::cerr << "Play button pressed: " << name << std::endl;
|
||||
});
|
||||
|
||||
uiManager.setButtonCallback("settingsButton", [this](const std::string& name) {
|
||||
std::cerr << "Settings button pressed: " << name << std::endl;
|
||||
uiManager.startAnimationOnNode("playButton", "buttonsExit");
|
||||
uiManager.startAnimationOnNode("settingsButton", "buttonsExit");
|
||||
uiManager.startAnimationOnNode("exitButton", "buttonsExit");
|
||||
});
|
||||
|
||||
uiManager.setButtonCallback("exitButton", [this](const std::string& name) {
|
||||
std::cerr << "Exit button pressed: " << name << std::endl;
|
||||
|
||||
if (!g_exitBgAnimating) {
|
||||
std::cerr << "start repeat anim bgScroll on exitButton" << std::endl;
|
||||
g_exitBgAnimating = true;
|
||||
uiManager.startAnimationOnNode("exitButton", "bgScroll");
|
||||
}
|
||||
else {
|
||||
std::cerr << "stop repeat anim bgScroll on exitButton" << std::endl;
|
||||
g_exitBgAnimating = false;
|
||||
uiManager.stopAnimationOnNode("exitButton", "bgScroll");
|
||||
|
||||
auto exitButton = uiManager.findButton("exitButton");
|
||||
if (exitButton) {
|
||||
exitButton->animOffsetX = 0.0f;
|
||||
exitButton->animOffsetY = 0.0f;
|
||||
exitButton->animScaleX = 1.0f;
|
||||
exitButton->animScaleY = 1.0f;
|
||||
exitButton->buildMesh();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
uiManager.setButtonCallback("shootButton", [this](const std::string& name) {
|
||||
onFirePressed();
|
||||
});
|
||||
uiManager.setButtonCallback("shootButton2", [this](const std::string& name) {
|
||||
onFirePressed();
|
||||
});
|
||||
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
|
||||
int newVel = roundf(value * 10);
|
||||
/*if (newVel > 2)
|
||||
{
|
||||
newVel = 2;
|
||||
}*/
|
||||
|
||||
if (newVel != Environment::shipState.selectedVelocity) {
|
||||
onVelocityChanged(newVel);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
uiManager.setButtonCallback("singleButton", [loadGameplayUI, this](const std::string& name) {
|
||||
std::cerr << "Single button pressed: " << name << " -> load gameplay UI\n";
|
||||
loadGameplayUI();
|
||||
onSingleplayerPressed();
|
||||
});
|
||||
uiManager.setButtonCallback("multiplayerButton", [loadGameplayUI, this](const std::string& name) {
|
||||
std::cerr << "Multiplayer button pressed: " << name << " -> load gameplay UI\n";
|
||||
loadGameplayUI();
|
||||
onMultiplayerPressed();
|
||||
});
|
||||
|
||||
uiManager.setButtonCallback("multiplayerButton2", [this](const std::string& name) {
|
||||
std::cerr << "Multiplayer button pressed → opening multiplayer menu\n";
|
||||
|
||||
uiManager.startAnimationOnNode("playButton", "buttonsExit");
|
||||
uiManager.startAnimationOnNode("settingsButton", "buttonsExit");
|
||||
uiManager.startAnimationOnNode("multiplayerButton", "buttonsExit");
|
||||
uiManager.startAnimationOnNode("exitButton", "buttonsExit");
|
||||
|
||||
if (uiManager.pushMenuFromSavedRoot(multiplayerSavedRoot)) {
|
||||
|
||||
// Callback для кнопки подключения
|
||||
uiManager.setButtonCallback("connectButton", [this](const std::string& buttonName) {
|
||||
std::string serverAddress = uiManager.getTextFieldValue("serverInputField");
|
||||
|
||||
if (serverAddress.empty()) {
|
||||
uiManager.setText("statusText", "Please enter server address");
|
||||
return;
|
||||
}
|
||||
|
||||
uiManager.setText("statusText", "Connecting to " + serverAddress + "...");
|
||||
std::cerr << "Connecting to server: " << serverAddress << std::endl;
|
||||
|
||||
// Здесь добавить вашу логику подключения к серверу
|
||||
// connectToServer(serverAddress);
|
||||
});
|
||||
|
||||
// Callback для кнопки назад
|
||||
uiManager.setButtonCallback("backButton", [this](const std::string& buttonName) {
|
||||
uiManager.popMenu();
|
||||
});
|
||||
|
||||
// Callback для отслеживания ввода текста
|
||||
uiManager.setTextFieldCallback("serverInputField",
|
||||
[this](const std::string& fieldName, const std::string& newText) {
|
||||
std::cout << "Server input field changed to: " << newText << std::endl;
|
||||
});
|
||||
|
||||
std::cerr << "Multiplayer menu loaded successfully\n";
|
||||
}
|
||||
else {
|
||||
std::cerr << "Failed to load multiplayer menu\n";
|
||||
}
|
||||
});
|
||||
uiManager.setButtonCallback("exitButton", [](const std::string& name) {
|
||||
std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n";
|
||||
Environment::exitGameLoop = true;
|
||||
});
|
||||
}
|
||||
|
||||
void MenuManager::showGameOver()
|
||||
{
|
||||
if (!uiGameOverShown) {
|
||||
if (uiManager.pushMenuFromSavedRoot(gameOverSavedRoot)) {
|
||||
uiManager.setButtonCallback("restartButton", [this](const std::string& name) {
|
||||
uiGameOverShown = false;
|
||||
uiManager.popMenu();
|
||||
onRestartPressed();
|
||||
});
|
||||
|
||||
uiManager.setButtonCallback("gameOverExitButton", [this](const std::string& name) {
|
||||
Environment::exitGameLoop = true;
|
||||
});
|
||||
|
||||
uiGameOverShown = true;
|
||||
}
|
||||
else {
|
||||
std::cerr << "Failed to load game_over.json\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/MenuManager.h
Normal file
42
src/MenuManager.h
Normal file
@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include "render/Renderer.h"
|
||||
#include "Environment.h"
|
||||
#include "render/TextureManager.h"
|
||||
#include "UiManager.h"
|
||||
|
||||
namespace ZL {
|
||||
|
||||
extern const char* CONST_ZIP_FILE;
|
||||
//extern bool g_exitBgAnimating;
|
||||
|
||||
class MenuManager
|
||||
{
|
||||
protected:
|
||||
Renderer& renderer;
|
||||
std::shared_ptr<UiNode> uiSavedRoot;
|
||||
std::shared_ptr<UiNode> gameOverSavedRoot;
|
||||
std::shared_ptr<UiNode> settingsSavedRoot;
|
||||
std::shared_ptr<UiNode> multiplayerSavedRoot;
|
||||
|
||||
public:
|
||||
bool uiGameOverShown = false;
|
||||
bool g_exitBgAnimating = false;
|
||||
|
||||
UiManager uiManager;
|
||||
|
||||
MenuManager(Renderer& iRenderer);
|
||||
|
||||
void setupMenu();
|
||||
|
||||
void showGameOver();
|
||||
|
||||
std::function<void()> onRestartPressed;
|
||||
std::function<void(float)> onVelocityChanged;
|
||||
std::function<void()> onFirePressed;
|
||||
|
||||
std::function<void()> onSingleplayerPressed;
|
||||
std::function<void()> onMultiplayerPressed;
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
1450
src/Space.cpp
Normal file
1450
src/Space.cpp
Normal file
File diff suppressed because it is too large
Load Diff
139
src/Space.h
Normal file
139
src/Space.h
Normal file
@ -0,0 +1,139 @@
|
||||
#pragma once
|
||||
|
||||
#include "render/Renderer.h"
|
||||
#include "Environment.h"
|
||||
#include "render/TextureManager.h"
|
||||
#include "SparkEmitter.h"
|
||||
#include "planet/PlanetObject.h"
|
||||
#include "UiManager.h"
|
||||
#include "Projectile.h"
|
||||
#include "utils/TaskManager.h"
|
||||
#include "network/NetworkInterface.h"
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <render/TextRenderer.h>
|
||||
#include "MenuManager.h"
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
namespace ZL {
|
||||
|
||||
|
||||
struct BoxCoords
|
||||
{
|
||||
Vector3f pos;
|
||||
Matrix3f m;
|
||||
};
|
||||
|
||||
|
||||
class Space {
|
||||
public:
|
||||
Space(Renderer& iRenderer, TaskManager& iTaskManager, MainThreadHandler& iMainThreadHandler, std::unique_ptr<INetworkClient>& iNetworkClient, MenuManager& iMenuManager);
|
||||
~Space();
|
||||
|
||||
void setup();
|
||||
void update();
|
||||
|
||||
Renderer& renderer;
|
||||
TaskManager& taskManager;
|
||||
MainThreadHandler& mainThreadHandler;
|
||||
std::unique_ptr<INetworkClient>& networkClient;
|
||||
MenuManager& menuManager;
|
||||
|
||||
|
||||
public:
|
||||
void processTickCount(int64_t newTickCount, int64_t delta);
|
||||
void drawScene();
|
||||
void drawCubemap(float skyPercent);
|
||||
void drawShip();
|
||||
void drawBoxes();
|
||||
void drawBoxesLabels();
|
||||
void drawRemoteShips();
|
||||
void drawRemoteShipsLabels();
|
||||
void fireProjectiles();
|
||||
|
||||
void handleDown(int mx, int my);
|
||||
void handleUp(int mx, int my);
|
||||
void handleMotion(int mx, int my);
|
||||
|
||||
|
||||
std::vector<BoxCoords> boxCoordsArr;
|
||||
std::vector<VertexRenderStruct> boxRenderArr;
|
||||
|
||||
std::vector<std::string> boxLabels;
|
||||
std::unique_ptr<TextRenderer> textRenderer;
|
||||
|
||||
std::unordered_map<int, ClientState> remotePlayerStates;
|
||||
|
||||
float newShipVelocity = 0;
|
||||
|
||||
static const size_t CONST_TIMER_INTERVAL = 10;
|
||||
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
|
||||
|
||||
std::shared_ptr<Texture> sparkTexture;
|
||||
std::shared_ptr<Texture> spaceshipTexture;
|
||||
std::shared_ptr<Texture> cubemapTexture;
|
||||
VertexDataStruct spaceshipBase;
|
||||
VertexRenderStruct spaceship;
|
||||
|
||||
|
||||
VertexRenderStruct cubemap;
|
||||
|
||||
std::shared_ptr<Texture> boxTexture;
|
||||
VertexDataStruct boxBase;
|
||||
|
||||
SparkEmitter sparkEmitter;
|
||||
SparkEmitter projectileEmitter;
|
||||
SparkEmitter explosionEmitter;
|
||||
PlanetObject planetObject;
|
||||
|
||||
std::vector<std::unique_ptr<Projectile>> projectiles;
|
||||
std::shared_ptr<Texture> projectileTexture;
|
||||
float projectileCooldownMs = 500.0f;
|
||||
int64_t lastProjectileFireTime = 0;
|
||||
int maxProjectiles = 32;
|
||||
std::vector<Vector3f> shipLocalEmissionPoints;
|
||||
|
||||
|
||||
bool shipAlive = true;
|
||||
bool gameOver = false;
|
||||
bool firePressed = false;
|
||||
std::vector<bool> boxAlive;
|
||||
float shipCollisionRadius = 15.0f;
|
||||
float boxCollisionRadius = 2.0f;
|
||||
bool showExplosion = false;
|
||||
uint64_t lastExplosionTime = 0;
|
||||
const uint64_t explosionDurationMs = 500;
|
||||
|
||||
bool serverBoxesApplied = false;
|
||||
|
||||
static constexpr float MAX_DIST_SQ = 10000.f * 10000.f;
|
||||
static constexpr float FADE_START = 6000.f;
|
||||
static constexpr float FADE_RANGE = 4000.f;
|
||||
static constexpr float BASE_SCALE = 140.f;
|
||||
static constexpr float PERSPECTIVE_K = 0.05f; // Tune
|
||||
static constexpr float MIN_SCALE = 0.4f;
|
||||
static constexpr float MAX_SCALE = 0.8f;
|
||||
static constexpr float CLOSE_DIST = 600.0f;
|
||||
|
||||
std::unordered_set<int> deadRemotePlayers;
|
||||
|
||||
// --- Target HUD (brackets + offscreen arrow) ---
|
||||
int trackedTargetId = -1;
|
||||
bool targetWasVisible = false;
|
||||
float targetAcquireAnim = 0.0f; // 0..1 схлопывание (0 = далеко, 1 = на месте)
|
||||
|
||||
// временный меш для HUD (будем перезаливать VBO маленькими порциями)
|
||||
VertexRenderStruct hudTempMesh;
|
||||
|
||||
// helpers
|
||||
void drawTargetHud(); // рисует рамку или стрелку
|
||||
int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок)
|
||||
|
||||
void clearTextRendererCache();
|
||||
};
|
||||
|
||||
|
||||
} // namespace ZL
|
||||
@ -168,59 +168,21 @@ namespace ZL {
|
||||
renderer.DisableVertexAttribArray(vTexCoordName);
|
||||
}
|
||||
|
||||
void UiManager::loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
|
||||
std::string content;
|
||||
try {
|
||||
if (zipFile.empty()) {
|
||||
content = readTextFile(path);
|
||||
void UiTextField::draw(Renderer& renderer) const {
|
||||
if (textRenderer) {
|
||||
float textX = rect.x + 10.0f;
|
||||
float textY = rect.y + rect.h / 2.0f;
|
||||
|
||||
if (text.empty()) {
|
||||
textRenderer->drawText(placeholder, textX, textY, 1.0f, false, placeholderColor);
|
||||
}
|
||||
else {
|
||||
auto buf = readFileFromZIP(path, zipFile);
|
||||
if (buf.empty()) {
|
||||
std::cerr << "UiManager: failed to read " << path << " from zip " << zipFile << std::endl;
|
||||
throw std::runtime_error("Failed to load UI file: " + path);
|
||||
}
|
||||
content.assign(buf.begin(), buf.end());
|
||||
textRenderer->drawText(text, textX, textY, 1.0f, false, color);
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "UiManager: failed to open " << path << " : " << e.what() << std::endl;
|
||||
throw std::runtime_error("Failed to load UI file: " + path);
|
||||
}
|
||||
|
||||
json j;
|
||||
try {
|
||||
j = json::parse(content);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "UiManager: json parse error: " << e.what() << std::endl;
|
||||
throw std::runtime_error("Failed to load UI file: " + path);
|
||||
}
|
||||
|
||||
if (!j.contains("root") || !j["root"].is_object()) {
|
||||
std::cerr << "UiManager: root node missing or invalid" << std::endl;
|
||||
throw std::runtime_error("Failed to load UI file: " + path);
|
||||
}
|
||||
|
||||
root = parseNode(j["root"], renderer, zipFile);
|
||||
layoutNode(root);
|
||||
buttons.clear();
|
||||
sliders.clear();
|
||||
textViews.clear();
|
||||
collectButtonsAndSliders(root);
|
||||
|
||||
nodeActiveAnims.clear();
|
||||
|
||||
for (auto& b : buttons) {
|
||||
b->buildMesh();
|
||||
}
|
||||
for (auto& s : sliders) {
|
||||
s->buildTrackMesh();
|
||||
s->buildKnobMesh();
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<UiNode> UiManager::parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
|
||||
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
|
||||
auto node = std::make_shared<UiNode>();
|
||||
if (j.contains("type") && j["type"].is_string()) node->type = j["type"].get<std::string>();
|
||||
if (j.contains("name") && j["name"].is_string()) node->name = j["name"].get<std::string>();
|
||||
@ -277,7 +239,7 @@ namespace ZL {
|
||||
if (!t.contains(key) || !t[key].is_string()) return nullptr;
|
||||
std::string path = t[key].get<std::string>();
|
||||
try {
|
||||
std::cout << "UiManager: --loading texture for button '" << "' : " << path << " Zip file: " << zipFile << std::endl;
|
||||
std::cout << "UiManager: --loading texture for slider '" << s->name << "' : " << path << " Zip file: " << zipFile << std::endl;
|
||||
auto data = CreateTextureDataFromPng(path.c_str(), zipFile.c_str());
|
||||
return std::make_shared<Texture>(data);
|
||||
}
|
||||
@ -299,6 +261,44 @@ namespace ZL {
|
||||
|
||||
node->slider = s;
|
||||
}
|
||||
else if (node->type == "TextField") {
|
||||
auto tf = std::make_shared<UiTextField>();
|
||||
tf->name = node->name;
|
||||
tf->rect = node->rect;
|
||||
|
||||
if (j.contains("placeholder")) tf->placeholder = j["placeholder"].get<std::string>();
|
||||
if (j.contains("fontPath")) tf->fontPath = j["fontPath"].get<std::string>();
|
||||
if (j.contains("fontSize")) tf->fontSize = j["fontSize"].get<int>();
|
||||
if (j.contains("maxLength")) tf->maxLength = j["maxLength"].get<int>();
|
||||
|
||||
if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
tf->color[i] = j["color"][i].get<float>();
|
||||
}
|
||||
}
|
||||
if (j.contains("placeholderColor") && j["placeholderColor"].is_array() && j["placeholderColor"].size() == 4) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
tf->placeholderColor[i] = j["placeholderColor"][i].get<float>();
|
||||
}
|
||||
}
|
||||
if (j.contains("backgroundColor") && j["backgroundColor"].is_array() && j["backgroundColor"].size() == 4) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
tf->backgroundColor[i] = j["backgroundColor"][i].get<float>();
|
||||
}
|
||||
}
|
||||
if (j.contains("borderColor") && j["borderColor"].is_array() && j["borderColor"].size() == 4) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
tf->borderColor[i] = j["borderColor"][i].get<float>();
|
||||
}
|
||||
}
|
||||
|
||||
tf->textRenderer = std::make_unique<TextRenderer>();
|
||||
if (!tf->textRenderer->init(renderer, tf->fontPath, tf->fontSize, zipFile)) {
|
||||
std::cerr << "Failed to init TextRenderer for TextField: " << tf->name << std::endl;
|
||||
}
|
||||
|
||||
node->textField = tf;
|
||||
}
|
||||
|
||||
if (j.contains("animations") && j["animations"].is_object()) {
|
||||
for (auto it = j["animations"].begin(); it != j["animations"].end(); ++it) {
|
||||
@ -363,6 +363,76 @@ namespace ZL {
|
||||
return node;
|
||||
}
|
||||
|
||||
std::shared_ptr<UiNode> loadUiFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile)
|
||||
{
|
||||
std::shared_ptr<UiNode> root;
|
||||
|
||||
std::string content;
|
||||
try {
|
||||
if (zipFile.empty()) {
|
||||
content = readTextFile(path);
|
||||
}
|
||||
else {
|
||||
auto buf = readFileFromZIP(path, zipFile);
|
||||
if (buf.empty()) {
|
||||
std::cerr << "UiManager: failed to read " << path << " from zip " << zipFile << std::endl;
|
||||
throw std::runtime_error("Failed to load UI file: " + path);
|
||||
}
|
||||
content.assign(buf.begin(), buf.end());
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "UiManager: failed to open " << path << " : " << e.what() << std::endl;
|
||||
throw std::runtime_error("Failed to load UI file: " + path);
|
||||
}
|
||||
|
||||
json j;
|
||||
try {
|
||||
j = json::parse(content);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "UiManager: json parse error: " << e.what() << std::endl;
|
||||
throw std::runtime_error("Failed to load UI file: " + path);
|
||||
}
|
||||
|
||||
if (!j.contains("root") || !j["root"].is_object()) {
|
||||
std::cerr << "UiManager: root node missing or invalid" << std::endl;
|
||||
throw std::runtime_error("Failed to load UI file: " + path);
|
||||
}
|
||||
|
||||
root = parseNode(j["root"], renderer, zipFile);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
void UiManager::replaceRoot(std::shared_ptr<UiNode> newRoot) {
|
||||
root = newRoot;
|
||||
layoutNode(root);
|
||||
buttons.clear();
|
||||
sliders.clear();
|
||||
textViews.clear();
|
||||
textFields.clear();
|
||||
collectButtonsAndSliders(root);
|
||||
|
||||
nodeActiveAnims.clear();
|
||||
|
||||
for (auto& b : buttons) {
|
||||
b->buildMesh();
|
||||
}
|
||||
for (auto& s : sliders) {
|
||||
s->buildTrackMesh();
|
||||
s->buildKnobMesh();
|
||||
}
|
||||
}
|
||||
|
||||
void UiManager::loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
|
||||
|
||||
std::shared_ptr<UiNode> newRoot = loadUiFromFile(path, renderer, zipFile);
|
||||
replaceRoot(newRoot);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void UiManager::layoutNode(const std::shared_ptr<UiNode>& node) {
|
||||
for (auto& child : node->children) {
|
||||
child->rect.x += node->rect.x;
|
||||
@ -406,7 +476,10 @@ namespace ZL {
|
||||
if (node->textView) {
|
||||
textViews.push_back(node->textView);
|
||||
}
|
||||
for (auto& c : node->children) collectButtonsAndSliders(c); // ìîæíî ïåðåèìåíîâàòü â collectControls
|
||||
if (node->textField) {
|
||||
textFields.push_back(node->textField);
|
||||
}
|
||||
for (auto& c : node->children) collectButtonsAndSliders(c);
|
||||
}
|
||||
|
||||
bool UiManager::setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb) {
|
||||
@ -476,13 +549,37 @@ namespace ZL {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UiManager::pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
|
||||
std::shared_ptr<UiTextField> UiManager::findTextField(const std::string& name) {
|
||||
for (auto& tf : textFields) if (tf->name == name) return tf;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool UiManager::setTextFieldCallback(const std::string& name, std::function<void(const std::string&, const std::string&)> cb) {
|
||||
auto tf = findTextField(name);
|
||||
if (!tf) {
|
||||
std::cerr << "UiManager: setTextFieldCallback failed, textfield not found: " << name << std::endl;
|
||||
return false;
|
||||
}
|
||||
tf->onTextChanged = std::move(cb);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string UiManager::getTextFieldValue(const std::string& name) {
|
||||
auto tf = findTextField(name);
|
||||
if (!tf) return "";
|
||||
return tf->text;
|
||||
}
|
||||
|
||||
bool UiManager::pushMenuFromSavedRoot(std::shared_ptr<UiNode> newRoot)
|
||||
{
|
||||
MenuState prev;
|
||||
prev.root = root;
|
||||
prev.buttons = buttons;
|
||||
prev.sliders = sliders;
|
||||
prev.textFields = textFields;
|
||||
prev.pressedButton = pressedButton;
|
||||
prev.pressedSlider = pressedSlider;
|
||||
prev.focusedTextField = focusedTextField;
|
||||
prev.path = "";
|
||||
|
||||
prev.animCallbacks = animCallbacks;
|
||||
@ -490,6 +587,7 @@ namespace ZL {
|
||||
try {
|
||||
nodeActiveAnims.clear();
|
||||
animCallbacks.clear();
|
||||
focusedTextField = nullptr;
|
||||
for (auto& b : buttons) {
|
||||
if (b) {
|
||||
b->animOffsetX = 0.0f;
|
||||
@ -499,17 +597,22 @@ namespace ZL {
|
||||
}
|
||||
}
|
||||
|
||||
loadFromFile(path, renderer, zipFile);
|
||||
replaceRoot(newRoot);
|
||||
menuStack.push_back(std::move(prev));
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "UiManager: pushMenuFromFile failed to load " << path << " : " << e.what() << std::endl;
|
||||
std::cerr << "UiManager: pushMenuFromFile failed to load from root : " << e.what() << std::endl;
|
||||
animCallbacks = prev.animCallbacks;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool UiManager::pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
|
||||
auto newRoot = loadUiFromFile(path, renderer, zipFile);
|
||||
return pushMenuFromSavedRoot(newRoot);
|
||||
}
|
||||
|
||||
bool UiManager::popMenu() {
|
||||
if (menuStack.empty()) {
|
||||
std::cerr << "UiManager: popMenu called but menu stack is empty" << std::endl;
|
||||
@ -523,8 +626,10 @@ namespace ZL {
|
||||
root = s.root;
|
||||
buttons = s.buttons;
|
||||
sliders = s.sliders;
|
||||
textFields = s.textFields;
|
||||
pressedButton = s.pressedButton;
|
||||
pressedSlider = s.pressedSlider;
|
||||
focusedTextField = s.focusedTextField;
|
||||
|
||||
animCallbacks = s.animCallbacks;
|
||||
|
||||
@ -565,6 +670,9 @@ namespace ZL {
|
||||
for (const auto& tv : textViews) {
|
||||
tv->draw(renderer);
|
||||
}
|
||||
for (const auto& tf : textFields) {
|
||||
tf->draw(renderer);
|
||||
}
|
||||
|
||||
renderer.PopMatrix();
|
||||
renderer.PopProjectionMatrix();
|
||||
@ -785,6 +893,16 @@ namespace ZL {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& tf : textFields) {
|
||||
if (tf->rect.contains((float)x, (float)y)) {
|
||||
focusedTextField = tf;
|
||||
tf->focused = true;
|
||||
}
|
||||
else {
|
||||
tf->focused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UiManager::onMouseUp(int x, int y) {
|
||||
@ -806,6 +924,30 @@ namespace ZL {
|
||||
}
|
||||
}
|
||||
|
||||
void UiManager::onKeyPress(unsigned char key) {
|
||||
if (!focusedTextField) return;
|
||||
|
||||
if (key >= 32 && key <= 126) {
|
||||
if (focusedTextField->text.length() < (size_t)focusedTextField->maxLength) {
|
||||
focusedTextField->text += key;
|
||||
if (focusedTextField->onTextChanged) {
|
||||
focusedTextField->onTextChanged(focusedTextField->name, focusedTextField->text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UiManager::onKeyBackspace() {
|
||||
if (!focusedTextField) return;
|
||||
|
||||
if (!focusedTextField->text.empty()) {
|
||||
focusedTextField->text.pop_back();
|
||||
if (focusedTextField->onTextChanged) {
|
||||
focusedTextField->onTextChanged(focusedTextField->name, focusedTextField->text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<UiButton> UiManager::findButton(const std::string& name) {
|
||||
for (auto& b : buttons) if (b->name == name) return b;
|
||||
return nullptr;
|
||||
|
||||
@ -90,6 +90,26 @@ namespace ZL {
|
||||
}
|
||||
};
|
||||
|
||||
struct UiTextField {
|
||||
std::string name;
|
||||
UiRect rect;
|
||||
std::string text = "";
|
||||
std::string placeholder = "";
|
||||
std::string fontPath = "resources/fonts/DroidSans.ttf";
|
||||
int fontSize = 32;
|
||||
std::array<float, 4> color = { 1.f, 1.f, 1.f, 1.f };
|
||||
std::array<float, 4> placeholderColor = { 0.5f, 0.5f, 0.5f, 1.f };
|
||||
std::array<float, 4> backgroundColor = { 0.2f, 0.2f, 0.2f, 1.f };
|
||||
std::array<float, 4> borderColor = { 0.5f, 0.5f, 0.5f, 1.f };
|
||||
int maxLength = 256;
|
||||
bool focused = false;
|
||||
|
||||
std::unique_ptr<TextRenderer> textRenderer;
|
||||
std::function<void(const std::string&, const std::string&)> onTextChanged;
|
||||
|
||||
void draw(Renderer& renderer) const;
|
||||
};
|
||||
|
||||
struct UiNode {
|
||||
std::string type;
|
||||
UiRect rect;
|
||||
@ -98,6 +118,7 @@ namespace ZL {
|
||||
std::shared_ptr<UiButton> button;
|
||||
std::shared_ptr<UiSlider> slider;
|
||||
std::shared_ptr<UiTextView> textView;
|
||||
std::shared_ptr<UiTextField> textField;
|
||||
std::string orientation = "vertical";
|
||||
float spacing = 0.0f;
|
||||
|
||||
@ -115,10 +136,15 @@ namespace ZL {
|
||||
std::map<std::string, AnimSequence> animations;
|
||||
};
|
||||
|
||||
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile);
|
||||
std::shared_ptr<UiNode> loadUiFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
|
||||
|
||||
|
||||
class UiManager {
|
||||
public:
|
||||
UiManager() = default;
|
||||
|
||||
void replaceRoot(std::shared_ptr<UiNode> newRoot);
|
||||
void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
|
||||
|
||||
void draw(Renderer& renderer);
|
||||
@ -126,9 +152,11 @@ namespace ZL {
|
||||
void onMouseMove(int x, int y);
|
||||
void onMouseDown(int x, int y);
|
||||
void onMouseUp(int x, int y);
|
||||
void onKeyPress(unsigned char key);
|
||||
void onKeyBackspace();
|
||||
|
||||
bool isUiInteraction() const {
|
||||
return pressedButton != nullptr || pressedSlider != nullptr;
|
||||
return pressedButton != nullptr || pressedSlider != nullptr || focusedTextField != nullptr;
|
||||
}
|
||||
|
||||
void stopAllAnimations() {
|
||||
@ -158,7 +186,12 @@ namespace ZL {
|
||||
std::shared_ptr<UiTextView> findTextView(const std::string& name);
|
||||
bool setText(const std::string& name, const std::string& newText);
|
||||
|
||||
std::shared_ptr<UiTextField> findTextField(const std::string& name);
|
||||
bool setTextFieldCallback(const std::string& name, std::function<void(const std::string&, const std::string&)> cb);
|
||||
std::string getTextFieldValue(const std::string& name);
|
||||
|
||||
bool pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
|
||||
bool pushMenuFromSavedRoot(std::shared_ptr<UiNode> newRoot);
|
||||
bool popMenu();
|
||||
void clearMenuStack();
|
||||
|
||||
@ -169,7 +202,6 @@ namespace ZL {
|
||||
bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function<void()> cb);
|
||||
|
||||
private:
|
||||
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile);
|
||||
void layoutNode(const std::shared_ptr<UiNode>& node);
|
||||
void collectButtonsAndSliders(const std::shared_ptr<UiNode>& node);
|
||||
|
||||
@ -200,19 +232,23 @@ namespace ZL {
|
||||
std::vector<std::shared_ptr<UiButton>> buttons;
|
||||
std::vector<std::shared_ptr<UiSlider>> sliders;
|
||||
std::vector<std::shared_ptr<UiTextView>> textViews;
|
||||
std::vector<std::shared_ptr<UiTextField>> textFields;
|
||||
|
||||
std::map<std::shared_ptr<UiNode>, std::vector<ActiveAnim>> nodeActiveAnims;
|
||||
std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks; // key: (nodeName, animName)
|
||||
|
||||
std::shared_ptr<UiButton> pressedButton;
|
||||
std::shared_ptr<UiSlider> pressedSlider;
|
||||
std::shared_ptr<UiTextField> focusedTextField;
|
||||
|
||||
struct MenuState {
|
||||
std::shared_ptr<UiNode> root;
|
||||
std::vector<std::shared_ptr<UiButton>> buttons;
|
||||
std::vector<std::shared_ptr<UiSlider>> sliders;
|
||||
std::vector<std::shared_ptr<UiTextField>> textFields;
|
||||
std::shared_ptr<UiButton> pressedButton;
|
||||
std::shared_ptr<UiSlider> pressedSlider;
|
||||
std::shared_ptr<UiTextField> focusedTextField;
|
||||
std::string path;
|
||||
std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks;
|
||||
};
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
#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) {
|
||||
if (discreteMag > 0.01f)
|
||||
|
||||
@ -9,6 +9,9 @@
|
||||
using std::min;
|
||||
using std::max;
|
||||
|
||||
constexpr auto NET_SECRET = "880b3713b9ff3e7a94b2712d54679e1f";
|
||||
#define ENABLE_NETWORK_CHECKSUM
|
||||
|
||||
constexpr float ANGULAR_ACCEL = 0.005f * 1000.0f;
|
||||
constexpr float SHIP_ACCEL = 1.0f * 1000.0f;
|
||||
constexpr float ROTATION_SENSITIVITY = 0.002f;
|
||||
@ -20,9 +23,11 @@ constexpr float PLANET_MAX_ANGULAR_VELOCITY = 10.f;
|
||||
constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f;
|
||||
|
||||
constexpr long long SERVER_DELAY = 0; //ms
|
||||
constexpr long long CLIENT_DELAY = 1000; //ms
|
||||
constexpr long long CLIENT_DELAY = 500; //ms
|
||||
constexpr long long CUTOFF_TIME = 5000; //ms
|
||||
|
||||
uint32_t fnv1a_hash(const std::string& data);
|
||||
|
||||
struct ClientState {
|
||||
int id = 0;
|
||||
Eigen::Vector3f position = { 0, 0, 45000.0f };
|
||||
|
||||
@ -1,20 +1,461 @@
|
||||
#include "LocalClient.h"
|
||||
#include <iostream>
|
||||
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#define _USE_MATH_DEFINES
|
||||
#include <math.h>
|
||||
|
||||
namespace ZL {
|
||||
|
||||
void LocalClient::Connect(const std::string& host, uint16_t port) {
|
||||
generateBoxes();
|
||||
initializeNPCs();
|
||||
lastUpdateMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
void LocalClient::generateBoxes() {
|
||||
serverBoxes.clear();
|
||||
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
|
||||
const float MIN_COORD = -100.0f;
|
||||
const float MAX_COORD = 100.0f;
|
||||
const float MIN_DISTANCE = 3.0f;
|
||||
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
|
||||
const int MAX_ATTEMPTS = 1000;
|
||||
|
||||
std::uniform_real_distribution<> posDistrib(MIN_COORD, MAX_COORD);
|
||||
std::uniform_real_distribution<> angleDistrib(0.0, M_PI * 2.0);
|
||||
|
||||
for (int i = 0; i < 50; i++) {
|
||||
bool accepted = false;
|
||||
int attempts = 0;
|
||||
|
||||
while (!accepted && attempts < MAX_ATTEMPTS) {
|
||||
LocalServerBox box;
|
||||
box.position = Eigen::Vector3f(
|
||||
(float)posDistrib(gen),
|
||||
(float)posDistrib(gen),
|
||||
(float)posDistrib(gen)
|
||||
);
|
||||
|
||||
accepted = true;
|
||||
for (const auto& existingBox : serverBoxes) {
|
||||
Eigen::Vector3f diff = box.position - existingBox.position;
|
||||
if (diff.squaredNorm() < MIN_DISTANCE_SQUARED) {
|
||||
accepted = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (accepted) {
|
||||
float randomAngle = (float)angleDistrib(gen);
|
||||
Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized();
|
||||
box.rotation = Eigen::AngleAxisf(randomAngle, axis).toRotationMatrix();
|
||||
serverBoxes.push_back(box);
|
||||
}
|
||||
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "LocalClient: Generated " << serverBoxes.size() << " boxes\n";
|
||||
}
|
||||
|
||||
Eigen::Vector3f LocalClient::generateRandomPosition() {
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_real_distribution<> distrib(-500.0, 500.0);
|
||||
|
||||
return Eigen::Vector3f(
|
||||
(float)distrib(gen),
|
||||
(float)distrib(gen),
|
||||
(float)distrib(gen) + 45000.0f
|
||||
);
|
||||
}
|
||||
|
||||
void LocalClient::initializeNPCs() {
|
||||
npcs.clear();
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
LocalNPC npc;
|
||||
npc.id = 100 + i;
|
||||
npc.currentState.id = npc.id;
|
||||
npc.currentState.position = generateRandomPosition();
|
||||
npc.currentState.rotation = Eigen::Matrix3f::Identity();
|
||||
npc.currentState.velocity = 0.0f;
|
||||
npc.currentState.selectedVelocity = 0;
|
||||
npc.currentState.discreteMag = 0.0f;
|
||||
npc.currentState.discreteAngle = -1;
|
||||
npc.currentState.currentAngularVelocity = Eigen::Vector3f::Zero();
|
||||
|
||||
npc.targetPosition = generateRandomPosition();
|
||||
npc.lastStateUpdateMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
npc.destroyed = false;
|
||||
|
||||
npc.stateHistory.add_state(npc.currentState);
|
||||
npcs.push_back(npc);
|
||||
}
|
||||
}
|
||||
|
||||
void LocalClient::updateNPCs() {
|
||||
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
|
||||
for (auto& npc : npcs) {
|
||||
if (npc.destroyed) continue;
|
||||
|
||||
uint64_t deltaMs = now_ms - npc.lastStateUpdateMs;
|
||||
if (deltaMs == 0) {
|
||||
npc.lastStateUpdateMs = now_ms;
|
||||
continue;
|
||||
}
|
||||
npc.lastStateUpdateMs = now_ms;
|
||||
|
||||
Eigen::Vector3f toTarget = npc.targetPosition - npc.currentState.position;
|
||||
float distance = toTarget.norm();
|
||||
|
||||
const float ARRIVAL_THRESHOLD = 100.0f;
|
||||
|
||||
if (distance < ARRIVAL_THRESHOLD) {
|
||||
npc.targetPosition = generateRandomPosition();
|
||||
toTarget = npc.targetPosition - npc.currentState.position;
|
||||
distance = toTarget.norm();
|
||||
}
|
||||
|
||||
Eigen::Vector3f forwardWorld = -npc.currentState.rotation.col(2);
|
||||
forwardWorld.normalize();
|
||||
|
||||
Eigen::Vector3f desiredDir = (distance > 0.001f) ? toTarget.normalized() : Eigen::Vector3f::UnitZ();
|
||||
float dot = forwardWorld.dot(desiredDir);
|
||||
float angleErrorRad = std::acos(std::clamp(dot, -1.0f, 1.0f));
|
||||
|
||||
const float ALIGN_TOLERANCE = 0.15f;
|
||||
|
||||
const float HYSTERESIS_FACTOR = 1.35f;
|
||||
const float SOFT_THRUST_ANGLE = ALIGN_TOLERANCE * HYSTERESIS_FACTOR;
|
||||
|
||||
if (angleErrorRad < ALIGN_TOLERANCE) {
|
||||
npc.currentState.selectedVelocity = 1;
|
||||
npc.currentState.discreteMag = 0.0f;
|
||||
}
|
||||
else if (angleErrorRad < SOFT_THRUST_ANGLE) {
|
||||
npc.currentState.selectedVelocity = 1;
|
||||
npc.currentState.discreteMag = std::min(0.50f, (angleErrorRad - ALIGN_TOLERANCE) * 10.0f);
|
||||
}
|
||||
else {
|
||||
npc.currentState.selectedVelocity = 0;
|
||||
|
||||
Eigen::Vector3f localDesired = npc.currentState.rotation.transpose() * desiredDir;
|
||||
float dx = localDesired.x();
|
||||
float dy = localDesired.y();
|
||||
float dz = localDesired.z();
|
||||
|
||||
float turnX = dy;
|
||||
float turnY = -dx;
|
||||
float turnLen = std::sqrt(turnX * turnX + turnY * turnY);
|
||||
|
||||
if (turnLen > 0.0001f) {
|
||||
turnX /= turnLen;
|
||||
turnY /= turnLen;
|
||||
|
||||
float rad = std::atan2(turnX, turnY);
|
||||
int angleDeg = static_cast<int>(std::round(rad * 180.0f / M_PI));
|
||||
if (angleDeg < 0) angleDeg += 360;
|
||||
|
||||
npc.currentState.discreteAngle = angleDeg;
|
||||
npc.currentState.discreteMag = std::min(1.0f, angleErrorRad * 2.2f);
|
||||
}
|
||||
else if (angleErrorRad > 0.1f) {
|
||||
npc.currentState.discreteAngle = 0;
|
||||
npc.currentState.discreteMag = 1.0f;
|
||||
}
|
||||
else {
|
||||
npc.currentState.discreteMag = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
npc.currentState.simulate_physics(static_cast<size_t>(deltaMs));
|
||||
npc.currentState.lastUpdateServerTime = std::chrono::system_clock::time_point(
|
||||
std::chrono::milliseconds(now_ms));
|
||||
npc.stateHistory.add_state(npc.currentState);
|
||||
}
|
||||
}
|
||||
|
||||
void LocalClient::Poll() {
|
||||
updatePhysics();
|
||||
updateNPCs();
|
||||
checkCollisions();
|
||||
}
|
||||
|
||||
void LocalClient::updatePhysics() {
|
||||
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
|
||||
if (lastUpdateMs == 0) {
|
||||
lastUpdateMs = now_ms;
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t deltaMs = now_ms - lastUpdateMs;
|
||||
float dt = deltaMs / 1000.0f;
|
||||
lastUpdateMs = now_ms;
|
||||
|
||||
std::vector<int> indicesToRemove;
|
||||
|
||||
for (size_t i = 0; i < projectiles.size(); ++i) {
|
||||
auto& pr = projectiles[i];
|
||||
pr.pos += pr.vel * dt;
|
||||
|
||||
if (now_ms > pr.spawnMs + static_cast<uint64_t>(pr.lifeMs)) {
|
||||
indicesToRemove.push_back(static_cast<int>(i));
|
||||
}
|
||||
}
|
||||
|
||||
if (!indicesToRemove.empty()) {
|
||||
std::sort(indicesToRemove.rbegin(), indicesToRemove.rend());
|
||||
for (int idx : indicesToRemove) {
|
||||
if (idx >= 0 && idx < (int)projectiles.size()) {
|
||||
projectiles.erase(projectiles.begin() + idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LocalClient::checkCollisions() {
|
||||
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
|
||||
const float projectileHitRadius = 1.5f;
|
||||
const float boxCollisionRadius = 2.0f;
|
||||
const float shipCollisionRadius = 15.0f;
|
||||
const float npcCollisionRadius = 5.0f;
|
||||
|
||||
std::vector<std::pair<size_t, size_t>> boxProjectileCollisions;
|
||||
|
||||
for (size_t bi = 0; bi < serverBoxes.size(); ++bi) {
|
||||
if (serverBoxes[bi].destroyed) continue;
|
||||
|
||||
Eigen::Vector3f boxWorld = serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
|
||||
|
||||
for (size_t pi = 0; pi < projectiles.size(); ++pi) {
|
||||
const auto& pr = projectiles[pi];
|
||||
Eigen::Vector3f diff = pr.pos - boxWorld;
|
||||
float thresh = boxCollisionRadius + projectileHitRadius;
|
||||
|
||||
if (diff.squaredNorm() <= thresh * thresh) {
|
||||
boxProjectileCollisions.push_back({ bi, pi });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<int> projIndicesToRemove;
|
||||
for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) {
|
||||
if (!serverBoxes[boxIdx].destroyed) {
|
||||
serverBoxes[boxIdx].destroyed = true;
|
||||
|
||||
Eigen::Vector3f boxWorld = serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
|
||||
|
||||
BoxDestroyedInfo destruction;
|
||||
destruction.boxIndex = static_cast<int>(boxIdx);
|
||||
destruction.serverTime = now_ms;
|
||||
destruction.position = boxWorld;
|
||||
destruction.destroyedBy = projectiles[projIdx].shooterId;
|
||||
|
||||
pendingBoxDestructions.push_back(destruction);
|
||||
|
||||
std::cout << "LocalClient: Box " << boxIdx << " destroyed by projectile from player "
|
||||
<< projectiles[projIdx].shooterId << std::endl;
|
||||
|
||||
if (std::find(projIndicesToRemove.begin(), projIndicesToRemove.end(), (int)projIdx)
|
||||
== projIndicesToRemove.end()) {
|
||||
projIndicesToRemove.push_back(static_cast<int>(projIdx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::pair<size_t, size_t>> npcProjectileCollisions;
|
||||
|
||||
for (size_t ni = 0; ni < npcs.size(); ++ni) {
|
||||
if (npcs[ni].destroyed) continue;
|
||||
|
||||
for (size_t pi = 0; pi < projectiles.size(); ++pi) {
|
||||
const auto& pr = projectiles[pi];
|
||||
Eigen::Vector3f diff = pr.pos - npcs[ni].currentState.position;
|
||||
float thresh = npcCollisionRadius + projectileHitRadius;
|
||||
|
||||
if (diff.squaredNorm() <= thresh * thresh) {
|
||||
npcProjectileCollisions.push_back({ ni, pi });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [npcIdx, projIdx] : npcProjectileCollisions) {
|
||||
if (!npcs[npcIdx].destroyed) {
|
||||
npcs[npcIdx].destroyed = true;
|
||||
|
||||
DeathInfo death;
|
||||
death.targetId = npcs[npcIdx].id;
|
||||
death.serverTime = now_ms;
|
||||
death.position = npcs[npcIdx].currentState.position;
|
||||
death.killerId = projectiles[projIdx].shooterId;
|
||||
|
||||
pendingDeaths.push_back(death);
|
||||
|
||||
std::cout << "LocalClient: NPC " << npcs[npcIdx].id << " destroyed by projectile from player "
|
||||
<< projectiles[projIdx].shooterId << " at position ("
|
||||
<< npcs[npcIdx].currentState.position.x() << ", "
|
||||
<< npcs[npcIdx].currentState.position.y() << ", "
|
||||
<< npcs[npcIdx].currentState.position.z() << ")" << std::endl;
|
||||
|
||||
if (std::find(projIndicesToRemove.begin(), projIndicesToRemove.end(), (int)projIdx)
|
||||
== projIndicesToRemove.end()) {
|
||||
projIndicesToRemove.push_back(static_cast<int>(projIdx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!projIndicesToRemove.empty()) {
|
||||
std::sort(projIndicesToRemove.rbegin(), projIndicesToRemove.rend());
|
||||
for (int idx : projIndicesToRemove) {
|
||||
if (idx >= 0 && idx < (int)projectiles.size()) {
|
||||
projectiles.erase(projectiles.begin() + idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasLocalPlayerState) {
|
||||
for (size_t bi = 0; bi < serverBoxes.size(); ++bi) {
|
||||
if (serverBoxes[bi].destroyed) continue;
|
||||
|
||||
Eigen::Vector3f boxWorld = serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
|
||||
Eigen::Vector3f diff = localPlayerState.position - boxWorld;
|
||||
float thresh = shipCollisionRadius + boxCollisionRadius;
|
||||
|
||||
if (diff.squaredNorm() <= thresh * thresh) {
|
||||
serverBoxes[bi].destroyed = true;
|
||||
|
||||
BoxDestroyedInfo destruction;
|
||||
destruction.boxIndex = static_cast<int>(bi);
|
||||
destruction.serverTime = now_ms;
|
||||
destruction.position = boxWorld;
|
||||
destruction.destroyedBy = GetClientId();
|
||||
|
||||
pendingBoxDestructions.push_back(destruction);
|
||||
|
||||
std::cout << "LocalClient: Box " << bi << " destroyed by ship collision with player "
|
||||
<< GetClientId() << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LocalClient::Send(const std::string& message) {
|
||||
auto parts = [](const std::string& s, char delimiter) {
|
||||
std::vector<std::string> tokens;
|
||||
std::string token;
|
||||
std::istringstream tokenStream(s);
|
||||
while (std::getline(tokenStream, token, delimiter)) {
|
||||
tokens.push_back(token);
|
||||
}
|
||||
return tokens;
|
||||
}(message, ':');
|
||||
|
||||
if (parts.empty()) return;
|
||||
|
||||
std::string type = parts[0];
|
||||
|
||||
if (type == "FIRE") {
|
||||
if (parts.size() < 10) return;
|
||||
|
||||
uint64_t clientTime = std::stoull(parts[1]);
|
||||
Eigen::Vector3f pos{
|
||||
std::stof(parts[2]), std::stof(parts[3]), std::stof(parts[4])
|
||||
};
|
||||
Eigen::Quaternionf dir(
|
||||
std::stof(parts[5]), std::stof(parts[6]), std::stof(parts[7]), std::stof(parts[8])
|
||||
);
|
||||
float velocity = std::stof(parts[9]);
|
||||
|
||||
int shotCount = 2;
|
||||
if (parts.size() >= 11) {
|
||||
try { shotCount = std::stoi(parts[10]); }
|
||||
catch (...) { shotCount = 2; }
|
||||
}
|
||||
|
||||
const std::vector<Eigen::Vector3f> localOffsets = {
|
||||
Eigen::Vector3f(-1.5f, 0.9f - 6.f, 5.0f),
|
||||
Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
|
||||
};
|
||||
|
||||
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
|
||||
for (int i = 0; i < std::min(shotCount, (int)localOffsets.size()); ++i) {
|
||||
LocalProjectile pr;
|
||||
pr.shooterId = GetClientId();
|
||||
pr.spawnMs = now_ms;
|
||||
Eigen::Vector3f shotPos = pos + dir.toRotationMatrix() * localOffsets[i];
|
||||
pr.pos = shotPos;
|
||||
Eigen::Vector3f localForward(0.0f, 0.0f, -1.0f);
|
||||
Eigen::Vector3f worldForward = dir.toRotationMatrix() * localForward;
|
||||
float len = worldForward.norm();
|
||||
if (len > 1e-6f) worldForward /= len;
|
||||
pr.vel = worldForward * velocity;
|
||||
pr.lifeMs = 5000.0f;
|
||||
projectiles.push_back(pr);
|
||||
|
||||
ProjectileInfo pinfo;
|
||||
pinfo.shooterId = pr.shooterId;
|
||||
pinfo.clientTime = clientTime;
|
||||
pinfo.position = pr.pos;
|
||||
pinfo.rotation = dir.toRotationMatrix();
|
||||
pinfo.velocity = velocity;
|
||||
pendingProjectiles.push_back(pinfo);
|
||||
|
||||
std::cout << "LocalClient: Created projectile at pos (" << shotPos.x() << ", "
|
||||
<< shotPos.y() << ", " << shotPos.z() << ") vel (" << pr.vel.x() << ", "
|
||||
<< pr.vel.y() << ", " << pr.vel.z() << ")" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ProjectileInfo> LocalClient::getPendingProjectiles() {
|
||||
return {};
|
||||
auto result = pendingProjectiles;
|
||||
pendingProjectiles.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<DeathInfo> LocalClient::getPendingDeaths() {
|
||||
auto result = pendingDeaths;
|
||||
pendingDeaths.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unordered_map<int, ClientStateInterval> LocalClient::getRemotePlayers() {
|
||||
std::unordered_map<int, ClientStateInterval> result;
|
||||
for (const auto& npc : npcs) {
|
||||
if (!npc.destroyed) {
|
||||
result[npc.id] = npc.stateHistory;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> LocalClient::getServerBoxes() {
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> result;
|
||||
for (const auto& box : serverBoxes) {
|
||||
result.push_back({ box.position, box.rotation });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<BoxDestroyedInfo> LocalClient::getPendingBoxDestructions() {
|
||||
auto result = pendingBoxDestructions;
|
||||
pendingBoxDestructions.clear();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -2,41 +2,86 @@
|
||||
|
||||
#include "NetworkInterface.h"
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
#include <Eigen/Dense>
|
||||
#include <chrono>
|
||||
#include <random>
|
||||
|
||||
namespace ZL {
|
||||
class LocalClient : public INetworkClient {
|
||||
private:
|
||||
std::queue<std::string> messageQueue;
|
||||
public:
|
||||
void Connect(const std::string& host, uint16_t port) override;
|
||||
|
||||
void Poll() override;
|
||||
struct LocalServerBox {
|
||||
Eigen::Vector3f position;
|
||||
Eigen::Matrix3f rotation;
|
||||
float collisionRadius = 2.0f;
|
||||
bool destroyed = false;
|
||||
};
|
||||
|
||||
void Send(const std::string& message) override;
|
||||
struct LocalProjectile {
|
||||
int shooterId = -1;
|
||||
uint64_t spawnMs = 0;
|
||||
Eigen::Vector3f pos;
|
||||
Eigen::Vector3f vel;
|
||||
float lifeMs = 5000.0f;
|
||||
};
|
||||
|
||||
bool IsConnected() const override { return true; }
|
||||
int GetClientId() const override { return 1; }
|
||||
std::vector<ProjectileInfo> getPendingProjectiles() override;
|
||||
struct LocalNPC {
|
||||
int id = -1;
|
||||
ClientState currentState;
|
||||
ClientStateInterval stateHistory;
|
||||
Eigen::Vector3f targetPosition;
|
||||
uint64_t lastStateUpdateMs = 0;
|
||||
bool destroyed = false;
|
||||
};
|
||||
|
||||
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override {
|
||||
return std::unordered_map<int, ClientStateInterval>();
|
||||
class LocalClient : public INetworkClient {
|
||||
private:
|
||||
std::queue<std::string> messageQueue;
|
||||
std::vector<LocalServerBox> serverBoxes;
|
||||
std::vector<LocalProjectile> projectiles;
|
||||
std::vector<ProjectileInfo> pendingProjectiles;
|
||||
std::vector<DeathInfo> pendingDeaths;
|
||||
std::vector<BoxDestroyedInfo> pendingBoxDestructions;
|
||||
std::vector<int> pendingRespawns;
|
||||
|
||||
uint64_t lastUpdateMs = 0;
|
||||
ClientState localPlayerState;
|
||||
bool hasLocalPlayerState = false;
|
||||
|
||||
std::vector<LocalNPC> npcs;
|
||||
|
||||
void updatePhysics();
|
||||
void checkCollisions();
|
||||
void generateBoxes();
|
||||
void initializeNPCs();
|
||||
void updateNPCs();
|
||||
Eigen::Vector3f generateRandomPosition();
|
||||
|
||||
public:
|
||||
void Connect(const std::string& host, uint16_t port) override;
|
||||
|
||||
void Poll() override;
|
||||
|
||||
void Send(const std::string& message) override;
|
||||
|
||||
bool IsConnected() const override { return true; }
|
||||
int GetClientId() const override { return 1; }
|
||||
std::vector<ProjectileInfo> getPendingProjectiles() override;
|
||||
|
||||
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override;
|
||||
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override;
|
||||
|
||||
std::vector<DeathInfo> getPendingDeaths() override;
|
||||
|
||||
std::vector<int> getPendingRespawns() override {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override {
|
||||
return {};
|
||||
}
|
||||
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
|
||||
|
||||
std::vector<DeathInfo> getPendingDeaths() override {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<int> getPendingRespawns() override {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
};
|
||||
void setLocalPlayerState(const ClientState& state) {
|
||||
localPlayerState = state;
|
||||
hasLocalPlayerState = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -47,5 +47,7 @@ namespace ZL {
|
||||
virtual std::vector<int> getPendingRespawns() = 0;
|
||||
virtual int GetClientId() const { return -1; }
|
||||
virtual std::vector<BoxDestroyedInfo> getPendingBoxDestructions() = 0;
|
||||
virtual int64_t getTimeOffset() const { return 0; }
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,17 +4,6 @@
|
||||
#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 WebSocketClient::Connect(const std::string& host, uint16_t port) {
|
||||
@ -55,48 +44,20 @@ namespace ZL {
|
||||
|
||||
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));
|
||||
}
|
||||
}*/
|
||||
|
||||
// Безопасно кладем в очередь для главного потока
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
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() {
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
|
||||
while (!messageQueue.empty()) {
|
||||
|
||||
/*
|
||||
auto nowTime = std::chrono::system_clock::now();
|
||||
|
||||
//Apply server delay:
|
||||
@ -104,229 +65,36 @@ namespace ZL {
|
||||
|
||||
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
nowTime.time_since_epoch()
|
||||
).count();
|
||||
|
||||
|
||||
|
||||
).count();*/
|
||||
std::string msg = messageQueue.front();
|
||||
messageQueue.pop();
|
||||
|
||||
// Обработка списка коробок от сервера
|
||||
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;
|
||||
}
|
||||
|
||||
HandlePollMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void WebSocketClient::Send(const std::string& message) {
|
||||
if (!connected) return;
|
||||
|
||||
auto ss = std::make_shared<std::string>(message);
|
||||
std::string finalMessage = SignMessage(message);
|
||||
/*
|
||||
#ifdef ENABLE_NETWORK_CHECKSUM
|
||||
// Вычисляем хеш. Для примера используем std::hash,
|
||||
// но в продакшене лучше взять быструю реализацию типа MurmurHash3.
|
||||
size_t hashValue = std::hash<std::string>{}(message + NET_SECRET);
|
||||
|
||||
// Преобразуем хеш в hex-строку для передачи
|
||||
std::stringstream ss_hash;
|
||||
ss_hash << std::hex << hashValue;
|
||||
|
||||
// Добавляем хеш в конец сообщения через разделитель
|
||||
// Например: "UPD:12345:pos...#hash:a1b2c3d4"
|
||||
finalMessage += "#hash:" + ss_hash.str();
|
||||
#endif
|
||||
*/
|
||||
auto ss = std::make_shared<std::string>(finalMessage);
|
||||
|
||||
std::lock_guard<std::mutex> lock(writeMutex_);
|
||||
writeQueue_.push(ss);
|
||||
@ -364,4 +132,4 @@ namespace ZL {
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
#ifdef NETWORK
|
||||
|
||||
#include "NetworkInterface.h"
|
||||
#include "WebSocketClientBase.h"
|
||||
#include <queue>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
namespace ZL {
|
||||
|
||||
class WebSocketClient : public INetworkClient {
|
||||
class WebSocketClient : public WebSocketClientBase {
|
||||
private:
|
||||
// Переиспользуем io_context из TaskManager
|
||||
boost::asio::io_context& ioc_;
|
||||
@ -28,26 +28,7 @@ namespace ZL {
|
||||
std::mutex writeMutex_; // Отдельный мьютекс для очереди записи
|
||||
|
||||
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 processIncomingMessage(const std::string& msg);
|
||||
@ -63,22 +44,6 @@ namespace ZL {
|
||||
void doWrite();
|
||||
|
||||
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
|
||||
|
||||
294
src/network/WebSocketClientBase.cpp
Normal file
294
src/network/WebSocketClientBase.cpp
Normal file
@ -0,0 +1,294 @@
|
||||
#ifdef NETWORK
|
||||
|
||||
#include "WebSocketClientBase.h"
|
||||
#include <iostream>
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
// Вспомогательный split
|
||||
std::vector<std::string> split(const std::string& s, char delimiter) {
|
||||
std::vector<std::string> tokens;
|
||||
std::string token;
|
||||
std::istringstream tokenStream(s);
|
||||
while (std::getline(tokenStream, token, delimiter)) {
|
||||
tokens.push_back(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
namespace ZL {
|
||||
|
||||
void WebSocketClientBase::HandlePollMessage(const std::string& msg) {
|
||||
auto parts = split(msg, ':');
|
||||
if (parts.empty()) return;
|
||||
|
||||
if (parts[0] == "ID") {
|
||||
std::cout << "ID Message Received:" << msg << std::endl;
|
||||
clientId = std::stoi(parts[1]);
|
||||
if (parts.size() >= 3) {
|
||||
std::cout << "ID Message Received step 2" << std::endl;
|
||||
uint64_t serverTime = std::stoull(parts[2]);
|
||||
uint64_t localTime = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
|
||||
std::cout << "ID Message Received localTime = " << localTime << std::endl;
|
||||
std::cout << "ID Message Received serverTime = " << serverTime << std::endl;
|
||||
|
||||
// Вычисляем смещение
|
||||
timeOffset = static_cast<int64_t>(serverTime) - static_cast<int64_t>(localTime);
|
||||
|
||||
std::cout << "Time synchronized. Offset: " << timeOffset << " ms" << std::endl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка списка коробок от сервера
|
||||
if (msg.rfind("BOXES:", 0) == 0) {
|
||||
std::string payload = msg.substr(6); // после "BOXES:"
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> parsedBoxes;
|
||||
if (!payload.empty()) {
|
||||
auto items = split(payload, '|');
|
||||
for (auto& item : items) {
|
||||
if (item.empty()) return;
|
||||
auto parts = split(item, ':');
|
||||
if (parts.size() < 7) return;
|
||||
try {
|
||||
float px = std::stof(parts[0]);
|
||||
float py = std::stof(parts[1]);
|
||||
float pz = std::stof(parts[2]);
|
||||
Eigen::Quaternionf q(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
std::stof(parts[5]),
|
||||
std::stof(parts[6])
|
||||
);
|
||||
Eigen::Matrix3f rot = q.toRotationMatrix();
|
||||
parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot);
|
||||
}
|
||||
catch (...) {
|
||||
// пропускаем некорректную запись
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> bLock(boxesMutex);
|
||||
serverBoxes_ = std::move(parsedBoxes);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
|
||||
//auto parts = split(msg, ':');
|
||||
if (parts.size() >= 2) {
|
||||
try {
|
||||
int respawnedPlayerId = std::stoi(parts[1]);
|
||||
{
|
||||
std::lock_guard<std::mutex> rLock(respawnMutex_);
|
||||
pendingRespawns_.push_back(respawnedPlayerId);
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> pLock(playersMutex);
|
||||
remotePlayers.erase(respawnedPlayerId);
|
||||
}
|
||||
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.rfind("BOX_DESTROYED:", 0) == 0) {
|
||||
//auto parts = split(msg, ':');
|
||||
if (parts.size() >= 7) {
|
||||
try {
|
||||
BoxDestroyedInfo destruction;
|
||||
destruction.boxIndex = std::stoi(parts[1]);
|
||||
destruction.serverTime = std::stoull(parts[2]);
|
||||
destruction.position = Eigen::Vector3f(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
std::stof(parts[5])
|
||||
);
|
||||
destruction.destroyedBy = std::stoi(parts[6]);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
|
||||
pendingBoxDestructions_.push_back(destruction);
|
||||
}
|
||||
|
||||
std::cout << "Client: Received BOX_DESTROYED for box " << destruction.boxIndex
|
||||
<< " destroyed by player " << destruction.destroyedBy << std::endl;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "Client: Error parsing BOX_DESTROYED: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.rfind("PROJECTILE:", 0) == 0) {
|
||||
//auto parts = split(msg, ':');
|
||||
if (parts.size() >= 10) {
|
||||
try {
|
||||
ProjectileInfo pi;
|
||||
pi.shooterId = std::stoi(parts[1]);
|
||||
pi.clientTime = std::stoull(parts[2]);
|
||||
pi.position = Eigen::Vector3f(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
std::stof(parts[5])
|
||||
);
|
||||
Eigen::Quaternionf q(
|
||||
std::stof(parts[6]),
|
||||
std::stof(parts[7]),
|
||||
std::stof(parts[8]),
|
||||
std::stof(parts[9])
|
||||
);
|
||||
pi.rotation = q.toRotationMatrix();
|
||||
|
||||
pi.velocity = std::stof(parts[10]);
|
||||
std::lock_guard<std::mutex> pl(projMutex_);
|
||||
pendingProjectiles_.push_back(pi);
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.rfind("DEAD:", 0) == 0) {
|
||||
//auto parts = split(msg, ':');
|
||||
if (parts.size() >= 7) {
|
||||
try {
|
||||
DeathInfo di;
|
||||
di.serverTime = std::stoull(parts[1]);
|
||||
di.targetId = std::stoi(parts[2]);
|
||||
di.position = Eigen::Vector3f(
|
||||
std::stof(parts[3]),
|
||||
std::stof(parts[4]),
|
||||
std::stof(parts[5])
|
||||
);
|
||||
di.killerId = std::stoi(parts[6]);
|
||||
|
||||
std::lock_guard<std::mutex> dl(deathsMutex_);
|
||||
pendingDeaths_.push_back(di);
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.rfind("EVENT:", 0) == 0) {
|
||||
//auto parts = split(msg, ':');
|
||||
if (parts.size() < 5) return; // EVENT:ID:TYPE:TIME:DATA...
|
||||
|
||||
int remoteId = std::stoi(parts[1]);
|
||||
std::string subType = parts[2];
|
||||
uint64_t sentTime = std::stoull(parts[3]);
|
||||
|
||||
ClientState remoteState;
|
||||
remoteState.id = remoteId;
|
||||
|
||||
std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast<std::chrono::system_clock::time_point::duration>(std::chrono::milliseconds(sentTime)) };
|
||||
remoteState.lastUpdateServerTime = uptime_timepoint;
|
||||
|
||||
if (subType == "UPD") {
|
||||
int startFrom = 4;
|
||||
remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) };
|
||||
Eigen::Quaternionf q(
|
||||
std::stof(parts[startFrom + 3]),
|
||||
std::stof(parts[startFrom + 4]),
|
||||
std::stof(parts[startFrom + 5]),
|
||||
std::stof(parts[startFrom + 6]));
|
||||
remoteState.rotation = q.toRotationMatrix();
|
||||
|
||||
remoteState.currentAngularVelocity = Eigen::Vector3f{
|
||||
std::stof(parts[startFrom + 7]),
|
||||
std::stof(parts[startFrom + 8]),
|
||||
std::stof(parts[startFrom + 9]) };
|
||||
remoteState.velocity = std::stof(parts[startFrom + 10]);
|
||||
remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]);
|
||||
remoteState.discreteMag = std::stof(parts[startFrom + 12]);
|
||||
remoteState.discreteAngle = std::stoi(parts[startFrom + 13]);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Unknown EVENT subtype: " + subType);
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> pLock(playersMutex);
|
||||
auto& rp = remotePlayers[remoteId];
|
||||
|
||||
rp.add_state(remoteState);
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.rfind("SNAPSHOT:", 0) == 0) {
|
||||
auto mainParts = split(msg.substr(9), '|'); // Отсекаем "SNAPSHOT:" и делим по игрокам
|
||||
if (mainParts.empty()) return;
|
||||
|
||||
uint64_t serverTimestamp = std::stoull(mainParts[0]);
|
||||
std::chrono::system_clock::time_point serverTime{ std::chrono::milliseconds(serverTimestamp) };
|
||||
|
||||
for (size_t i = 1; i < mainParts.size(); ++i) {
|
||||
auto playerParts = split(mainParts[i], ':');
|
||||
if (playerParts.size() < 15) return; // ID + 14 полей ClientState
|
||||
|
||||
int rId = std::stoi(playerParts[0]);
|
||||
if (rId == clientId) return; // Свое состояние игрок знает лучше всех (Client Side Prediction)
|
||||
|
||||
ClientState remoteState;
|
||||
remoteState.id = rId;
|
||||
remoteState.lastUpdateServerTime = serverTime;
|
||||
|
||||
// Используем твой handle_full_sync, начиная со 2-го индекса (пропускаем ID в playerParts)
|
||||
remoteState.handle_full_sync(playerParts, 1);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> pLock(playersMutex);
|
||||
remotePlayers[rId].add_state(remoteState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string WebSocketClientBase::SignMessage(const std::string& msg) {
|
||||
#ifdef ENABLE_NETWORK_CHECKSUM
|
||||
size_t hashValue = fnv1a_hash(msg + NET_SECRET);
|
||||
std::stringstream ss;
|
||||
ss << msg << "#hash:" << std::hex << hashValue;
|
||||
return ss.str();
|
||||
#else
|
||||
return msg;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::vector<ProjectileInfo> WebSocketClientBase::getPendingProjectiles() {
|
||||
std::lock_guard<std::mutex> lock(projMutex_);
|
||||
auto copy = pendingProjectiles_;
|
||||
pendingProjectiles_.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
std::vector<DeathInfo> WebSocketClientBase::getPendingDeaths() {
|
||||
std::lock_guard<std::mutex> lock(deathsMutex_);
|
||||
auto copy = pendingDeaths_;
|
||||
pendingDeaths_.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
std::vector<int> WebSocketClientBase::getPendingRespawns() {
|
||||
std::lock_guard<std::mutex> lock(respawnMutex_);
|
||||
auto copy = pendingRespawns_;
|
||||
pendingRespawns_.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
std::vector<BoxDestroyedInfo> WebSocketClientBase::getPendingBoxDestructions() {
|
||||
std::lock_guard<std::mutex> lock(boxDestructionsMutex_);
|
||||
auto copy = pendingBoxDestructions_;
|
||||
pendingBoxDestructions_.clear();
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
61
src/network/WebSocketClientBase.h
Normal file
61
src/network/WebSocketClientBase.h
Normal file
@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include "NetworkInterface.h"
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
|
||||
namespace ZL {
|
||||
|
||||
|
||||
class WebSocketClientBase : public INetworkClient {
|
||||
protected:
|
||||
|
||||
|
||||
std::unordered_map<int, ClientStateInterval> remotePlayers;
|
||||
std::mutex playersMutex;
|
||||
|
||||
// Серверные коробки
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_;
|
||||
std::mutex boxesMutex;
|
||||
|
||||
std::vector<ProjectileInfo> pendingProjectiles_;
|
||||
std::mutex projMutex_;
|
||||
|
||||
std::vector<DeathInfo> pendingDeaths_;
|
||||
std::mutex deathsMutex_;
|
||||
|
||||
std::vector<int> pendingRespawns_;
|
||||
std::mutex respawnMutex_;
|
||||
|
||||
std::vector<BoxDestroyedInfo> pendingBoxDestructions_;
|
||||
std::mutex boxDestructionsMutex_;
|
||||
int clientId = -1;
|
||||
int64_t timeOffset = 0;
|
||||
|
||||
public:
|
||||
int GetClientId() const override { return clientId; }
|
||||
|
||||
int64_t getTimeOffset() const override { return timeOffset; }
|
||||
|
||||
void HandlePollMessage(const std::string& msg);
|
||||
|
||||
std::string SignMessage(const std::string& msg);
|
||||
|
||||
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override {
|
||||
std::lock_guard<std::mutex> lock(playersMutex);
|
||||
return remotePlayers;
|
||||
}
|
||||
|
||||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override {
|
||||
std::lock_guard<std::mutex> lock(boxesMutex);
|
||||
return serverBoxes_;
|
||||
}
|
||||
|
||||
std::vector<ProjectileInfo> getPendingProjectiles() override;
|
||||
std::vector<DeathInfo> getPendingDeaths() override;
|
||||
std::vector<int> getPendingRespawns() override;
|
||||
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
94
src/network/WebSocketClientEmscripten.cpp
Normal file
94
src/network/WebSocketClientEmscripten.cpp
Normal file
@ -0,0 +1,94 @@
|
||||
#ifdef NETWORK
|
||||
#ifdef EMSCRIPTEN
|
||||
#include "WebSocketClientEmscripten.h"
|
||||
#include <iostream>
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
namespace ZL {
|
||||
void WebSocketClientEmscripten::Connect(const std::string& host, uint16_t port) {
|
||||
// Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost
|
||||
//std::string url = "ws://" + host + ":" + std::to_string(port);
|
||||
std::string url = "wss://api.spacegame.fishrungames.com";
|
||||
|
||||
EmscriptenWebSocketCreateAttributes attr = {
|
||||
url.c_str(),
|
||||
nullptr,
|
||||
EM_TRUE // create_on_main_thread
|
||||
};
|
||||
|
||||
socket_ = emscripten_websocket_new(&attr);
|
||||
|
||||
emscripten_websocket_set_onopen_callback(socket_, this, onOpen);
|
||||
emscripten_websocket_set_onmessage_callback(socket_, this, onMessage);
|
||||
emscripten_websocket_set_onerror_callback(socket_, this, onError);
|
||||
emscripten_websocket_set_onclose_callback(socket_, this, onClose);
|
||||
}
|
||||
|
||||
void WebSocketClientEmscripten::Send(const std::string& message) {
|
||||
if (connected && socket_ > 0) {
|
||||
auto signedMsg = SignMessage(message);
|
||||
std::cout << "[WebWS] Sending message: " << signedMsg << std::endl;
|
||||
emscripten_websocket_send_utf8_text(socket_, signedMsg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketClientEmscripten::Poll() {
|
||||
// Локальная очередь для минимизации времени блокировки мьютекса
|
||||
std::queue<std::string> localQueue;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
if (messageQueue.empty()) return;
|
||||
std::swap(localQueue, messageQueue);
|
||||
}
|
||||
|
||||
while (!localQueue.empty()) {
|
||||
const std::string& msg = localQueue.front();
|
||||
std::cout << "[WebWS] Processing message: " << msg << std::endl;
|
||||
|
||||
// Передаем в базовый класс для парсинга игровых событий (BOXES, UPD, и т.д.)
|
||||
HandlePollMessage(msg);
|
||||
|
||||
localQueue.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Колбэки ---
|
||||
|
||||
EM_BOOL WebSocketClientEmscripten::onOpen(int eventType, const EmscriptenWebSocketOpenEvent* e, void* userData) {
|
||||
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
|
||||
self->connected = true;
|
||||
std::cout << "[WebWS] Connection opened" << std::endl;
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
EM_BOOL WebSocketClientEmscripten::onMessage(int eventType, const EmscriptenWebSocketMessageEvent* e, void* userData) {
|
||||
std::cout << "[WebWS] onMessage " << std::endl;
|
||||
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
|
||||
if (e->isText && e->data) {
|
||||
std::string msg(reinterpret_cast<const char*>(e->data), e->numBytes);
|
||||
std::lock_guard<std::mutex> lock(self->queueMutex);
|
||||
self->messageQueue.push(msg);
|
||||
}
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
EM_BOOL WebSocketClientEmscripten::onError(int eventType, const EmscriptenWebSocketErrorEvent* e, void* userData) {
|
||||
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
|
||||
self->connected = false;
|
||||
std::cerr << "[WebWS] Error detected" << std::endl;
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
EM_BOOL WebSocketClientEmscripten::onClose(int eventType, const EmscriptenWebSocketCloseEvent* e, void* userData) {
|
||||
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
|
||||
self->connected = false;
|
||||
std::cout << "[WebWS] Connection closed" << std::endl;
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
42
src/network/WebSocketClientEmscripten.h
Normal file
42
src/network/WebSocketClientEmscripten.h
Normal file
@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef NETWORK
|
||||
#ifdef EMSCRIPTEN
|
||||
|
||||
#include <emscripten/websocket.h>
|
||||
#include "WebSocketClientBase.h"
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace ZL {
|
||||
|
||||
class WebSocketClientEmscripten : public WebSocketClientBase {
|
||||
private:
|
||||
EMSCRIPTEN_WEBSOCKET_T socket_ = 0;
|
||||
bool connected = false;
|
||||
|
||||
// Очередь для хранения сырых строк от браузера
|
||||
std::queue<std::string> messageQueue;
|
||||
std::mutex queueMutex;
|
||||
|
||||
public:
|
||||
WebSocketClientEmscripten() = default;
|
||||
virtual ~WebSocketClientEmscripten() = default;
|
||||
|
||||
void Connect(const std::string& host, uint16_t port) override;
|
||||
void Send(const std::string& message) override;
|
||||
void Poll() override;
|
||||
|
||||
bool IsConnected() const override { return connected; }
|
||||
|
||||
// Статические методы-переходники для C-API Emscripten
|
||||
static EM_BOOL onOpen(int eventType, const EmscriptenWebSocketOpenEvent* e, void* userData);
|
||||
static EM_BOOL onMessage(int eventType, const EmscriptenWebSocketMessageEvent* e, void* userData);
|
||||
static EM_BOOL onError(int eventType, const EmscriptenWebSocketErrorEvent* e, void* userData);
|
||||
static EM_BOOL onClose(int eventType, const EmscriptenWebSocketCloseEvent* e, void* userData);
|
||||
};
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@ -291,7 +291,7 @@ namespace ZL {
|
||||
|
||||
drawPlanet(renderer);
|
||||
drawStones(renderer);
|
||||
drawCamp(renderer);
|
||||
//drawCamp(renderer);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
drawAtmosphere(renderer);
|
||||
}
|
||||
|
||||
@ -13,21 +13,14 @@ namespace ZL {
|
||||
|
||||
TextRenderer::~TextRenderer()
|
||||
{
|
||||
/*for (auto& kv : glyphs) {
|
||||
if (kv.second.texID) glDeleteTextures(1, &kv.second.texID);
|
||||
}*/
|
||||
glyphs.clear();
|
||||
atlasTexture.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)
|
||||
{
|
||||
|
||||
r = &renderer;
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
@ -49,15 +42,6 @@ bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixe
|
||||
|
||||
textMesh.data.PositionData.resize(6, Eigen::Vector3f(0, 0, 0));
|
||||
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();
|
||||
return true;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user