adjust label scale: constant far, smooth growth near

This commit is contained in:
vottozi 2026-02-05 01:34:14 +06:00
commit c8076b6018
18 changed files with 867 additions and 325 deletions

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"
}
}
]
}
]
}
}

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View File

@ -11,16 +11,18 @@
#define _USE_MATH_DEFINES
#include <math.h>
#include "../src/network/ClientState.h"
#include <random>
#include <algorithm>
// Вспомогательный 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;
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 beast = boost::beast;
@ -31,152 +33,319 @@ using tcp = net::ip::tcp;
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::mutex g_sessions_mutex;
class Session : public std::enable_shared_from_this<Session> {
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
int id_;
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
int id_;
bool is_writing_ = false;
ClientStateInterval timedClientStates;
void process_message(const std::string& msg) {
auto now_server = std::chrono::system_clock::now();
void process_message(const std::string& msg) {
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)
{
throw std::runtime_error("Unknown message type received, too small");
}
std::string type = parts[0];
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() < 8) {
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::Vector3f dir{
std::stof(parts[5]), std::stof(parts[6]), std::stof(parts[7])
};
receivedState.id = id_;
int shotCount = 1;
if (parts.size() >= 9) {
try {
shotCount = std::stoi(parts[8]);
}
catch (...) {
shotCount = 1;
}
}
std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast<std::chrono::system_clock::time_point::duration>(std::chrono::milliseconds(clientTimestamp)) };
receivedState.lastUpdateServerTime = uptime_timepoint;
std::string broadcast = "PROJECTILE:" +
std::to_string(id_) + ":" +
std::to_string(clientTime) + ":" +
std::to_string(pos.x()) + ":" +
std::to_string(pos.y()) + ":" +
std::to_string(pos.z()) + ":" +
std::to_string(dir.x()) + ":" +
std::to_string(dir.y()) + ":" +
std::to_string(dir.z()) + ":" +
std::to_string(shotCount);
if (parts[0] == "UPD") {
receivedState.handle_full_sync(parts, 2);
retranslateMessage(msg);
}
else
{
throw std::runtime_error("Unknown message type received: " + parts[0]);
}
std::lock_guard<std::mutex> lock(g_sessions_mutex);
std::cout << "Player " << id_ << " fired " << shotCount << " shots → broadcasting\n";
timedClientStates.add_state(receivedState);
}
for (auto& session : g_sessions) {
if (session->get_id() != id_) {
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;
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:
explicit Session(tcp::socket&& socket, int id)
: ws_(std::move(socket)), id_(id) {
}
explicit Session(tcp::socket&& socket, int 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();
}
});
}
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
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();
});
}
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) {});
}
/*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_;
int get_id() const {
return id_;
}
private:
void do_read() {
ws_.async_read(buffer_, [self = shared_from_this()](beast::error_code ec, std::size_t) {
if (ec)
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
g_sessions.erase(std::remove_if(g_sessions.begin(), g_sessions.end(),
[self](const std::shared_ptr<Session>& session) {
return session.get() == self.get();
void do_read() {
ws_.async_read(buffer_, [self = shared_from_this()](beast::error_code ec, std::size_t) {
if (ec)
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
g_sessions.erase(std::remove_if(g_sessions.begin(), g_sessions.end(),
[self](const std::shared_ptr<Session>& session) {
return session.get() == self.get();
}), g_sessions.end());
return;
}
return;
}
std::string msg = beast::buffers_to_string(self->buffer_.data());
self->process_message(msg);
std::string msg = beast::buffers_to_string(self->buffer_.data());
self->process_message(msg);
self->buffer_.consume(self->buffer_.size());
self->do_read();
});
}
self->buffer_.consume(self->buffer_.size());
self->do_read();
});
}
};
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.async_wait([&](const boost::system::error_code& ec) {
if (!ec) update_world(timer, ioc);
});
timer.expires_after(std::chrono::milliseconds(50));
timer.async_wait([&](const boost::system::error_code& ec) {
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() {
try {
net::io_context ioc;
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} };
int next_id = 0;
try {
{
std::lock_guard<std::mutex> lock(g_boxes_mutex);
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 = 0;
std::cout << "Server started on port 8080...\n";
std::cout << "Server started on port 8080...\n";
auto do_accept = [&](auto& self_fn) -> void {
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<Session>(std::move(socket), next_id++)->run();
}
self_fn(self_fn);
});
};
auto do_accept = [&](auto& self_fn) -> void {
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<Session>(std::move(socket), next_id++)->run();
}
self_fn(self_fn);
});
};
net::steady_timer timer(ioc);
update_world(timer, ioc);
do_accept(do_accept);
ioc.run();
}
catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
net::steady_timer timer(ioc);
update_world(timer, ioc);
do_accept(do_accept);
ioc.run();
}
catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}

View File

@ -10,6 +10,7 @@
#include <random>
#include <cmath>
#include <algorithm>
#include <functional>
#ifdef __ANDROID__
#include <android/log.h>
#endif
@ -268,83 +269,119 @@ namespace ZL
bool explosionCfgLoaded = explosionEmitter.loadFromJsonFile("resources/config/explosion_config.json", renderer, CONST_ZIP_FILE);
explosionEmitter.setEmissionPoints(std::vector<Vector3f>());
projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
uiManager.loadFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE);
uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
static bool isExitButtonAnimating = 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)) {
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.loadFromFile("resources/config/main_menu.json", renderer, CONST_ZIP_FILE);
std::function<void()> loadGameplayUI;
loadGameplayUI = [this]() {
uiManager.loadFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE);
uiManager.setAnimationCallback("exitButton", "bgScroll", []() {
std::cerr << "Exit button bgScroll animation finished" << std::endl;
g_exitBgAnimating = false;
});
// Set UI button callbacks
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.startAnimationOnNode("backgroundNode", "bgScroll");
static bool isExitButtonAnimating = 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)) {
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();
});
}
}
});
uiManager.setButtonCallback("shootButton", [this](const std::string& name) {
uint64_t now = SDL_GetTicks64();
if (now - lastProjectileFireTime >= static_cast<uint64_t>(projectileCooldownMs)) {
lastProjectileFireTime = now;
fireProjectiles();
}
});
else {
std::cerr << "Failed to open settings menu after animations" << std::endl;
}
});
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
int newVel = roundf(value * 10);
if (newVel != Environment::shipState.selectedVelocity) {
newShipVelocity = newVel;
}
uiManager.setAnimationCallback("exitButton", "bgScroll", []() {
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) {
uint64_t now = SDL_GetTicks64();
if (now - lastProjectileFireTime >= static_cast<uint64_t>(projectileCooldownMs)) {
lastProjectileFireTime = now;
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 };
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(worldForward.x()) + ":" +
std::to_string(worldForward.y()) + ":" +
std::to_string(worldForward.z()) + ":" +
"2";
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>(
@ -692,6 +729,27 @@ namespace ZL
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
for (auto const& [id, remotePlayer] : latestRemotePlayers) {
@ -701,7 +759,7 @@ namespace ZL
}
ClientState playerState = remotePlayer.fetchClientStateAtTime(now);
renderer.PushMatrix();
renderer.LoadIdentity();
@ -770,7 +828,16 @@ namespace ZL
continue;
float uiX = sx, uiY = sy;
float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);
//float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);
float scale;
if (dist > CLOSE_DIST) {
scale = 0.4f;
}
else {
float t = 1.0f - (dist / CLOSE_DIST);
scale = 0.4f + (MAX_SCALE - 0.4f) * t;
}
scale = std::clamp(scale, MIN_SCALE, MAX_SCALE);
// Дефолтный лейбл
std::string label = "Player (" + std::to_string(st.id) + ") " + std::to_string((int)dist) + "m";
@ -854,7 +921,7 @@ namespace ZL
float radians = atan2f(diffy, diffx);
discreteAngle = static_cast<int>(radians * 180.0f / M_PI);
if (discreteAngle < 0) discreteAngle += 360;
}
else
{
@ -868,7 +935,7 @@ namespace ZL
discreteMag = 0.0f;
}
if (discreteAngle != Environment::shipState.discreteAngle || discreteMag != Environment::shipState.discreteMag) {
Environment::shipState.discreteAngle = discreteAngle;
Environment::shipState.discreteMag = discreteMag;
@ -1247,7 +1314,7 @@ namespace ZL
{
if (event.key.keysym.sym == SDLK_i)
{
}
}*/
#endif
@ -1255,6 +1322,57 @@ namespace ZL
render();
mainThreadHandler.processMainThreadTasks();
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;
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)

