Working on running the game even in Telegram

This commit is contained in:
Vladislav Khorev 2026-03-04 21:41:51 +03:00
parent 5fcb1d1234
commit e78429b600
9 changed files with 133 additions and 60 deletions

View File

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

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

@ -57,9 +57,9 @@ namespace ZL
#endif
Game::Game()
: window(nullptr)
: /*window(nullptr)
, glContext(nullptr)
, newTickCount(0)
, */newTickCount(0)
, lastTickCount(0)
, menuManager(renderer)
, space(renderer, taskManager, mainThreadHandler, networkClient, menuManager)
@ -67,12 +67,13 @@ namespace ZL
}
Game::~Game() {
/*
if (glContext) {
SDL_GL_DeleteContext(glContext);
}
if (window) {
SDL_DestroyWindow(window);
}
}*/
#ifndef EMSCRIPTEN
// In Emscripten, SDL must stay alive across context loss/restore cycles
// so the window remains valid when the game object is re-created.
@ -81,7 +82,8 @@ namespace ZL
}
void Game::setup() {
glContext = SDL_GL_CreateContext(ZL::Environment::window);
//glContext = SDL_GL_CreateContext(ZL::Environment::window);
//glContext = in_glContext;
Environment::computeProjectionDimensions();
@ -101,19 +103,23 @@ namespace ZL
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE));
#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();
#ifdef EMSCRIPTEN
// Asynchronously download resources.zip; setupPart2() is called on completion.
// The loading screen stays visible until the download finishes.
s_instance = this;
std::cout << "Load resurces step 1" << std::endl;
emscripten_async_wget("resources.zip", "resources.zip", onResourcesZipLoaded, onResourcesZipError);
#else
mainThreadHandler.EnqueueMainThreadTask([this]() {
std::cout << "Load resurces step 2" << std::endl;
this->setupPart2();
std::cout << "Load resurces step 3" << std::endl;
});
#endif
}
@ -164,8 +170,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) {
@ -249,12 +255,12 @@ namespace ZL
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
float width = Environment::projectionWidth;
float height = Environment::projectionHeight;
//float width = Environment::projectionWidth;
//float height = Environment::projectionHeight;
renderer.PushProjectionMatrix(
0, width,
0, height,
0, 1,
0, 1,
-10, 10);
renderer.PushMatrix();
@ -275,8 +281,7 @@ namespace ZL
int64_t Game::getSyncTimeMs() {
int64_t localNow = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
// Добавляем смещение, полученное от сервера
if (networkClient)
if(networkClient)
{
return localNow + networkClient->getTimeOffset();
}
@ -317,10 +322,10 @@ namespace ZL
}
void Game::render() {
SDL_GL_MakeCurrent(ZL::Environment::window, glContext);
//SDL_GL_MakeCurrent(ZL::Environment::window, glContext);
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);
drawScene();

View File

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

View File

@ -16,37 +16,46 @@
// For Android and Desktop a plain global value is used (no context loss).
#ifdef EMSCRIPTEN
ZL::Game* g_game = nullptr;
//static SDL_Window* win_x = nullptr;
static SDL_GLContext glContext_x; // Не указатель, а сам объект (который суть void*)
#else
ZL::Game game;
#endif
void MainLoop() {
#ifdef EMSCRIPTEN
if (g_game) g_game->update();
#else
game.update();
#endif
void MainLoop() {
// SDL_GL_MakeCurrent тут не нужен каждый раз
/*glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
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
EM_BOOL onWebGLContextLost(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) {
delete g_game;
g_game = nullptr;
//delete g_game;
//g_game = nullptr;
return EM_TRUE;
}
EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) {
g_game = new ZL::Game();
g_game->setup();
//g_game = new ZL::Game();
//g_game->setup();
return EM_TRUE;
}
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();
// Вычисляем реальные физические пиксели
int physicalW = static_cast<int>(logicalW * dpr);
@ -72,7 +81,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*/) {
@ -91,6 +100,44 @@ EM_BOOL onFullscreenChanged(int /*eventType*/, const EmscriptenFullscreenChangeE
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
@ -178,7 +225,9 @@ extern "C" int SDL_main(int argc, char* argv[]) {
}
#else
#endif
#ifdef WIN32_LEAN_AND_MEAN
int main(int argc, char *argv[]) {
try
@ -192,7 +241,7 @@ int main(int argc, char *argv[]) {
ZL::Environment::height = CONST_HEIGHT;
#ifdef EMSCRIPTEN
/*#ifdef EMSCRIPTEN
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
return 1;
@ -251,7 +300,7 @@ int main(int argc, char *argv[]) {
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
emscripten_set_main_loop(MainLoop, 0, 1);
#else
#else*/
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
SDL_Log("SDL init failed: %s", SDL_GetError());
return 1;
@ -277,7 +326,7 @@ int main(int argc, char *argv[]) {
game.update();
SDL_Delay(2);
}
#endif
//#endif
}
catch (const std::exception& e)

View File

@ -7,8 +7,8 @@
namespace ZL {
void WebSocketClientEmscripten::Connect(const std::string& host, uint16_t port) {
// Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost
std::string url = "ws://" + host + ":" + std::to_string(port);
//std::string url = "wss://api.spacegame.fishrungames.com";
//std::string url = "ws://" + host + ":" + std::to_string(port);
std::string url = "wss://api.spacegame.fishrungames.com";
EmscriptenWebSocketCreateAttributes attr = {
url.c_str(),
@ -36,11 +36,8 @@ namespace ZL {
// Локальная очередь для минимизации времени блокировки мьютекса
std::queue<std::string> localQueue;
{
std::lock_guard<std::mutex> lock(queueMutex);
if (messageQueue.empty()) return;
std::swap(localQueue, messageQueue);
}
if (messageQueue.empty()) return;
std::swap(localQueue, messageQueue);
while (!localQueue.empty()) {
const std::string& msg = localQueue.front();
@ -67,7 +64,6 @@ namespace ZL {
auto* self = static_cast<WebSocketClientEmscripten*>(userData);
if (e->isText && e->data) {
std::string msg(reinterpret_cast<const char*>(e->data), e->numBytes);
std::lock_guard<std::mutex> lock(self->queueMutex);
self->messageQueue.push(msg);
}
return EM_TRUE;

View File

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

View File

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

View File

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