Compare commits

...

6 Commits

Author SHA1 Message Date
Vladislav Khorev
147aa266d5 Merge branch 'linux' into ShipSpawnLinux 2026-03-05 07:28:05 +00:00
Vladislav Khorev
70ef730e86 Merge branch 'main' into ShipSpawn 2026-03-05 10:24:17 +03:00
Vladislav Khorev
92b288d978 fixing bugs 2026-03-05 10:15:55 +03:00
05a13a40e7 fixed: Box destroition 2026-03-04 16:37:00 +06:00
78046e5e2d Merge remote-tracking branch 'origin/main' into ShipSpawn 2026-03-02 17:49:09 +06:00
26233f934f fixed: spawning ships 2026-03-02 17:47:46 +06:00
6 changed files with 220 additions and 15 deletions

View File

@ -23,6 +23,15 @@ namespace http = beast::http;
namespace websocket = beast::websocket;
namespace net = boost::asio;
using tcp = net::ip::tcp;
static constexpr float kWorldZOffset = 45000.0f;
static const Eigen::Vector3f kWorldOffset(0.0f, 0.0f, kWorldZOffset);
static constexpr float kShipRadius = 15.0f;
static constexpr float kSpawnShipMargin = 25.0f;
static constexpr float kSpawnBoxMargin = 15.0f;
static constexpr float kSpawnZJitter = 60.0f;
Eigen::Vector3f PickSafeSpawnPos(int forPlayerId);
struct DeathInfo {
int targetId = -1;
@ -92,6 +101,10 @@ class Session : public std::enable_shared_from_this<Session> {
public:
ClientStateInterval timedClientStates;
bool joined_ = false;
bool hasReservedSpawn_ = false;
Eigen::Vector3f reservedSpawn_ = Eigen::Vector3f(0.0f, 0.0f, kWorldZOffset);
std::string nickname = "Player";
int shipType = 0;
@ -102,6 +115,9 @@ public:
int get_id() const { return id_; }
bool hasSpawnReserved() const { return hasReservedSpawn_; }
const Eigen::Vector3f& reservedSpawn() const { return reservedSpawn_; }
bool fetchStateAtTime(std::chrono::system_clock::time_point targetTime, ClientState& outState) const {
if (timedClientStates.canFetchClientStateAtTime(targetTime)) {
outState = timedClientStates.fetchClientStateAtTime(targetTime);
@ -160,9 +176,18 @@ private:
std::lock_guard<std::mutex> lock(g_boxes_mutex);
std::string boxMsg = "BOXES:";
bool first = true;
for (size_t i = 0; i < g_serverBoxes.size(); ++i) {
const auto& box = g_serverBoxes[i];
if (box.destroyed) continue;
Eigen::Quaternionf q(box.rotation);
if (!first) boxMsg += "|";
first = false;
boxMsg += std::to_string(i) + ":" +
std::to_string(box.position.x()) + ":" +
std::to_string(box.position.y()) + ":" +
@ -171,10 +196,10 @@ private:
std::to_string(q.x()) + ":" +
std::to_string(q.y()) + ":" +
std::to_string(q.z()) + ":" +
(std::to_string(box.destroyed ? 1 : 0)) + "|";
"0";
}
if (!boxMsg.empty() && boxMsg.back() == '|') boxMsg.pop_back();
// Если все коробки уничтожены — отправится просто "BOXES:" (это нормально)
send_message(boxMsg);
}
@ -189,7 +214,11 @@ public:
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_));
auto now_tp = std::chrono::system_clock::now();
uint64_t now_ms = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(now_tp.time_since_epoch()).count());
self->send_message("ID:" + std::to_string(self->id_) + ":" + std::to_string(now_ms));
self->do_read();
}
});
@ -281,7 +310,45 @@ private:
this->nickname = nick;
this->shipType = sType;
this->joined_ = true;
auto now_tp = std::chrono::system_clock::now();
uint64_t now_ms = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(now_tp.time_since_epoch()).count());
Eigen::Vector3f spawnPos = PickSafeSpawnPos(id_);
this->hasReservedSpawn_ = true;
this->reservedSpawn_ = spawnPos;
ClientState st;
st.id = id_;
st.position = spawnPos;
st.rotation = Eigen::Matrix3f::Identity();
st.currentAngularVelocity = Eigen::Vector3f::Zero();
st.velocity = 0.0f;
st.selectedVelocity = 0;
st.discreteMag = 0.0f;
st.discreteAngle = -1;
st.lastUpdateServerTime = now_tp;
st.nickname = this->nickname;
st.shipType = this->shipType;
timedClientStates.add_state(st);
this->send_message(
"SPAWN:" + std::to_string(id_) + ":" + std::to_string(now_ms) + ":" + st.formPingMessageContent()
);
std::string eventMsg =
"EVENT:" + std::to_string(id_) + ":UPD:" + std::to_string(now_ms) + ":" + st.formPingMessageContent();
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
if (session->get_id() == id_) continue;
session->send_message(eventMsg);
}
}
std::cout << "Server: Player " << id_ << " joined as [" << nick << "] shipType=" << sType << std::endl;
std::string info = "PLAYERINFO:" + std::to_string(id_) + ":" + nick + ":" + std::to_string(sType);
@ -298,12 +365,16 @@ private:
for (auto& session : g_sessions) {
if (session->get_id() == this->id_) continue;
std::string otherInfo = "PLAYERINFO:" + std::to_string(session->get_id()) + ":" + session->nickname + ":" + std::to_string(session->shipType);
// Отправляем именно новому клиенту
this->send_message(otherInfo);
}
}
}
else if (type == "UPD") {
if (!joined_) {
std::cout << "Server: Ignoring UPD before JOIN from " << id_ << std::endl;
return;
}
{
std::lock_guard<std::mutex> gd(g_dead_mutex);
if (g_dead_players.find(id_) != g_dead_players.end()) {
@ -338,7 +409,13 @@ private:
ClientState st;
st.id = id_;
st.position = Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
Eigen::Vector3f spawnPos = PickSafeSpawnPos(id_);
st.position = spawnPos;
this->hasReservedSpawn_ = true;
this->reservedSpawn_ = spawnPos;
st.rotation = Eigen::Matrix3f::Identity();
st.currentAngularVelocity = Eigen::Vector3f::Zero();
st.velocity = 0.0f;
@ -350,7 +427,9 @@ private:
st.shipType = this->shipType;
timedClientStates.add_state(st);
this->send_message(
"SPAWN:" + std::to_string(id_) + ":" + std::to_string(now_ms) + ":" + st.formPingMessageContent()
);
std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_);
broadcastToAll(respawnMsg);
@ -436,6 +515,71 @@ private:
};
Eigen::Vector3f PickSafeSpawnPos(int forPlayerId)
{
static thread_local std::mt19937 rng{ std::random_device{}() };
std::scoped_lock lock(g_boxes_mutex, g_sessions_mutex, g_dead_mutex);
auto isSafe = [&](const Eigen::Vector3f& pWorld) -> bool
{
for (const auto& box : g_serverBoxes) {
if (box.destroyed) continue;
Eigen::Vector3f boxWorld = box.position + kWorldOffset;
float minDist = kShipRadius + box.collisionRadius + kSpawnBoxMargin;
if ((pWorld - boxWorld).squaredNorm() < minDist * minDist)
return false;
}
for (const auto& s : g_sessions) {
int pid = s->get_id();
if (pid == forPlayerId) continue;
if (g_dead_players.count(pid)) continue;
Eigen::Vector3f otherPos;
if (!s->timedClientStates.timedStates.empty()) {
otherPos = s->timedClientStates.timedStates.back().position;
}
else if (s->hasSpawnReserved()) {
otherPos = s->reservedSpawn();
}
else {
continue;
}
float minDist = (kShipRadius * 2.0f) + kSpawnShipMargin;
if ((pWorld - otherPos).squaredNorm() < minDist * minDist)
return false;
}
return true;
};
const float radii[] = { 150.f, 250.f, 400.f, 650.f, 1000.f, 1600.f };
for (float r : radii) {
std::uniform_real_distribution<float> dxy(-r, r);
std::uniform_real_distribution<float> dz(-kSpawnZJitter, kSpawnZJitter);
for (int attempt = 0; attempt < 250; ++attempt) {
Eigen::Vector3f cand(
dxy(rng),
dxy(rng),
kWorldZOffset + dz(rng)
);
if (isSafe(cand))
return cand;
}
}
int a = (forPlayerId % 10);
int b = ((forPlayerId / 10) % 10);
return Eigen::Vector3f(600.0f + a * 100.0f, -600.0f + b * 100.0f, kWorldZOffset);
}
void broadcastToAll(const std::string& message) {
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (const auto& session : g_sessions) {

View File

@ -16,6 +16,7 @@
#endif
#ifdef NETWORK
#include "network/WebSocketClientBase.h"
#ifdef EMSCRIPTEN
#include "network/WebSocketClientEmscripten.h"
#else
@ -170,8 +171,8 @@ namespace ZL
networkClient = std::make_unique<WebSocketClientEmscripten>();
networkClient->Connect("localhost", 8081);
#else
//networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext());
//networkClient->Connect("localhost", 8081);
networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext());
networkClient->Connect("localhost", 8081);
#endif
if (networkClient) {
@ -458,6 +459,31 @@ namespace ZL
}
//#endif
networkClient->Poll();
#ifdef NETWORK
auto* wsBase = dynamic_cast<ZL::WebSocketClientBase*>(networkClient.get());
if (wsBase) {
auto spawns = wsBase->getPendingSpawns();
for (auto& st : spawns) {
if (st.id == wsBase->getClientId()) {
// применяем к локальному кораблю
ZL::Environment::shipState.position = st.position;
ZL::Environment::shipState.rotation = st.rotation;
// обнуляем движение чтобы не было рывков
ZL::Environment::shipState.currentAngularVelocity = Eigen::Vector3f::Zero();
ZL::Environment::shipState.velocity = 0.0f;
ZL::Environment::shipState.selectedVelocity = 0;
ZL::Environment::shipState.discreteMag = 0.0f;
ZL::Environment::shipState.discreteAngle = -1;
std::cout << "Game: Applied SPAWN at "
<< st.position.x() << ", "
<< st.position.y() << ", "
<< st.position.z() << std::endl;
}
}
}
#endif
}
mainThreadHandler.processMainThreadTasks();

View File

@ -350,6 +350,10 @@ namespace ZL
}
boxAlive.resize(boxCoordsArr.size(), true);
#ifdef NETWORK
std::fill(boxAlive.begin(), boxAlive.end(), false);
serverBoxesApplied = false;
#endif
ZL::CheckGlError();
boxLabels.clear();
boxLabels.reserve(boxCoordsArr.size());
@ -662,10 +666,10 @@ namespace ZL
boxRenderArr[i].RefreshVBO();
}
boxAlive.assign(boxCoordsArr.size(), true);
if (destroyedFlags.size() == boxAlive.size()) {
for (size_t i = 0; i < boxAlive.size(); ++i) {
if (destroyedFlags[i]) boxAlive[i] = false;
}
size_t n = (std::min)(destroyedFlags.size(), boxAlive.size());
for (size_t i = 0; i < n; ++i) {
if (destroyedFlags[i]) boxAlive[i] = false; // destroyed => не рисуем
}
serverBoxesApplied = true;
}