View File

@ -121,7 +121,9 @@ namespace ZL {
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 = 1.5f;
static constexpr float MAX_SCALE = 0.8f;
static constexpr float CLOSE_DIST = 600.0f;
bool serverBoxesApplied = false;
};

View File

@ -14,7 +14,7 @@ constexpr float SHIP_ACCEL = 1.0f * 1000.0f;
constexpr float ROTATION_SENSITIVITY = 0.002f;
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
struct ClientState {

View File

@ -4,15 +4,17 @@
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 {};
}
}

View File

@ -17,9 +17,14 @@ namespace ZL {
bool IsConnected() const override { return true; }
int GetClientId() const { return 1; }
std::vector<ProjectileInfo> getPendingProjectiles();
std::unordered_map<int, ClientStateInterval> getRemotePlayers() override {
return std::unordered_map<int, ClientStateInterval>();
}
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> getServerBoxes() override {
return {};
}
};
}

View File

@ -7,6 +7,13 @@
// NetworkInterface.h - »нтерфейс дл¤ разных типов соединений
namespace ZL {
struct ProjectileInfo {
int shooterId = -1;
uint64_t clientTime = 0;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
Eigen::Vector3f direction = Eigen::Vector3f::Zero();
};
class INetworkClient {
public:
virtual ~INetworkClient() = default;
@ -15,5 +22,9 @@ namespace ZL {
virtual bool IsConnected() const = 0;
virtual void Poll() = 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;
};
}

View File

@ -6,170 +6,239 @@
// Вспомогательный 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;
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) {
try {
boost::asio::ip::tcp::resolver resolver(ioc_);
auto const results = resolver.resolve(host, std::to_string(port));
void WebSocketClient::Connect(const std::string& host, uint16_t port) {
try {
boost::asio::ip::tcp::resolver resolver(ioc_);
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 для простоты старта
boost::beast::get_lowest_layer(*ws_).connect(results);
ws_->handshake(host, "/");
// Выполняем синхронный коннект и handshake для простоты старта
boost::beast::get_lowest_layer(*ws_).connect(results);
ws_->handshake(host, "/");
connected = true;
connected = true;
// Запускаем асинхронное чтение в пуле потоков TaskManager
startAsyncRead();
// Запускаем асинхронное чтение в пуле потоков TaskManager
startAsyncRead();
}
catch (std::exception& e) {
std::cerr << "Network Error: " << e.what() << std::endl;
}
}
}
catch (std::exception& e) {
std::cerr << "Network Error: " << e.what() << std::endl;
}
}
void WebSocketClient::startAsyncRead() {
ws_->async_read(buffer_, [this](boost::beast::error_code ec, std::size_t bytes) {
if (!ec) {
std::string msg = boost::beast::buffers_to_string(buffer_.data());
buffer_.consume(bytes);
processIncomingMessage(msg);
startAsyncRead();
}
else {
connected = false;
}
});
}
void WebSocketClient::startAsyncRead() {
ws_->async_read(buffer_, [this](boost::beast::error_code ec, std::size_t bytes) {
if (!ec) {
std::string msg = boost::beast::buffers_to_string(buffer_.data());
buffer_.consume(bytes);
processIncomingMessage(msg);
startAsyncRead();
}
else {
connected = false;
}
});
}
void WebSocketClient::processIncomingMessage(const std::string& msg) {
// Логика парсинга...
if (msg.rfind("ID:", 0) == 0) {
clientId = std::stoi(msg.substr(3));
}
void WebSocketClient::processIncomingMessage(const std::string& msg) {
// Логика парсинга...
if (msg.rfind("ID:", 0) == 0) {
clientId = std::stoi(msg.substr(3));
}
// Безопасно кладем в очередь для главного потока
std::lock_guard<std::mutex> lock(queueMutex);
messageQueue.push(msg);
}
// Безопасно кладем в очередь для главного потока
std::lock_guard<std::mutex> lock(queueMutex);
messageQueue.push(msg);
}
void WebSocketClient::Poll() {
std::lock_guard<std::mutex> lock(queueMutex);
std::vector<ProjectileInfo> WebSocketClient::getPendingProjectiles() {
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:
nowTime -= std::chrono::milliseconds(CLIENT_DELAY);
auto nowTime = std::chrono::system_clock::now();
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
nowTime.time_since_epoch()
).count();
//Apply server delay:
nowTime -= std::chrono::milliseconds(CLIENT_DELAY);
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
nowTime.time_since_epoch()
).count();
std::string msg = messageQueue.front();
messageQueue.pop();
std::string msg = messageQueue.front();
messageQueue.pop();
if (msg.rfind("EVENT:", 0) == 0) {
auto parts = split(msg, ':');
if (parts.size() < 5) continue; // EVENT:ID:TYPE:TIME:DATA...
// Обработка списка коробок от сервера
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;
}
int remoteId = std::stoi(parts[1]);
std::string subType = parts[2];
uint64_t sentTime = std::stoull(parts[3]);
if (msg.rfind("PROJECTILE:", 0) == 0) {
auto parts = split(msg, ':');
if (parts.size() >= 9) {
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])
);
pi.direction = Eigen::Vector3f(
std::stof(parts[6]),
std::stof(parts[7]),
std::stof(parts[8])
);
std::lock_guard<std::mutex> pl(projMutex_);
pendingProjectiles_.push_back(pi);
}
catch (...) {
}
}
continue;
}
ClientState remoteState;
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();
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.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
{
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);
{
std::lock_guard<std::mutex> pLock(playersMutex);
auto& rp = remotePlayers[remoteId];
rp.add_state(remoteState);
}
}
rp.add_state(remoteState);
}
}
}
}
void WebSocketClient::Send(const std::string& message) {
if (!connected) return;
}
}
void WebSocketClient::Send(const std::string& message) {
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_);
writeQueue_.push(ss);
std::lock_guard<std::mutex> lock(writeMutex_);
writeQueue_.push(ss);
// Если сейчас ничего не записывается, инициируем первую запись
if (!isWriting_) {
doWrite();
}
}
// Если сейчас ничего не записывается, инициируем первую запись
if (!isWriting_) {
doWrite();
}
}
void WebSocketClient::doWrite() {
// Эта функция всегда вызывается под мьютексом или из колбэка
if (writeQueue_.empty()) {
isWriting_ = false;
return;
}
void WebSocketClient::doWrite() {
// Эта функция всегда вызывается под мьютексом или из колбэка
if (writeQueue_.empty()) {
isWriting_ = false;
return;
}
isWriting_ = true;
auto message = writeQueue_.front();
isWriting_ = true;
auto message = writeQueue_.front();
// Захватываем self (shared_from_this), чтобы объект не удалился во время записи
ws_->async_write(
boost::asio::buffer(*message),
[this, message](boost::beast::error_code ec, std::size_t) {
if (ec) {
connected = false;
return;
}
// Захватываем self (shared_from_this), чтобы объект не удалился во время записи
ws_->async_write(
boost::asio::buffer(*message),
[this, message](boost::beast::error_code ec, std::size_t) {
if (ec) {
connected = false;
return;
}
std::lock_guard<std::mutex> lock(writeMutex_);
writeQueue_.pop(); // Удаляем отправленное сообщение
doWrite(); // Проверяем следующее
}
);
}
std::lock_guard<std::mutex> lock(writeMutex_);
writeQueue_.pop(); // Удаляем отправленное сообщение
doWrite(); // Проверяем следующее
}
);
}
}
#endif

View File

@ -34,6 +34,13 @@ namespace ZL {
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_;
void startAsyncRead();
void processIncomingMessage(const std::string& msg);
@ -54,6 +61,13 @@ namespace ZL {
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;
};
}
#endif