Compare commits

...

10 Commits

Author SHA1 Message Date
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
Vladislav Khorev
cca1d0ece2 Merge branch 'spark' 2026-03-05 09:10:38 +03:00
Vladislav Khorev
8effdd5dfa Merge branch 'sergey' 2026-03-05 09:10:00 +03:00
Vladislav Khorev
e78429b600 Working on running the game even in Telegram 2026-03-04 21:41:51 +03:00
05a13a40e7 fixed: Box destroition 2026-03-04 16:37:00 +06:00
Vlad
3879eb1d75 fixed web ship types boxes / local boxes collision and projectiles 2026-03-03 19:17:07 +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
Vlad
167c04b107 install 2026-03-02 14:05:23 +06:00
15 changed files with 392 additions and 79 deletions

View File

@ -63,8 +63,6 @@ set(SOURCES
../src/network/LocalClient.cpp ../src/network/LocalClient.cpp
../src/network/ClientState.h ../src/network/ClientState.h
../src/network/ClientState.cpp ../src/network/ClientState.cpp
../src/network/WebSocketClient.h
../src/network/WebSocketClient.cpp
../src/network/WebSocketClientBase.h ../src/network/WebSocketClientBase.h
../src/network/WebSocketClientBase.cpp ../src/network/WebSocketClientBase.cpp
../src/network/WebSocketClientEmscripten.h ../src/network/WebSocketClientEmscripten.h
@ -95,10 +93,7 @@ set(ENABLE_COMMONCRYPTO OFF CACHE BOOL "" FORCE)
add_subdirectory("../thirdparty/libzip-1.11.4" libzip-build) add_subdirectory("../thirdparty/libzip-1.11.4" libzip-build)
# Линковка: target_link_libraries(space-game001 PRIVATE zip z websocket.js)
# 'zip' берется из add_subdirectory
# 'z' - это системный zlib Emscripten-а (флаг -sUSE_ZLIB=1 добавим ниже)
target_link_libraries(space-game001 PRIVATE zip z websocket)
# Эмскриптен-флаги # Эмскриптен-флаги
set(EMSCRIPTEN_FLAGS set(EMSCRIPTEN_FLAGS
@ -107,8 +102,8 @@ set(EMSCRIPTEN_FLAGS
"-sUSE_LIBPNG=1" "-sUSE_LIBPNG=1"
"-sUSE_ZLIB=1" "-sUSE_ZLIB=1"
"-sUSE_SDL_TTF=2" "-sUSE_SDL_TTF=2"
"-pthread" #"-pthread"
"-sUSE_PTHREADS=1" #"-sUSE_PTHREADS=1"
"-fexceptions" "-fexceptions"
"-DNETWORK" "-DNETWORK"
) )
@ -120,8 +115,9 @@ target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
set(EMSCRIPTEN_LINK_FLAGS set(EMSCRIPTEN_LINK_FLAGS
${EMSCRIPTEN_FLAGS} ${EMSCRIPTEN_FLAGS}
"-O2" "-O2"
"-sPTHREAD_POOL_SIZE=4" #"-sPTHREAD_POOL_SIZE=4"
"-sALLOW_MEMORY_GROWTH=1" "-sALLOW_MEMORY_GROWTH=1"
"-sFULL_ES3=1"
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/loading.png@resources/loading.png" "--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/loading.png@resources/loading.png"
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/shaders@resources/shaders" "--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/shaders@resources/shaders"
) )

View File

@ -3,6 +3,7 @@
Activate the environment: Activate the environment:
``` ```
C:\Work\Projects\emsdk\emsdk.bat install latest
C:\Work\Projects\emsdk\emsdk.bat activate latest C:\Work\Projects\emsdk\emsdk.bat activate latest
C:\Work\Projects\emsdk\emsdk_env.bat C:\Work\Projects\emsdk\emsdk_env.bat
``` ```

View File

@ -0,0 +1,2 @@
<!doctypehtml><html lang=en-us><head><meta charset=utf-8><meta content="text/html; charset=utf-8"http-equiv=Content-Type><title>Emscripten-Generated Code</title><style>body{font-family:arial;margin:0;padding:none}.emscripten{padding-right:0;margin-left:auto;margin-right:auto;display:block}div.emscripten{text-align:center}div.emscripten_border{border:1px solid #000}canvas.emscripten{border:0 none;background-color:#000}#emscripten_logo{display:inline-block;margin:0;padding:6px;width:265px}.spinner{height:30px;width:30px;margin:0;margin-top:20px;margin-left:20px;display:inline-block;vertical-align:top;-webkit-animation:rotation .8s linear infinite;-moz-animation:rotation .8s linear infinite;-o-animation:rotation .8s linear infinite;animation:rotation .8s linear infinite;border-left:5px solid #ebebeb;border-right:5px solid #ebebeb;border-bottom:5px solid #ebebeb;border-top:5px solid #787878;border-radius:100%;background-color:#bdd72e}@-webkit-keyframes rotation{from{-webkit-transform:rotate(0)}to{-webkit-transform:rotate(360deg)}}@-moz-keyframes rotation{from{-moz-transform:rotate(0)}to{-moz-transform:rotate(360deg)}}@-o-keyframes rotation{from{-o-transform:rotate(0)}to{-o-transform:rotate(360deg)}}@keyframes rotation{from{transform:rotate(0)}to{transform:rotate(360deg)}}#status{display:inline-block;vertical-align:top;margin-top:30px;margin-left:20px;font-weight:700;color:#787878}#progress{height:20px;width:300px}#controls{display:inline-block;float:right;vertical-align:top;margin-top:30px;margin-right:20px}#output{width:100%;height:200px;margin:0 auto;margin-top:10px;border-left:0;border-right:0px;padding-left:0;padding-right:0;display:block;background-color:#000;color:#fff;font-family:'Lucida Console',Monaco,monospace;outline:0}</style></head><body><script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script><a href=http://emscripten.org><img id=emscripten_logo src=""></a><div class=spinner id=spinner></div><div class=emscripten id=status>Downloading...</div><span id=controls><span><input type=checkbox id=resize>Resize canvas</span> <span><input type=checkbox id=pointerLock checked>Lock/hide mouse pointer    </span><span><input type=button onclick='Module.requestFullscreen(document.getElementById("pointerLock").checked,document.getElementById("resize").checked)'value=Fullscreen></span></span><div class=emscripten><progress hidden id=progress max=100 value=0></progress></div><div class=emscripten_border><canvas class=emscripten id=canvas oncontextmenu=event.preventDefault() tabindex=-1></canvas></div><textarea id=output rows=8></textarea><script>var statusElement=document.getElementById("status"),progressElement=document.getElementById("progress"),spinnerElement=document.getElementById("spinner"),canvasElement=document.getElementById("canvas"),outputElement=document.getElementById("output");outputElement&&(outputElement.value=""),canvasElement.addEventListener("webglcontextlost",(e=>{alert("WebGL context lost. You will need to reload the page."),e.preventDefault()}),!1);var Module={print(...e){if(console.log(...e),outputElement){var t=e.join(" ");outputElement.value+=t+"\n",outputElement.scrollTop=outputElement.scrollHeight}},canvas:canvasElement,setStatus(e){if(Module.setStatus.last||(Module.setStatus.last={time:Date.now(),text:""}),e!==Module.setStatus.last.text){var t=e.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/),n=Date.now();t&&n-Module.setStatus.last.time<30||(Module.setStatus.last.time=n,Module.setStatus.last.text=e,t?(e=t[1],progressElement.value=100*parseInt(t[2]),progressElement.max=100*parseInt(t[4]),progressElement.hidden=!1,spinnerElement.hidden=!1):(progressElement.value=null,progressElement.max=null,progressElement.hidden=!0,e||(spinnerElement.style.display="none")),statusElement.innerHTML=e)}},totalDependencies:0,monitorRunDependencies(e){this.totalDependencies=Math.max(this.totalDependencies,e),Module.setStatus(e?"Preparing... ("+(this.totalDependencies-e)+"/"+this.totalDependencies+")":"All downloads complete.")}};Module.setStatus("Downloading..."),window.onerror=e=>{Module.setStatus("Exception thrown, see JavaScript console"),spinnerElement.style.display="none",Module.setStatus=e=>{e&&console.error("[post-exception status] "+e)}}</script><script async src="space-game001.js" crossorigin="anonymous"></script></body></html>

View File

@ -23,6 +23,15 @@ namespace http = beast::http;
namespace websocket = beast::websocket; namespace websocket = beast::websocket;
namespace net = boost::asio; namespace net = boost::asio;
using tcp = net::ip::tcp; 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 { struct DeathInfo {
int targetId = -1; int targetId = -1;
@ -92,6 +101,10 @@ class Session : public std::enable_shared_from_this<Session> {
public: public:
ClientStateInterval timedClientStates; ClientStateInterval timedClientStates;
bool joined_ = false;
bool hasReservedSpawn_ = false;
Eigen::Vector3f reservedSpawn_ = Eigen::Vector3f(0.0f, 0.0f, kWorldZOffset);
std::string nickname = "Player"; std::string nickname = "Player";
int shipType = 0; int shipType = 0;
@ -102,6 +115,9 @@ public:
int get_id() const { return id_; } 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 { bool fetchStateAtTime(std::chrono::system_clock::time_point targetTime, ClientState& outState) const {
if (timedClientStates.canFetchClientStateAtTime(targetTime)) { if (timedClientStates.canFetchClientStateAtTime(targetTime)) {
outState = timedClientStates.fetchClientStateAtTime(targetTime); outState = timedClientStates.fetchClientStateAtTime(targetTime);
@ -160,9 +176,18 @@ private:
std::lock_guard<std::mutex> lock(g_boxes_mutex); std::lock_guard<std::mutex> lock(g_boxes_mutex);
std::string boxMsg = "BOXES:"; std::string boxMsg = "BOXES:";
bool first = true;
for (size_t i = 0; i < g_serverBoxes.size(); ++i) { for (size_t i = 0; i < g_serverBoxes.size(); ++i) {
const auto& box = g_serverBoxes[i]; const auto& box = g_serverBoxes[i];
if (box.destroyed) continue;
Eigen::Quaternionf q(box.rotation); Eigen::Quaternionf q(box.rotation);
if (!first) boxMsg += "|";
first = false;
boxMsg += std::to_string(i) + ":" + boxMsg += std::to_string(i) + ":" +
std::to_string(box.position.x()) + ":" + std::to_string(box.position.x()) + ":" +
std::to_string(box.position.y()) + ":" + std::to_string(box.position.y()) + ":" +
@ -171,10 +196,10 @@ private:
std::to_string(q.x()) + ":" + std::to_string(q.x()) + ":" +
std::to_string(q.y()) + ":" + std::to_string(q.y()) + ":" +
std::to_string(q.z()) + ":" + 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); send_message(boxMsg);
} }
@ -189,7 +214,11 @@ public:
timer->expires_after(std::chrono::milliseconds(100)); timer->expires_after(std::chrono::milliseconds(100));
timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) { timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) {
if (!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(); self->do_read();
} }
}); });
@ -281,7 +310,45 @@ private:
this->nickname = nick; this->nickname = nick;
this->shipType = sType; 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::cout << "Server: Player " << id_ << " joined as [" << nick << "] shipType=" << sType << std::endl;
std::string info = "PLAYERINFO:" + std::to_string(id_) + ":" + nick + ":" + std::to_string(sType); std::string info = "PLAYERINFO:" + std::to_string(id_) + ":" + nick + ":" + std::to_string(sType);
@ -298,12 +365,16 @@ private:
for (auto& session : g_sessions) { for (auto& session : g_sessions) {
if (session->get_id() == this->id_) continue; if (session->get_id() == this->id_) continue;
std::string otherInfo = "PLAYERINFO:" + std::to_string(session->get_id()) + ":" + session->nickname + ":" + std::to_string(session->shipType); std::string otherInfo = "PLAYERINFO:" + std::to_string(session->get_id()) + ":" + session->nickname + ":" + std::to_string(session->shipType);
// Отправляем именно новому клиенту
this->send_message(otherInfo); this->send_message(otherInfo);
} }
} }
} }
else if (type == "UPD") { 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); std::lock_guard<std::mutex> gd(g_dead_mutex);
if (g_dead_players.find(id_) != g_dead_players.end()) { if (g_dead_players.find(id_) != g_dead_players.end()) {
@ -338,7 +409,13 @@ private:
ClientState st; ClientState st;
st.id = id_; 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.rotation = Eigen::Matrix3f::Identity();
st.currentAngularVelocity = Eigen::Vector3f::Zero(); st.currentAngularVelocity = Eigen::Vector3f::Zero();
st.velocity = 0.0f; st.velocity = 0.0f;
@ -350,7 +427,9 @@ private:
st.shipType = this->shipType; st.shipType = this->shipType;
timedClientStates.add_state(st); 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_); std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_);
broadcastToAll(respawnMsg); 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) { void broadcastToAll(const std::string& message) {
std::lock_guard<std::mutex> lock(g_sessions_mutex); std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (const auto& session : g_sessions) { for (const auto& session : g_sessions) {

View File

@ -16,6 +16,7 @@
#endif #endif
#ifdef NETWORK #ifdef NETWORK
#include "network/WebSocketClientBase.h"
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
#include "network/WebSocketClientEmscripten.h" #include "network/WebSocketClientEmscripten.h"
#else #else
@ -57,9 +58,9 @@ namespace ZL
#endif #endif
Game::Game() Game::Game()
: window(nullptr) : /*window(nullptr)
, glContext(nullptr) , glContext(nullptr)
, newTickCount(0) , */newTickCount(0)
, lastTickCount(0) , lastTickCount(0)
, menuManager(renderer) , menuManager(renderer)
, space(renderer, taskManager, mainThreadHandler, networkClient, menuManager) , space(renderer, taskManager, mainThreadHandler, networkClient, menuManager)
@ -67,12 +68,13 @@ namespace ZL
} }
Game::~Game() { Game::~Game() {
/*
if (glContext) { if (glContext) {
SDL_GL_DeleteContext(glContext); SDL_GL_DeleteContext(glContext);
} }
if (window) { if (window) {
SDL_DestroyWindow(window); SDL_DestroyWindow(window);
} }*/
#ifndef EMSCRIPTEN #ifndef EMSCRIPTEN
// In Emscripten, SDL must stay alive across context loss/restore cycles // In Emscripten, SDL must stay alive across context loss/restore cycles
// so the window remains valid when the game object is re-created. // so the window remains valid when the game object is re-created.
@ -81,14 +83,15 @@ namespace ZL
} }
void Game::setup() { void Game::setup() {
glContext = SDL_GL_CreateContext(ZL::Environment::window); //glContext = SDL_GL_CreateContext(ZL::Environment::window);
//glContext = in_glContext;
Environment::computeProjectionDimensions(); Environment::computeProjectionDimensions();
ZL::BindOpenGlFunctions(); ZL::BindOpenGlFunctions();
ZL::CheckGlError(); ZL::CheckGlError();
renderer.InitOpenGL(); renderer.InitOpenGL();
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
// These shaders and loading.png are preloaded separately (not from zip), // These shaders and loading.png are preloaded separately (not from zip),
// so they are available immediately without waiting for resources.zip. // so they are available immediately without waiting for resources.zip.
@ -101,19 +104,23 @@ namespace ZL
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE)); loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE));
#endif #endif
loadingMesh.data = CreateRect2D({ Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, { Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, 3); loadingMesh.data = CreateRect2D({ 0.5f, 0.5f }, { 0.5f, 0.5f }, 3);
loadingMesh.RefreshVBO(); loadingMesh.RefreshVBO();
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
// Asynchronously download resources.zip; setupPart2() is called on completion. // Asynchronously download resources.zip; setupPart2() is called on completion.
// The loading screen stays visible until the download finishes. // The loading screen stays visible until the download finishes.
s_instance = this; s_instance = this;
std::cout << "Load resurces step 1" << std::endl;
emscripten_async_wget("resources.zip", "resources.zip", onResourcesZipLoaded, onResourcesZipError); emscripten_async_wget("resources.zip", "resources.zip", onResourcesZipLoaded, onResourcesZipError);
#else #else
mainThreadHandler.EnqueueMainThreadTask([this]() { mainThreadHandler.EnqueueMainThreadTask([this]() {
std::cout << "Load resurces step 2" << std::endl;
this->setupPart2(); this->setupPart2();
std::cout << "Load resurces step 3" << std::endl;
}); });
#endif #endif
} }
@ -167,13 +174,19 @@ namespace ZL
networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext()); networkClient = std::make_unique<WebSocketClient>(taskManager.getIOContext());
networkClient->Connect("localhost", 8081); networkClient->Connect("localhost", 8081);
#endif #endif
if (networkClient) { if (networkClient) {
std::string joinMsg = std::string("JOIN:") + nickname + ":" + std::to_string(shipType); std::string joinMsg = std::string("JOIN:") + nickname + ":" + std::to_string(shipType);
networkClient->Send(joinMsg); networkClient->Send(joinMsg);
std::cerr << "Sent JOIN: " << joinMsg << std::endl; std::cerr << "Sent JOIN: " << joinMsg << std::endl;
} }
space.boxCoordsArr.clear();
space.boxRenderArr.clear();
//space.boxLabels.clear();
space.boxAlive.clear();
space.serverBoxesApplied = false;
lastTickCount = 0; lastTickCount = 0;
spaceGameStarted = 1; spaceGameStarted = 1;
}; };
@ -249,12 +262,12 @@ namespace ZL
renderer.EnableVertexAttribArray(vPositionName); renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName); renderer.EnableVertexAttribArray(vTexCoordName);
float width = Environment::projectionWidth; //float width = Environment::projectionWidth;
float height = Environment::projectionHeight; //float height = Environment::projectionHeight;
renderer.PushProjectionMatrix( renderer.PushProjectionMatrix(
0, width, 0, 1,
0, height, 0, 1,
-10, 10); -10, 10);
renderer.PushMatrix(); renderer.PushMatrix();
@ -275,8 +288,7 @@ namespace ZL
int64_t Game::getSyncTimeMs() { int64_t Game::getSyncTimeMs() {
int64_t localNow = std::chrono::duration_cast<std::chrono::milliseconds>( int64_t localNow = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count(); std::chrono::system_clock::now().time_since_epoch()).count();
// Добавляем смещение, полученное от сервера if(networkClient)
if (networkClient)
{ {
return localNow + networkClient->getTimeOffset(); return localNow + networkClient->getTimeOffset();
} }
@ -317,10 +329,10 @@ namespace ZL
} }
void Game::render() { void Game::render() {
SDL_GL_MakeCurrent(ZL::Environment::window, glContext); //SDL_GL_MakeCurrent(ZL::Environment::window, glContext);
ZL::CheckGlError(); ZL::CheckGlError();
glClearColor(0.0f, 1.0f, 0.0f, 1.0f); glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawScene(); drawScene();
@ -438,15 +450,40 @@ namespace ZL
#endif #endif
} }
render(); render();
if (networkClient) { if (networkClient) {
#ifndef NETWORK //#ifndef NETWORK
auto localClient = dynamic_cast<ZL::LocalClient*>(networkClient.get()); auto localClient = dynamic_cast<ZL::LocalClient*>(networkClient.get());
if (localClient) { if (localClient) {
localClient->setLocalPlayerState(Environment::shipState); localClient->setLocalPlayerState(Environment::shipState);
} }
#endif //#endif
networkClient->Poll(); 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(); mainThreadHandler.processMainThreadTasks();

View File

@ -60,8 +60,8 @@ namespace ZL {
static void onResourcesZipError(const char* filename); static void onResourcesZipError(const char* filename);
#endif #endif
SDL_Window* window; //SDL_Window* window;
SDL_GLContext glContext; //SDL_GLContext glContext;
int64_t newTickCount; int64_t newTickCount;
int64_t lastTickCount; int64_t lastTickCount;

View File

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

View File

@ -16,37 +16,47 @@
// For Android and Desktop a plain global value is used (no context loss). // For Android and Desktop a plain global value is used (no context loss).
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
ZL::Game* g_game = nullptr; ZL::Game* g_game = nullptr;
//static SDL_Window* win_x = nullptr;
static SDL_GLContext glContext_x; // Не указатель, а сам объект (который суть void*)
#else #else
ZL::Game game; ZL::Game game;
#endif #endif
void MainLoop() {
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
if (g_game) g_game->update(); void MainLoop() {
#else // SDL_GL_MakeCurrent тут не нужен каждый раз
game.update(); /*glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
#endif glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
SDL_GL_SwapWindow(ZL::Environment::window);*/
g_game->update();
} }
#else
void MainLoop() {
game.update();
}
#endif
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
EM_BOOL onWebGLContextLost(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) { EM_BOOL onWebGLContextLost(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) {
delete g_game; //delete g_game;
g_game = nullptr; //g_game = nullptr;
return EM_TRUE; return EM_TRUE;
} }
EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) { EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) {
g_game = new ZL::Game(); //g_game = new ZL::Game();
g_game->setup(); //g_game->setup();
return EM_TRUE; return EM_TRUE;
} }
static void applyResize(int logicalW, int logicalH) { static void applyResize(int logicalW, int logicalH) {
// Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung) // Получаем коэффициент плотности пикселей (например, 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); int physicalW = static_cast<int>(logicalW * dpr);
@ -91,6 +101,44 @@ EM_BOOL onFullscreenChanged(int /*eventType*/, const EmscriptenFullscreenChangeE
return EM_FALSE; return EM_FALSE;
} }
int main(int argc, char* argv[]) {
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); // Для WebGL 2.0
ZL::Environment::window = SDL_CreateWindow("Game", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
glContext_x = SDL_GL_CreateContext(ZL::Environment::window);
SDL_GL_MakeCurrent(ZL::Environment::window, glContext_x);
g_game = new ZL::Game();
g_game->setup();
emscripten_set_webglcontextlost_callback("#canvas", nullptr, EM_TRUE, onWebGLContextLost);
emscripten_set_webglcontextrestored_callback("#canvas", nullptr, EM_TRUE, onWebGLContextRestored);
// Keep Environment::width/height in sync when the canvas is resized.
emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized);
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_FALSE, onFullscreenChanged);
// 2. ИНИЦИАЛИЗАЦИЯ РАЗМЕРОВ:
// Получаем реальные размеры окна браузера на момент запуска
int canvasW = EM_ASM_INT({ return window.innerWidth; });
int canvasH = EM_ASM_INT({ return window.innerHeight; });
// Вызываем вашу функцию — она сама применит DPR, выставит физический размер
// канваса и отправит SDL_WINDOWEVENT_RESIZED для настройки проекции.
applyResize(canvasW, canvasH);
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
emscripten_set_main_loop(MainLoop, 0, 1);
return 0;
}
#endif #endif
@ -178,7 +226,9 @@ extern "C" int SDL_main(int argc, char* argv[]) {
} }
#else #endif
#ifdef WIN32_LEAN_AND_MEAN
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
try try
@ -192,7 +242,7 @@ int main(int argc, char *argv[]) {
ZL::Environment::height = CONST_HEIGHT; ZL::Environment::height = CONST_HEIGHT;
#ifdef EMSCRIPTEN /*#ifdef EMSCRIPTEN
if (SDL_Init(SDL_INIT_VIDEO) != 0) { if (SDL_Init(SDL_INIT_VIDEO) != 0) {
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl; std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
return 1; return 1;
@ -251,7 +301,7 @@ int main(int argc, char *argv[]) {
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
emscripten_set_main_loop(MainLoop, 0, 1); emscripten_set_main_loop(MainLoop, 0, 1);
#else #else*/
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
SDL_Log("SDL init failed: %s", SDL_GetError()); SDL_Log("SDL init failed: %s", SDL_GetError());
return 1; return 1;
@ -277,7 +327,7 @@ int main(int argc, char *argv[]) {
game.update(); game.update();
SDL_Delay(2); SDL_Delay(2);
} }
#endif //#endif
} }
catch (const std::exception& e) catch (const std::exception& e)

View File

@ -418,11 +418,13 @@ namespace ZL {
pinfo.position = pr.pos; pinfo.position = pr.pos;
pinfo.rotation = dir.toRotationMatrix(); pinfo.rotation = dir.toRotationMatrix();
pinfo.velocity = velocity; pinfo.velocity = velocity;
pendingProjectiles.push_back(pinfo);
std::cout << "LocalClient: Created projectile at pos (" << shotPos.x() << ", " if (pinfo.shooterId != GetClientId()) {
pendingProjectiles.push_back(pinfo);
}
std::cout << "LocalClient: Created server projectile at pos (" << shotPos.x() << ", "
<< shotPos.y() << ", " << shotPos.z() << ") vel (" << pr.vel.x() << ", " << shotPos.y() << ", " << shotPos.z() << ") vel (" << pr.vel.x() << ", "
<< pr.vel.y() << ", " << pr.vel.z() << ")" << std::endl; << pr.vel.y() << ", " << pr.vel.z() << ") shooter=" << pr.shooterId << std::endl;
} }
} }
} }

