This commit is contained in:
Vladislav Khorev committed 2026-02-05 07:30:12 +03:00
commit 0d2f705d42
18 files changed
+901 -322

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
{
"root": {
"type": "FrameLayout",
"x": 0,
"y": 0,
"width": 1280,
"height": 720,
"children": [
{
"type": "LinearLayout",
"name": "settingsButtons",
"orientation": "vertical",
"spacing": 10,
"x": 0,
"y": 0,
"width": 300,
"height": 300,
"children": [
{
"type": "Button",
"name": "langButton",
"x": 1100,
"y": 580,
"width": 142,
"height": 96,
"textures": {
"normal": "resources/main_menu/lang.png",
"hover": "resources/main_menu/lang.png",
"pressed": "resources/main_menu/lang.png"
}
},
{
"type": "Button",
"name": "titleBtn",
"x": 473,
"y": 500,
"width": 254,
"height": 35,
"textures": {
"normal": "resources/main_menu/title.png",
"hover": "resources/main_menu/title.png",
"pressed": "resources/main_menu/title.png"
}
},
{
"type": "Button",
"name": "underlineBtn",
"x": 516,
"y": 465,
"width": 168,
"height": 44,
"textures": {
"normal": "resources/main_menu/line.png",
"hover": "resources/main_menu/line.png",
"pressed": "resources/main_menu/line.png"
}
},
{
"type": "Button",
"name": "subtitleBtn",
"x": 528,
"y": 455,
"width": 144,
"height": 11,
"textures": {
"normal": "resources/main_menu/subtitle.png",
"hover": "resources/main_menu/subtitle.png",
"pressed": "resources/main_menu/subtitle.png"
}
},
{
"type": "Button",
"name": "singleButton",
"x": 409,
"y": 360,
"width": 382,
"height": 56,
"textures": {
"normal": "resources/main_menu/single.png",
"hover": "resources/main_menu/single.png",
"pressed": "resources/main_menu/single.png"
}
},
{
"type": "Button",
"name": "multiplayerButton",
"x": 409,
"y": 289,
"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": 218,
"width": 382,
"height": 56,
"textures": {
"normal": "resources/main_menu/exit.png",
"hover": "resources/main_menu/exit.png",
"pressed": "resources/main_menu/exit.png"
}
},
{
"type": "Button",
"name": "versionLabel",
"x": 559.5,
"y": 170,
"width": 81,
"height": 9,
"textures": {
"normal": "resources/main_menu/version.png",
"hover": "resources/main_menu/version.png",
"pressed": "resources/main_menu/version.png"
}
}
]
}
]
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+285 -112
View File
@@ -11,16 +11,18 @@
#define _USE_MATH_DEFINES #define _USE_MATH_DEFINES
#include <math.h> #include <math.h>
#include "../src/network/ClientState.h" #include "../src/network/ClientState.h"
#include <random>
#include <algorithm>
// Вспомогательный split // Вспомогательный split
std::vector<std::string> split(const std::string& s, char delimiter) { std::vector<std::string> split(const std::string& s, char delimiter) {
std::vector<std::string> tokens; std::vector<std::string> tokens;
std::string token; std::string token;
std::istringstream tokenStream(s); std::istringstream tokenStream(s);
while (std::getline(tokenStream, token, delimiter)) { while (std::getline(tokenStream, token, delimiter)) {
tokens.push_back(token); tokens.push_back(token);
} }
return tokens; return tokens;
} }
namespace beast = boost::beast; namespace beast = boost::beast;
@@ -31,152 +33,323 @@ using tcp = net::ip::tcp;
class Session; class Session;
struct ServerBox {
Eigen::Vector3f position;
Eigen::Matrix3f rotation;
float collisionRadius = 2.0f;
};
std::vector<ServerBox> g_serverBoxes;
std::mutex g_boxes_mutex;
std::vector<std::shared_ptr<Session>> g_sessions; std::vector<std::shared_ptr<Session>> g_sessions;
std::mutex g_sessions_mutex; std::mutex g_sessions_mutex;
class Session : public std::enable_shared_from_this<Session> { class Session : public std::enable_shared_from_this<Session> {
websocket::stream<beast::tcp_stream> ws_; websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_; beast::flat_buffer buffer_;
int id_; int id_;
bool is_writing_ = false;
ClientStateInterval timedClientStates; ClientStateInterval timedClientStates;
void process_message(const std::string& msg) { void process_message(const std::string& msg) {
auto now_server = std::chrono::system_clock::now(); auto now_server = std::chrono::system_clock::now();
auto parts = split(msg, ':');
auto parts = split(msg, ':'); if (parts.empty()) {
std::cerr << "Empty message received\n";
return;
}
if (parts.size() < 16) std::string type = parts[0];
{
throw std::runtime_error("Unknown message type received, too small");
}
uint64_t clientTimestamp = std::stoull(parts[1]); if (type == "UPD") {
if (parts.size() < 16) {
std::cerr << "Invalid UPD message: too few parts (" << parts.size() << ")\n";
return;
}
uint64_t clientTimestamp = std::stoull(parts[1]);
ClientState receivedState;
receivedState.id = id_;
std::chrono::system_clock::time_point uptime_timepoint{
std::chrono::milliseconds(clientTimestamp)
};
receivedState.lastUpdateServerTime = uptime_timepoint;
receivedState.handle_full_sync(parts, 2);
retranslateMessage(msg);
timedClientStates.add_state(receivedState);
}
else if (parts[0] == "FIRE") {
if (parts.size() < 10) {
std::cerr << "Invalid FIRE: too few parts\n";
return;
}
ClientState receivedState; uint64_t clientTime = std::stoull(parts[1]);
Eigen::Vector3f pos{
std::stof(parts[2]), std::stof(parts[3]), std::stof(parts[4])
};
Eigen::Quaternion dir{
std::stof(parts[5]), std::stof(parts[6]), std::stof(parts[7]), std::stof(parts[8])
};
receivedState.id = id_; float velocity = std::stof(parts[9]);
std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast<std::chrono::system_clock::time_point::duration>(std::chrono::milliseconds(clientTimestamp)) }; int shotCount = 2;
receivedState.lastUpdateServerTime = uptime_timepoint; if (parts.size() >= 11) {
try {
shotCount = std::stoi(parts[10]);
}
catch (...) {
shotCount = 2;
}
}
if (parts[0] == "UPD") { std::string broadcast = "PROJECTILE:" +
receivedState.handle_full_sync(parts, 2); std::to_string(id_) + ":" +
retranslateMessage(msg); std::to_string(clientTime) + ":" +
} std::to_string(pos.x()) + ":" +
else std::to_string(pos.y()) + ":" +
{ std::to_string(pos.z()) + ":" +
throw std::runtime_error("Unknown message type received: " + parts[0]); std::to_string(dir.w()) + ":" +
} std::to_string(dir.x()) + ":" +
std::to_string(dir.y()) + ":" +
std::to_string(dir.z()) + ":" +
std::to_string(velocity) + ":" +
std::to_string(shotCount);
timedClientStates.add_state(receivedState); std::lock_guard<std::mutex> lock(g_sessions_mutex);
} std::cout << "Player " << id_ << " fired " << shotCount << " shots → broadcasting\n";
void retranslateMessage(const std::string& msg) for (auto& session : g_sessions) {
{ if (session->get_id() != id_) {
std::string event_msg = "EVENT:" + std::to_string(id_) + ":" + msg; session->send_message(broadcast);
}
}
}
else {
std::cerr << "Unknown message type: " << type << "\n";
}
}
void retranslateMessage(const std::string& msg)
{
std::string event_msg = "EVENT:" + std::to_string(id_) + ":" + msg;
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
if (session->get_id() != id_) { // Не шлем отправителю
session->send_message(event_msg);
}
}
}
void sendBoxesToClient() {
std::lock_guard<std::mutex> lock(g_boxes_mutex);
std::string boxMsg = "BOXES:";
for (const auto& box : g_serverBoxes) {
Eigen::Quaternionf q(box.rotation);
boxMsg += std::to_string(box.position.x()) + ":" +
std::to_string(box.position.y()) + ":" +
std::to_string(box.position.z()) + ":" +
std::to_string(q.w()) + ":" +
std::to_string(q.x()) + ":" +
std::to_string(q.y()) + ":" +
std::to_string(q.z()) + "|";
}
if (!boxMsg.empty() && boxMsg.back() == '|') {
boxMsg.pop_back();
}
send_message(boxMsg);
}
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;
}
});
}
}
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
if (session->get_id() != id_) { // Не шлем отправителю
session->send_message(event_msg);
}
}
}
public: public:
explicit Session(tcp::socket&& socket, int id) explicit Session(tcp::socket&& socket, int id)
: ws_(std::move(socket)), id_(id) { : ws_(std::move(socket)), id_(id) {
} }
void init() void init()
{ {
} sendBoxesToClient();
void run() { auto timer = std::make_shared<net::steady_timer>(ws_.get_executor());
timer->expires_after(std::chrono::milliseconds(100));
timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) {
if (!ec) {
self->send_message("ID:" + std::to_string(self->id_));
self->do_read();
}
});
}
{ void run() {
std::lock_guard<std::mutex> lock(g_sessions_mutex);
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
g_sessions.push_back(shared_from_this()); g_sessions.push_back(shared_from_this());
} }
ws_.async_accept([self = shared_from_this()](beast::error_code ec) { ws_.async_accept([self = shared_from_this()](beast::error_code ec) {
if (ec) return; if (ec) return;
std::cout << "Client " << self->id_ << " connected\n"; std::cout << "Client " << self->id_ << " connected\n";
self->init(); self->init();
self->send_message("ID:" + std::to_string(self->id_)); // self->send_message("ID:" + std::to_string(self->id_));
self->do_read(); // self->do_read();
}); });
} }
void send_message(std::string msg) { /*void send_message(std::string msg) {
auto ss = std::make_shared<std::string>(std::move(msg)); auto ss = std::make_shared<std::string>(std::move(msg));
ws_.async_write(net::buffer(*ss), [ss](beast::error_code, std::size_t) {}); ws_.async_write(net::buffer(*ss), [ss](beast::error_code, std::size_t) {});
} }*/
int get_id() const { int get_id() const {
return id_; return id_;
} }
private: private:
void do_read() { void do_read() {
ws_.async_read(buffer_, [self = shared_from_this()](beast::error_code ec, std::size_t) { ws_.async_read(buffer_, [self = shared_from_this()](beast::error_code ec, std::size_t) {
if (ec) if (ec)
{ {
std::lock_guard<std::mutex> lock(g_sessions_mutex); std::lock_guard<std::mutex> lock(g_sessions_mutex);
g_sessions.erase(std::remove_if(g_sessions.begin(), g_sessions.end(), g_sessions.erase(std::remove_if(g_sessions.begin(), g_sessions.end(),
[self](const std::shared_ptr<Session>& session) { [self](const std::shared_ptr<Session>& session) {
return session.get() == self.get(); return session.get() == self.get();
}), g_sessions.end()); }), g_sessions.end());
return; return;
} }
std::string msg = beast::buffers_to_string(self->buffer_.data()); std::string msg = beast::buffers_to_string(self->buffer_.data());
self->process_message(msg); self->process_message(msg);
self->buffer_.consume(self->buffer_.size()); self->buffer_.consume(self->buffer_.size());
self->do_read(); self->do_read();
}); });
} }
}; };
void update_world(net::steady_timer& timer, net::io_context& ioc) { void update_world(net::steady_timer& timer, net::io_context& ioc) {
// TODO: Renew game state // TODO: Renew game state
timer.expires_after(std::chrono::milliseconds(50)); timer.expires_after(std::chrono::milliseconds(50));
timer.async_wait([&](const boost::system::error_code& ec) { timer.async_wait([&](const boost::system::error_code& ec) {
if (!ec) update_world(timer, ioc); if (!ec) update_world(timer, ioc);
}); });
} }
std::vector<ServerBox> generateServerBoxes(int count) {
std::vector<ServerBox> boxes;
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 < count; i++) {
bool accepted = false;
int attempts = 0;
while (!accepted && attempts < MAX_ATTEMPTS) {
ServerBox box;
box.position = Eigen::Vector3f(
(float)posDistrib(gen),
(float)posDistrib(gen),
(float)posDistrib(gen)
);
accepted = true;
for (const auto& existingBox : boxes) {
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();
boxes.push_back(box);
}
attempts++;
}
}
return boxes;
}
int main() { int main() {
try { try {
net::io_context ioc; {
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} }; std::lock_guard<std::mutex> lock(g_boxes_mutex);
int next_id = 1000; g_serverBoxes = generateServerBoxes(50);
std::cout << "Generated " << g_serverBoxes.size() << " boxes on server\n";
}
net::io_context ioc;
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} };
int next_id = 1000;
std::cout << "Server started on port 8080...\n"; std::cout << "Server started on port 8080...\n";
auto do_accept = [&](auto& self_fn) -> void { auto do_accept = [&](auto& self_fn) -> void {
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) { acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {
if (!ec) { if (!ec) {
std::make_shared<Session>(std::move(socket), next_id++)->run(); std::make_shared<Session>(std::move(socket), next_id++)->run();
} }
self_fn(self_fn); self_fn(self_fn);
}); });
}; };
net::steady_timer timer(ioc);
update_world(timer, ioc);
do_accept(do_accept); net::steady_timer timer(ioc);
ioc.run(); update_world(timer, ioc);
}
catch (std::exception const& e) { do_accept(do_accept);
std::cerr << "Error: " << e.what() << std::endl; ioc.run();
} }
return 0; catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
} }
+222 -76
View File
@@ -10,6 +10,7 @@
#include <random> #include <random>
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <functional>
#ifdef __ANDROID__ #ifdef __ANDROID__
#include <android/log.h> #include <android/log.h>
#endif #endif
@@ -164,83 +165,126 @@ namespace ZL
bool explosionCfgLoaded = explosionEmitter.loadFromJsonFile("resources/config/explosion_config.json", renderer, CONST_ZIP_FILE); bool explosionCfgLoaded = explosionEmitter.loadFromJsonFile("resources/config/explosion_config.json", renderer, CONST_ZIP_FILE);
explosionEmitter.setEmissionPoints(std::vector<Vector3f>()); explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
projectileEmitter.setEmissionPoints(std::vector<Vector3f>()); projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
uiManager.loadFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE);
uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); uiManager.loadFromFile("resources/config/main_menu.json", renderer, CONST_ZIP_FILE);
static bool isExitButtonAnimating = false; std::function<void()> loadGameplayUI;
uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() { loadGameplayUI = [this]() {
std::cerr << "Settings button animation finished -> переход в настройки" << std::endl; uiManager.loadFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE);
if (uiManager.pushMenuFromFile("resources/config/settings.json", this->renderer, CONST_ZIP_FILE)) {
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", []() { uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
std::cerr << "Exit button bgScroll animation finished" << std::endl; static bool isExitButtonAnimating = false;
g_exitBgAnimating = false; uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() {
}); std::cerr << "Settings button animation finished -> переход в настройки" << std::endl;
if (uiManager.pushMenuFromFile("resources/config/settings.json", this->renderer, CONST_ZIP_FILE)) {
// Set UI button callbacks uiManager.setButtonCallback("Opt1", [this](const std::string& n) {
uiManager.setButtonCallback("playButton", [this](const std::string& name) { std::cerr << "Opt1 pressed: " << n << std::endl;
std::cerr << "Play button pressed: " << name << std::endl; });
uiManager.setButtonCallback("Opt2", [this](const std::string& n) {
}); std::cerr << "Opt2 pressed: " << n << std::endl;
});
uiManager.setButtonCallback("settingsButton", [this](const std::string& name) { uiManager.setButtonCallback("backButton", [this](const std::string& n) {
std::cerr << "Settings button pressed: " << name << std::endl; uiManager.stopAllAnimations();
uiManager.startAnimationOnNode("playButton", "buttonsExit"); uiManager.popMenu();
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();
} }
} else {
}); std::cerr << "Failed to open settings menu after animations" << std::endl;
uiManager.setButtonCallback("shootButton", [this](const std::string& name) { }
uint64_t now = SDL_GetTicks64(); });
if (now - lastProjectileFireTime >= static_cast<uint64_t>(projectileCooldownMs)) {
lastProjectileFireTime = now;
fireProjectiles();
}
});
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) { uiManager.setAnimationCallback("exitButton", "bgScroll", []() {
int newVel = roundf(value * 10); std::cerr << "Exit button bgScroll animation finished" << std::endl;
if (newVel != Environment::shipState.selectedVelocity) { g_exitBgAnimating = false;
newShipVelocity = newVel; });
}
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) {
uint64_t now = SDL_GetTicks64();
if (now - lastProjectileFireTime >= static_cast<uint64_t>(projectileCooldownMs)) {
lastProjectileFireTime = now;
const float projectileSpeed = 60.0f;
this->fireProjectiles();
Eigen::Vector3f localForward = { 0, 0, -1 };
Eigen::Vector3f worldForward = (Environment::shipState.rotation * localForward).normalized();
Eigen::Vector3f centerPos = Environment::shipState.position +
Environment::shipState.rotation * Vector3f{ 0, 0.9f, 5.0f };
Eigen::Quaternionf q(Environment::shipState.rotation);
float speedToSend = projectileSpeed + Environment::shipState.velocity;
int shotCount = 2;
std::string fireMsg = "FIRE:" +
std::to_string(now) + ":" +
std::to_string(centerPos.x()) + ":" +
std::to_string(centerPos.y()) + ":" +
std::to_string(centerPos.z()) + ":" +
std::to_string(q.w()) + ":" +
std::to_string(q.x()) + ":" +
std::to_string(q.y()) + ":" +
std::to_string(q.z()) + ":" +
std::to_string(speedToSend) + ":" +
std::to_string(shotCount);
networkClient->Send(fireMsg);
}
});
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
int newVel = roundf(value * 10);
if (newVel != Environment::shipState.selectedVelocity) {
newShipVelocity = newVel;
}
});
};
uiManager.setButtonCallback("singleButton", [loadGameplayUI](const std::string& name) {
std::cerr << "Single button pressed: " << name << " -> load gameplay UI\n";
loadGameplayUI();
});
uiManager.setButtonCallback("multiplayerButton", [loadGameplayUI](const std::string& name) {
std::cerr << "Multiplayer button pressed: " << name << " -> load gameplay UI\n";
loadGameplayUI();
});
uiManager.setButtonCallback("exitButton", [](const std::string& name) {
std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n";
Environment::exitGameLoop = true;
}); });
cubemapTexture = std::make_shared<Texture>( cubemapTexture = std::make_shared<Texture>(
@@ -580,6 +624,27 @@ namespace ZL
latestRemotePlayers = networkClient->getRemotePlayers(); latestRemotePlayers = networkClient->getRemotePlayers();
// Если сервер прислал коробки, применяем их однократно вместо локальной генерации
if (!serverBoxesApplied && networkClient) {
auto sboxes = networkClient->getServerBoxes();
if (!sboxes.empty()) {
boxCoordsArr.clear();
for (auto& b : sboxes) {
BoxCoords bc;
bc.pos = b.first;
bc.m = b.second;
boxCoordsArr.push_back(bc);
}
boxRenderArr.resize(boxCoordsArr.size());
for (int i = 0; i < (int)boxCoordsArr.size(); ++i) {
boxRenderArr[i].AssignFrom(boxBase);
boxRenderArr[i].RefreshVBO();
}
boxAlive.assign(boxCoordsArr.size(), true);
serverBoxesApplied = true;
}
}
// Итерируемся по актуальным данным из extrapolateRemotePlayers // Итерируемся по актуальным данным из extrapolateRemotePlayers
for (auto const& [id, remotePlayer] : latestRemotePlayers) { for (auto const& [id, remotePlayer] : latestRemotePlayers) {
@@ -589,7 +654,7 @@ namespace ZL
} }
ClientState playerState = remotePlayer.fetchClientStateAtTime(now); ClientState playerState = remotePlayer.fetchClientStateAtTime(now);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.LoadIdentity(); renderer.LoadIdentity();
@@ -686,7 +751,7 @@ namespace ZL
float radians = atan2f(diffy, diffx); float radians = atan2f(diffy, diffx);
discreteAngle = static_cast<int>(radians * 180.0f / M_PI); discreteAngle = static_cast<int>(radians * 180.0f / M_PI);
if (discreteAngle < 0) discreteAngle += 360; if (discreteAngle < 0) discreteAngle += 360;
} }
else else
{ {
@@ -700,7 +765,7 @@ namespace ZL
discreteMag = 0.0f; discreteMag = 0.0f;
} }
if (discreteAngle != Environment::shipState.discreteAngle || discreteMag != Environment::shipState.discreteMag) { if (discreteAngle != Environment::shipState.discreteAngle || discreteMag != Environment::shipState.discreteMag) {
Environment::shipState.discreteAngle = discreteAngle; Environment::shipState.discreteAngle = discreteAngle;
Environment::shipState.discreteMag = discreteMag; Environment::shipState.discreteMag = discreteMag;
@@ -1095,6 +1160,87 @@ namespace ZL
render(); render();
mainThreadHandler.processMainThreadTasks(); mainThreadHandler.processMainThreadTasks();
networkClient->Poll(); networkClient->Poll();
if (networkClient) {
auto pending = networkClient->getPendingProjectiles();
if (!pending.empty()) {
const float projectileSpeed = 60.0f;
const float lifeMs = 5000.0f;
const float size = 0.5f;
for (const auto& pi : pending) {
const std::vector<Vector3f> localOffsets = {
Vector3f{ -1.5f, 0.9f, 5.0f },
Vector3f{ 1.5f, 0.9f, 5.0f }
//Vector3f{},
//Vector3f{}
};
Vector3f localForward = { 0, 0, -1 };
Vector3f worldForward = pi.rotation * localForward;
float len = worldForward.norm();
if (len <= 1e-6f) {
continue;
}
worldForward /= len;
Vector3f baseVel = worldForward * pi.velocity;
for (const auto& off : localOffsets) {
Vector3f shotPos = pi.position + (pi.rotation * off);
for (auto& p : projectiles) {
if (!p->isActive()) {
p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer);
break;
}
}
}
}
/*
auto remotePlayersSnapshot = networkClient->getRemotePlayers();
for (const auto& pi : pending) {
Eigen::Vector3f dir = pi.direction;
float len = dir.norm();
if (len <= 1e-6f) continue;
dir /= len;
Eigen::Matrix3f shooterRot = Eigen::Matrix3f::Identity();
float shooterVel = 0.0f;
auto it = remotePlayersSnapshot.find(pi.shooterId);
if (it != remotePlayersSnapshot.end()) {
std::chrono::system_clock::time_point pktTime{ std::chrono::milliseconds(pi.clientTime) };
if (it->second.canFetchClientStateAtTime(pktTime)) {
ClientState shooterState = it->second.fetchClientStateAtTime(pktTime);
shooterRot = shooterState.rotation;
shooterVel = shooterState.velocity;
}
}
float speedWithOwner = projectileSpeed + shooterVel;
Eigen::Vector3f baseVel = dir * speedWithOwner;
int shotCount = 2;
std::vector<Eigen::Vector3f> localOffsets = {
{-1.5f, 0.9f, 5.0f},
{ 1.5f, 0.9f, 5.0f}
};
for (int i = 0; i < shotCount; ++i) {
Eigen::Vector3f rotatedOffset = shooterRot * localOffsets[i];
Eigen::Vector3f shotPos = pi.position + rotatedOffset;
for (auto& p : projectiles) {
if (!p->isActive()) {
p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer);
break;
}
}
}
}*/
}
}
} }
void Game::handleDown(int mx, int my) void Game::handleDown(int mx, int my)
+1
View File
@@ -105,6 +105,7 @@ namespace ZL {
uint64_t lastExplosionTime = 0; uint64_t lastExplosionTime = 0;
const uint64_t explosionDurationMs = 500; const uint64_t explosionDurationMs = 500;
bool serverBoxesApplied = false;
}; };
+1 -1
View File
@@ -20,7 +20,7 @@ constexpr float PLANET_MAX_ANGULAR_VELOCITY = 10.f;
constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f; constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f;
constexpr long long SERVER_DELAY = 0; //ms constexpr long long SERVER_DELAY = 0; //ms
constexpr long long CLIENT_DELAY = 200; //ms constexpr long long CLIENT_DELAY = 1000; //ms
constexpr long long CUTOFF_TIME = 5000; //ms constexpr long long CUTOFF_TIME = 5000; //ms
struct ClientState { struct ClientState {
+9 -7
View File
@@ -4,15 +4,17 @@
namespace ZL { namespace ZL {
void LocalClient::Connect(const std::string& host, uint16_t port) { void LocalClient::Connect(const std::string& host, uint16_t port) {
} }
void LocalClient::Poll() { void LocalClient::Poll() {
} }
void LocalClient::Send(const std::string& message) { void LocalClient::Send(const std::string& message) {
}
}
std::vector<ProjectileInfo> LocalClient::getPendingProjectiles() {
return {};
}
} }
+5
View File
@@ -17,9 +17,14 @@ namespace ZL {
bool IsConnected() const override { return true; } bool IsConnected() const override { return true; }
int GetClientId() const { return 1; } int GetClientId() const { return 1; }
std::vector<ProjectileInfo> getPendingProjectiles();
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override { std::unordered_map<int, ClientStateInterval> getRemotePlayers() override {
return std::unordered_map<int, ClientStateInterval>(); return std::unordered_map<int, ClientStateInterval>();
} }
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override {
return {};
}
}; };
} }
+13
View File
@@ -7,6 +7,15 @@
// NetworkInterface.h - »нтерфейс дл¤ разных типов соединений // NetworkInterface.h - »нтерфейс дл¤ разных типов соединений
namespace ZL { namespace ZL {
struct ProjectileInfo {
int shooterId = -1;
uint64_t clientTime = 0;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
//Eigen::Vector3f direction = Eigen::Vector3f::Zero();
Eigen::Matrix3f rotation = Eigen::Matrix3f::Identity();
float velocity = 0;
};
class INetworkClient { class INetworkClient {
public: public:
virtual ~INetworkClient() = default; virtual ~INetworkClient() = default;
@@ -15,5 +24,9 @@ namespace ZL {
virtual bool IsConnected() const = 0; virtual bool IsConnected() const = 0;
virtual void Poll() = 0; // ƒл¤ обработки вход¤щих пакетов virtual void Poll() = 0; // ƒл¤ обработки вход¤щих пакетов
virtual std::unordered_map<int, ClientStateInterval> getRemotePlayers() = 0; virtual std::unordered_map<int, ClientStateInterval> getRemotePlayers() = 0;
virtual std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() = 0;
virtual std::vector<ProjectileInfo> getPendingProjectiles() = 0;
}; };
} }
+199 -126
View File
@@ -6,170 +6,243 @@
// Вспомогательный split // Вспомогательный split
std::vector<std::string> split(const std::string& s, char delimiter) { std::vector<std::string> split(const std::string& s, char delimiter) {
std::vector<std::string> tokens; std::vector<std::string> tokens;
std::string token; std::string token;
std::istringstream tokenStream(s); std::istringstream tokenStream(s);
while (std::getline(tokenStream, token, delimiter)) { while (std::getline(tokenStream, token, delimiter)) {
tokens.push_back(token); tokens.push_back(token);
} }
return tokens; return tokens;
} }
namespace ZL { namespace ZL {
void WebSocketClient::Connect(const std::string& host, uint16_t port) { void WebSocketClient::Connect(const std::string& host, uint16_t port) {
try { try {
boost::asio::ip::tcp::resolver resolver(ioc_); boost::asio::ip::tcp::resolver resolver(ioc_);
auto const results = resolver.resolve(host, std::to_string(port)); auto const results = resolver.resolve(host, std::to_string(port));
ws_ = std::make_unique<boost::beast::websocket::stream<boost::beast::tcp_stream>>(ioc_); ws_ = std::make_unique<boost::beast::websocket::stream<boost::beast::tcp_stream>>(ioc_);
// Выполняем синхронный коннект и handshake для простоты старта // Выполняем синхронный коннект и handshake для простоты старта
boost::beast::get_lowest_layer(*ws_).connect(results); boost::beast::get_lowest_layer(*ws_).connect(results);
ws_->handshake(host, "/"); ws_->handshake(host, "/");
connected = true; connected = true;
// Запускаем асинхронное чтение в пуле потоков TaskManager // Запускаем асинхронное чтение в пуле потоков TaskManager
startAsyncRead(); startAsyncRead();
} }
catch (std::exception& e) { catch (std::exception& e) {
std::cerr << "Network Error: " << e.what() << std::endl; std::cerr << "Network Error: " << e.what() << std::endl;
} }
} }
void WebSocketClient::startAsyncRead() { void WebSocketClient::startAsyncRead() {
ws_->async_read(buffer_, [this](boost::beast::error_code ec, std::size_t bytes) { ws_->async_read(buffer_, [this](boost::beast::error_code ec, std::size_t bytes) {
if (!ec) { if (!ec) {
std::string msg = boost::beast::buffers_to_string(buffer_.data()); std::string msg = boost::beast::buffers_to_string(buffer_.data());
buffer_.consume(bytes); buffer_.consume(bytes);
processIncomingMessage(msg); processIncomingMessage(msg);
startAsyncRead(); startAsyncRead();
} }
else { else {
connected = false; connected = false;
} }
}); });
} }
void WebSocketClient::processIncomingMessage(const std::string& msg) { void WebSocketClient::processIncomingMessage(const std::string& msg) {
// Логика парсинга... // Логика парсинга...
if (msg.rfind("ID:", 0) == 0) { if (msg.rfind("ID:", 0) == 0) {
clientId = std::stoi(msg.substr(3)); clientId = std::stoi(msg.substr(3));
} }
// Безопасно кладем в очередь для главного потока // Безопасно кладем в очередь для главного потока
std::lock_guard<std::mutex> lock(queueMutex); std::lock_guard<std::mutex> lock(queueMutex);
messageQueue.push(msg); messageQueue.push(msg);
} }
void WebSocketClient::Poll() { std::vector<ProjectileInfo> WebSocketClient::getPendingProjectiles() {
std::lock_guard<std::mutex> lock(queueMutex); std::lock_guard<std::mutex> lock(projMutex_);
auto copy = pendingProjectiles_;
pendingProjectiles_.clear();
return copy;
}
while (!messageQueue.empty()) { void WebSocketClient::Poll() {
std::lock_guard<std::mutex> lock(queueMutex);
auto nowTime = std::chrono::system_clock::now(); while (!messageQueue.empty()) {
//Apply server delay: auto nowTime = std::chrono::system_clock::now();
nowTime -= std::chrono::milliseconds(CLIENT_DELAY);
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>( //Apply server delay:
nowTime.time_since_epoch() nowTime -= std::chrono::milliseconds(CLIENT_DELAY);
).count();
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
nowTime.time_since_epoch()
).count();
std::string msg = messageQueue.front(); std::string msg = messageQueue.front();
messageQueue.pop(); messageQueue.pop();
if (msg.rfind("EVENT:", 0) == 0) { // Обработка списка коробок от сервера
auto parts = split(msg, ':'); if (msg.rfind("BOXES:", 0) == 0) {
if (parts.size() < 5) continue; // EVENT:ID:TYPE:TIME:DATA... 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;
}
int remoteId = std::stoi(parts[1]); if (msg.rfind("PROJECTILE:", 0) == 0) {
std::string subType = parts[2]; auto parts = split(msg, ':');
uint64_t sentTime = std::stoull(parts[3]); 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();
ClientState remoteState; pi.velocity = std::stof(parts[10]);
std::lock_guard<std::mutex> pl(projMutex_);
pendingProjectiles_.push_back(pi);
}
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; 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)) }; 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; 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{ if (subType == "UPD") {
std::stof(parts[startFrom + 7]), int startFrom = 4;
std::stof(parts[startFrom + 8]), remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) };
std::stof(parts[startFrom + 9]) }; Eigen::Quaternionf q(
remoteState.velocity = std::stof(parts[startFrom + 10]); std::stof(parts[startFrom + 3]),
remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]); std::stof(parts[startFrom + 4]),
remoteState.discreteMag = std::stof(parts[startFrom + 12]); std::stof(parts[startFrom + 5]),
remoteState.discreteAngle = std::stoi(parts[startFrom + 13]); std::stof(parts[startFrom + 6]));
} remoteState.rotation = q.toRotationMatrix();
else
{ 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); throw std::runtime_error("Unknown EVENT subtype: " + subType);
} }
{ {
std::lock_guard<std::mutex> pLock(playersMutex); std::lock_guard<std::mutex> pLock(playersMutex);
auto& rp = remotePlayers[remoteId]; auto& rp = remotePlayers[remoteId];
rp.add_state(remoteState); rp.add_state(remoteState);
} }
} }
} }
} }
void WebSocketClient::Send(const std::string& message) { void WebSocketClient::Send(const std::string& message) {
if (!connected) return; if (!connected) return;
auto ss = std::make_shared<std::string>(message); auto ss = std::make_shared<std::string>(message);
std::lock_guard<std::mutex> lock(writeMutex_); std::lock_guard<std::mutex> lock(writeMutex_);
writeQueue_.push(ss); writeQueue_.push(ss);
// Если сейчас ничего не записывается, инициируем первую запись // Если сейчас ничего не записывается, инициируем первую запись
if (!isWriting_) { if (!isWriting_) {
doWrite(); doWrite();
} }
} }
void WebSocketClient::doWrite() { void WebSocketClient::doWrite() {
// Эта функция всегда вызывается под мьютексом или из колбэка // Эта функция всегда вызывается под мьютексом или из колбэка
if (writeQueue_.empty()) { if (writeQueue_.empty()) {
isWriting_ = false; isWriting_ = false;
return; return;
} }
isWriting_ = true; isWriting_ = true;
auto message = writeQueue_.front(); auto message = writeQueue_.front();
// Захватываем self (shared_from_this), чтобы объект не удалился во время записи // Захватываем self (shared_from_this), чтобы объект не удалился во время записи
ws_->async_write( ws_->async_write(
boost::asio::buffer(*message), boost::asio::buffer(*message),
[this, message](boost::beast::error_code ec, std::size_t) { [this, message](boost::beast::error_code ec, std::size_t) {
if (ec) { if (ec) {
connected = false; connected = false;
return; return;
} }
std::lock_guard<std::mutex> lock(writeMutex_); std::lock_guard<std::mutex> lock(writeMutex_);
writeQueue_.pop(); // Удаляем отправленное сообщение writeQueue_.pop(); // Удаляем отправленное сообщение
doWrite(); // Проверяем следующее doWrite(); // Проверяем следующее
} }
); );
} }
} }
#endif #endif
+14
View File
@@ -34,6 +34,13 @@ namespace ZL {
std::unordered_map<int, ClientStateInterval> remotePlayers; std::unordered_map<int, ClientStateInterval> remotePlayers;
std::mutex playersMutex; std::mutex playersMutex;
// Серверные коробки
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> serverBoxes_;
std::mutex boxesMutex;
std::vector<ProjectileInfo> pendingProjectiles_;
std::mutex projMutex_;
void startAsyncRead(); void startAsyncRead();
void processIncomingMessage(const std::string& msg); void processIncomingMessage(const std::string& msg);
@@ -54,6 +61,13 @@ namespace ZL {
std::lock_guard<std::mutex> lock(playersMutex); std::lock_guard<std::mutex> lock(playersMutex);
return remotePlayers; 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;
}; };
} }
#endif #endif