View File

@ -55,7 +55,8 @@ EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void
static void applyResize(int logicalW, int logicalH) {
// Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung)
/*double dpr = emscripten_get_device_pixel_ratio();
//double dpr = emscripten_get_device_pixel_ratio();
double dpr = 1; // low quality
// Вычисляем реальные физические пиксели
int physicalW = static_cast<int>(logicalW * dpr);
@ -81,7 +82,7 @@ static void applyResize(int logicalW, int logicalH) {
e.window.event = SDL_WINDOWEVENT_RESIZED;
e.window.data1 = physicalW;
e.window.data2 = physicalH;
SDL_PushEvent(&e);*/
SDL_PushEvent(&e);
}
EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) {

View File

@ -144,7 +144,7 @@ namespace ZL {
if (msg.rfind("PROJECTILE:", 0) == 0) {
//auto parts = split(msg, ':');
if (parts.size() >= 10) {
if (parts.size() >= 11) {
try {
ProjectileInfo pi;
pi.shooterId = std::stoi(parts[1]);
@ -190,7 +190,28 @@ namespace ZL {
}
return;
}
if (msg.rfind("SPAWN:", 0) == 0) {
// SPAWN:playerId:serverTime:<14 полей>
if (parts.size() >= 3 + 14) {
try {
int pid = std::stoi(parts[1]);
uint64_t serverTime = std::stoull(parts[2]);
ClientState st;
st.id = pid;
std::chrono::system_clock::time_point tp{ std::chrono::milliseconds(serverTime) };
st.lastUpdateServerTime = tp;
// данные начинаются с parts[3]
st.handle_full_sync(parts, 3);
pendingSpawns_.push_back(st);
std::cout << "Client: SPAWN received for player " << pid << std::endl;
}
catch (...) {}
}
return;
}
if (msg.rfind("EVENT:", 0) == 0) {
//auto parts = split(msg, ':');
if (parts.size() < 5) return; // EVENT:ID:TYPE:TIME:DATA...
@ -334,6 +355,12 @@ namespace ZL {
copy.swap(pendingBoxDestructions_);
return copy;
}
std::vector<ClientState> WebSocketClientBase::getPendingSpawns() {
std::vector<ClientState> copy;
copy.swap(pendingSpawns_);
return copy;
}
}
#endif

View File

@ -22,6 +22,7 @@ namespace ZL {
std::vector<BoxDestroyedInfo> pendingBoxDestructions_;
int clientId = -1;
int64_t timeOffset = 0;
std::vector<ClientState> pendingSpawns_;
public:
int GetClientId() const override { return clientId; }
@ -48,6 +49,8 @@ namespace ZL {
std::vector<DeathInfo> getPendingDeaths() override;
std::vector<int> getPendingRespawns() override;
std::vector<BoxDestroyedInfo> getPendingBoxDestructions() override;
std::vector<ClientState> getPendingSpawns();
int getClientId() const { return clientId; }
};
}