View File

@ -144,7 +144,7 @@ namespace ZL {
if (msg.rfind("PROJECTILE:", 0) == 0) { if (msg.rfind("PROJECTILE:", 0) == 0) {
//auto parts = split(msg, ':'); //auto parts = split(msg, ':');
if (parts.size() >= 10) { if (parts.size() >= 11) {
try { try {
ProjectileInfo pi; ProjectileInfo pi;
pi.shooterId = std::stoi(parts[1]); pi.shooterId = std::stoi(parts[1]);
@ -190,7 +190,28 @@ namespace ZL {
} }
return; 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) { if (msg.rfind("EVENT:", 0) == 0) {
//auto parts = split(msg, ':'); //auto parts = split(msg, ':');
if (parts.size() < 5) return; // EVENT:ID:TYPE:TIME:DATA... if (parts.size() < 5) return; // EVENT:ID:TYPE:TIME:DATA...
@ -334,6 +355,12 @@ namespace ZL {
copy.swap(pendingBoxDestructions_); copy.swap(pendingBoxDestructions_);
return copy; return copy;
} }
std::vector<ClientState> WebSocketClientBase::getPendingSpawns() {
std::vector<ClientState> copy;
copy.swap(pendingSpawns_);
return copy;
}
} }
#endif #endif

View File

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

View File

@ -7,8 +7,8 @@
namespace ZL { namespace ZL {
void WebSocketClientEmscripten::Connect(const std::string& host, uint16_t port) { void WebSocketClientEmscripten::Connect(const std::string& host, uint16_t port) {
// Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost // Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost
std::string url = "ws://" + host + ":" + std::to_string(port); //std::string url = "ws://" + host + ":" + std::to_string(port);
//std::string url = "wss://api.spacegame.fishrungames.com"; std::string url = "wss://api.spacegame.fishrungames.com";
EmscriptenWebSocketCreateAttributes attr = { EmscriptenWebSocketCreateAttributes attr = {
url.c_str(), url.c_str(),
@ -22,13 +22,32 @@ namespace ZL {
emscripten_websocket_set_onmessage_callback(socket_, this, onMessage); emscripten_websocket_set_onmessage_callback(socket_, this, onMessage);
emscripten_websocket_set_onerror_callback(socket_, this, onError); emscripten_websocket_set_onerror_callback(socket_, this, onError);
emscripten_websocket_set_onclose_callback(socket_, this, onClose); emscripten_websocket_set_onclose_callback(socket_, this, onClose);
connected = false;
}
void WebSocketClientEmscripten::flushOutgoingQueue() {
std::lock_guard<std::mutex> lock(outgoingMutex);
if (!socket_) return;
while (!outgoingQueue.empty()) {
const std::string &m = outgoingQueue.front();
emscripten_websocket_send_utf8_text(socket_, m.c_str());
outgoingQueue.pop();
}
} }
void WebSocketClientEmscripten::Send(const std::string& message) { void WebSocketClientEmscripten::Send(const std::string& message) {
if (connected && socket_ > 0) { std::string signedMsg = SignMessage(message);
auto signedMsg = SignMessage(message);
std::cout << "[WebWS] Sending message: " << signedMsg << std::endl; {
emscripten_websocket_send_utf8_text(socket_, signedMsg.c_str()); std::lock_guard<std::mutex> lock(outgoingMutex);
if (connected && socket_ > 0) {
std::cout << "[WebWS] Sending message (immediate): " << signedMsg << std::endl;
emscripten_websocket_send_utf8_text(socket_, signedMsg.c_str());
return;
}
outgoingQueue.push(signedMsg);
std::cout << "[WebWS] Queued outgoing message (waiting for open): " << signedMsg << std::endl;
} }
} }
@ -36,19 +55,16 @@ namespace ZL {
// Локальная очередь для минимизации времени блокировки мьютекса // Локальная очередь для минимизации времени блокировки мьютекса
std::queue<std::string> localQueue; std::queue<std::string> localQueue;
{ if (messageQueue.empty()) return;
std::lock_guard<std::mutex> lock(queueMutex); std::swap(localQueue, messageQueue);
if (messageQueue.empty()) return;
std::swap(localQueue, messageQueue);
}
while (!localQueue.empty()) { while (!localQueue.empty()) {
const std::string& msg = localQueue.front(); const std::string& msg = localQueue.front();
std::cout << "[WebWS] Processing message: " << msg << std::endl; std::cout << "[WebWS] Processing message: " << msg << std::endl;
// Передаем в базовый класс для парсинга игровых событий (BOXES, UPD, и т.д.) // Передаем в базовый класс для парсинга игровых событий (BOXES, UPD, и т.д.)
HandlePollMessage(msg); HandlePollMessage(msg);
localQueue.pop(); localQueue.pop();
} }
} }
@ -59,6 +75,9 @@ namespace ZL {
auto* self = static_cast<WebSocketClientEmscripten*>(userData); auto* self = static_cast<WebSocketClientEmscripten*>(userData);
self->connected = true; self->connected = true;
std::cout << "[WebWS] Connection opened" << std::endl; std::cout << "[WebWS] Connection opened" << std::endl;
self->flushOutgoingQueue();
return EM_TRUE; return EM_TRUE;
} }
@ -67,7 +86,6 @@ namespace ZL {
auto* self = static_cast<WebSocketClientEmscripten*>(userData); auto* self = static_cast<WebSocketClientEmscripten*>(userData);
if (e->isText && e->data) { if (e->isText && e->data) {
std::string msg(reinterpret_cast<const char*>(e->data), e->numBytes); std::string msg(reinterpret_cast<const char*>(e->data), e->numBytes);
std::lock_guard<std::mutex> lock(self->queueMutex);
self->messageQueue.push(msg); self->messageQueue.push(msg);
} }
return EM_TRUE; return EM_TRUE;

View File

@ -18,7 +18,10 @@ namespace ZL {
// Очередь для хранения сырых строк от браузера // Очередь для хранения сырых строк от браузера
std::queue<std::string> messageQueue; std::queue<std::string> messageQueue;
std::mutex queueMutex;
std::queue<std::string> outgoingQueue;
std::mutex outgoingMutex;
void flushOutgoingQueue();
public: public:
WebSocketClientEmscripten() = default; WebSocketClientEmscripten() = default;

View File

@ -5,35 +5,52 @@ namespace ZL
{ {
TaskManager::TaskManager(size_t threadCount) { TaskManager::TaskManager(size_t threadCount) {
workGuard = std::make_unique<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>>(ioContext.get_executor());
#ifndef EMSCRIPTEN
workGuard = std::make_unique<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>>(ioContext.get_executor());
for (size_t i = 0; i < threadCount; ++i) { for (size_t i = 0; i < threadCount; ++i) {
workers.emplace_back([this]() { workers.emplace_back([this]() {
ioContext.run(); ioContext.run();
}); });
} }
#endif
} }
void TaskManager::EnqueueBackgroundTask(std::function<void()> task) { void TaskManager::EnqueueBackgroundTask(std::function<void()> task) {
#ifdef EMSCRIPTEN
task();
#else
boost::asio::post(ioContext, task); boost::asio::post(ioContext, task);
#endif
} }
TaskManager::~TaskManager() { TaskManager::~TaskManager() {
#ifndef EMSCRIPTEN
workGuard.reset(); // Ðàçðåøàåì ioContext.run() çàâåðøèòüñÿ, êîãäà çàäà÷ íå îñòàíåòñÿ workGuard.reset(); // Ðàçðåøàåì ioContext.run() çàâåðøèòüñÿ, êîãäà çàäà÷ íå îñòàíåòñÿ
ioContext.stop(); // Îïöèîíàëüíî: íåìåäëåííàÿ îñòàíîâêà ioContext.stop(); // Îïöèîíàëüíî: íåìåäëåííàÿ îñòàíîâêà
for (auto& t : workers) { for (auto& t : workers) {
if (t.joinable()) t.join(); if (t.joinable()) t.join();
} }
#endif
} }
void MainThreadHandler::EnqueueMainThreadTask(std::function<void()> task) { void MainThreadHandler::EnqueueMainThreadTask(std::function<void()> task) {
#ifndef EMSCRIPTEN
std::lock_guard<std::mutex> lock(mainThreadMutex); std::lock_guard<std::mutex> lock(mainThreadMutex);
#endif
mainThreadTasks.push(task); mainThreadTasks.push(task);
} }
void MainThreadHandler::processMainThreadTasks() { void MainThreadHandler::processMainThreadTasks() {
std::function<void()> task; std::function<void()> task;
#ifdef EMSCRIPTEN
if (!mainThreadTasks.empty()) {
task = std::move(mainThreadTasks.front());
mainThreadTasks.pop();
}
#else
// Èçâëåêàåì òîëüêî îäíó çàäà÷ó, ÷òîáû íå áëîêèðîâàòü update íàäîëãî // Èçâëåêàåì òîëüêî îäíó çàäà÷ó, ÷òîáû íå áëîêèðîâàòü update íàäîëãî
{ {
std::lock_guard<std::mutex> lock(mainThreadMutex); std::lock_guard<std::mutex> lock(mainThreadMutex);
@ -42,6 +59,7 @@ namespace ZL
mainThreadTasks.pop(); mainThreadTasks.pop();
} }
} }
#endif
if (task) { if (task) {
task(); // Çäåñü âûïîëíÿåòñÿ RefreshVBO èëè çàãðóçêà òåêñòóðû task(); // Çäåñü âûïîëíÿåòñÿ RefreshVBO èëè çàãðóçêà òåêñòóðû

View File

@ -1,8 +1,12 @@
#pragma once #pragma once
#ifndef EMSCRIPTEN
#include <boost/asio.hpp> #include <boost/asio.hpp>
#include <thread>
#endif
#include <functional> #include <functional>
#include <vector> #include <vector>
#include <thread>
#include <memory> #include <memory>
#include <queue> #include <queue>
@ -11,12 +15,12 @@ namespace ZL {
class TaskManager { class TaskManager {
private: private:
#ifndef EMSCRIPTEN
boost::asio::io_context ioContext; boost::asio::io_context ioContext;
std::unique_ptr<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>> workGuard; std::unique_ptr<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>> workGuard;
std::vector<std::thread> workers; std::vector<std::thread> workers;
#endif
public: public:
//TaskManager(size_t threadCount = std::thread::hardware_concurrency());
TaskManager(size_t threadCount = 2); TaskManager(size_t threadCount = 2);
// Ìåòîä äëÿ äîáàâëåíèÿ ôîíîâîé çàäà÷è // Ìåòîä äëÿ äîáàâëåíèÿ ôîíîâîé çàäà÷è
@ -24,11 +28,13 @@ namespace ZL {
// Graceful shutdown // Graceful shutdown
~TaskManager(); ~TaskManager();
#ifndef EMSCRIPTEN
boost::asio::io_context& getIOContext() boost::asio::io_context& getIOContext()
{ {
return ioContext; return ioContext;
} }
#endif
}; };
@ -36,8 +42,10 @@ namespace ZL {
{ {
private: private:
std::queue<std::function<void()>> mainThreadTasks; std::queue<std::function<void()>> mainThreadTasks;
std::mutex mainThreadMutex;
#ifndef EMSCRIPTEN
std::mutex mainThreadMutex;
#endif
public: public:
void EnqueueMainThreadTask(std::function<void()> task); void EnqueueMainThreadTask(std::function<void()> task);