From 9f82e7a2e6d10d3782a22014ad348bebe3b004f7 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Fri, 27 Feb 2026 22:31:07 +0300 Subject: [PATCH] Adapt for web --- proj-web/CMakeLists.txt | 9 ++- src/Game.cpp | 45 ++++++++++-- src/Game.h | 6 ++ src/MenuManager.cpp | 35 +++++++++- src/Space.cpp | 10 +-- src/main.cpp | 104 +++++++++++++++++++++++++--- src/network/ClientState.cpp | 2 +- src/network/ClientState.h | 6 +- src/network/LocalClient.cpp | 2 +- src/network/LocalClient.h | 2 +- src/network/WebSocketClient.cpp | 2 +- src/network/WebSocketClient.h | 2 +- src/network/WebSocketClientBase.cpp | 2 +- src/network/WebSocketClientBase.h | 2 +- 14 files changed, 189 insertions(+), 40 deletions(-) diff --git a/proj-web/CMakeLists.txt b/proj-web/CMakeLists.txt index e41f118..8b02c39 100644 --- a/proj-web/CMakeLists.txt +++ b/proj-web/CMakeLists.txt @@ -115,12 +115,15 @@ set(EMSCRIPTEN_FLAGS target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2") +# Only loading.png and the shaders used before resources.zip is ready are preloaded. +# resources.zip is downloaded asynchronously at runtime and served as a separate file. set(EMSCRIPTEN_LINK_FLAGS ${EMSCRIPTEN_FLAGS} "-O2" "-sPTHREAD_POOL_SIZE=4" "-sALLOW_MEMORY_GROWTH=1" - "--preload-file resources.zip" + "--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/loading.png@resources/loading.png" + "--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/shaders@resources/shaders" ) # Применяем настройки линковки @@ -170,8 +173,8 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/space-game001plain.html" DESTINATION . ) -# Если вам все еще нужен сам resources.zip отдельно в папке public: -#install(FILES "${RESOURCES_ZIP}" DESTINATION .) +# resources.zip is served separately and downloaded asynchronously at runtime +install(FILES "${RESOURCES_ZIP}" DESTINATION .) add_custom_command(TARGET space-game001 POST_BUILD COMMAND ${CMAKE_COMMAND} --install . diff --git a/src/Game.cpp b/src/Game.cpp index de0dd61..e653d94 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -23,6 +23,10 @@ #endif #endif +#ifdef EMSCRIPTEN +#include +#endif + #include "network/LocalClient.h" #include "network/ClientState.h" @@ -36,6 +40,22 @@ namespace ZL const char* CONST_ZIP_FILE = ""; #endif +#ifdef EMSCRIPTEN + Game* Game::s_instance = nullptr; + + void Game::onResourcesZipLoaded(const char* /*filename*/) { + if (s_instance) { + s_instance->mainThreadHandler.EnqueueMainThreadTask([&]() { + s_instance->setupPart2(); + }); + } + } + + void Game::onResourcesZipError(const char* /*filename*/) { + std::cerr << "Failed to download resources.zip" << std::endl; + } +#endif + Game::Game() : window(nullptr) , glContext(nullptr) @@ -53,7 +73,11 @@ namespace ZL 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. SDL_Quit(); +#endif } void Game::setup() { @@ -64,21 +88,30 @@ namespace ZL renderer.InitOpenGL(); #ifdef EMSCRIPTEN - renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE); - renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE); - + // These shaders and loading.png are preloaded separately (not from zip), + // so they are available immediately without waiting for resources.zip. + renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", ""); + renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", ""); + loadingTexture = std::make_unique(CreateTextureDataFromPng("resources/loading.png", "")); #else renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_desktop.fragment", CONST_ZIP_FILE); + loadingTexture = std::make_unique(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE)); #endif - loadingTexture = std::make_unique(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE)); loadingMesh.data = CreateRect2D({ Environment::width * 0.5, Environment::height * 0.5 }, { Environment::width * 0.5, Environment::height*0.5 }, 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; + emscripten_async_wget("resources.zip", "resources.zip", onResourcesZipLoaded, onResourcesZipError); +#else mainThreadHandler.EnqueueMainThreadTask([this]() { this->setupPart2(); - }); + }); +#endif } @@ -322,6 +355,8 @@ namespace ZL // Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях Environment::width = event.window.data1; Environment::height = event.window.data2; + std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl; + space.clearTextRendererCache(); } #endif diff --git a/src/Game.h b/src/Game.h index 6e0da91..ab4ab9f 100644 --- a/src/Game.h +++ b/src/Game.h @@ -54,6 +54,12 @@ namespace ZL { void handleUp(int mx, int my); void handleMotion(int mx, int my); +#ifdef EMSCRIPTEN + static Game* s_instance; + static void onResourcesZipLoaded(const char* filename); + static void onResourcesZipError(const char* filename); +#endif + SDL_Window* window; SDL_GLContext glContext; diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index c153cf0..459f33c 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -183,8 +183,8 @@ namespace ZL { } }); - uiManager.setButtonCallback("multiplayerButton2", [this](const std::string& name) { - std::cerr << "Multiplayer button pressed → opening multiplayer menu\n"; + uiManager.setButtonCallback("multiplayerButton2", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) { + /*std::cerr << "Multiplayer button pressed → opening multiplayer menu\n"; uiManager.startAnimationOnNode("playButton", "buttonsExit"); uiManager.startAnimationOnNode("settingsButton", "buttonsExit"); @@ -219,6 +219,37 @@ namespace ZL { } else { std::cerr << "Failed to load multiplayer menu\n"; + }*/ + std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n"; + if (!shipSelectionRoot) { + std::cerr << "Failed to load ship selection UI\n"; + return; + } + if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) { + uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) { + std::string nick = uiManager.getTextFieldValue("nicknameInput"); + if (nick.empty()) nick = "Player"; + int shipType = 0; + uiManager.popMenu(); + loadGameplayUI(); + if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType); + }); + + uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) { + std::string nick = uiManager.getTextFieldValue("nicknameInput"); + if (nick.empty()) nick = "Player"; + int shipType = 1; + uiManager.popMenu(); + loadGameplayUI(); + if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType); + }); + + uiManager.setButtonCallback("backButton", [this](const std::string& btnName) { + uiManager.popMenu(); + }); + } + else { + std::cerr << "Failed to push ship selection menu\n"; } }); uiManager.setButtonCallback("exitButton", [](const std::string& name) { diff --git a/src/Space.cpp b/src/Space.cpp index 3aafd38..fef7a82 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -616,13 +616,11 @@ namespace ZL } void Space::drawRemoteShips() { - // Используем те же константы имен для шейдеров, что и в drawShip static const std::string defaultShaderName = "default"; static const std::string vPositionName = "vPosition"; static const std::string vTexCoordName = "vTexCoord"; static const std::string textureUniformName = "Texture"; - // Активируем шейдер и текстуру (предполагаем, что меш у всех одинаковый) renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); @@ -633,10 +631,6 @@ namespace ZL static_cast(Environment::width) / static_cast(Environment::height), Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); - // Биндим текстуру корабля один раз для ?сех правильных игроков - // ?????????: ?????? ???????? ?????????? ?????? ????? ? ??????????? ?? ClientState.shipType - - // Если сервер прислал коробки, применяем их однократно вместо локальной генерации if (!serverBoxesApplied && networkClient) { auto sboxes = networkClient->getServerBoxes(); auto destroyedFlags = networkClient->getServerBoxDestroyedFlags(); @@ -664,7 +658,6 @@ namespace ZL } } - // Итерируемся по актуальным данным из extrapolateRemotePlayers for (auto const& [id, remotePlayer] : remotePlayerStates) { const ClientState& playerState = remotePlayer; @@ -674,7 +667,7 @@ namespace ZL renderer.LoadIdentity(); renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); - renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset + //renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset renderer.RotateMatrix(Environment::inverseShipMatrix); renderer.TranslateMatrix(-Environment::shipState.position); @@ -682,7 +675,6 @@ namespace ZL Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition; renderer.TranslateMatrix(relativePos); - // 3. Поворот врага renderer.RotateMatrix(playerState.rotation); if (playerState.shipType == 1 && cargoTexture) { diff --git a/src/main.cpp b/src/main.cpp index 57e0b5e..d1bdc1e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,15 +5,87 @@ #include #endif +#ifdef EMSCRIPTEN +#include +#include +#endif + +// For Emscripten the game is heap-allocated so it can be destroyed and +// re-created when the WebGL context is lost and restored (e.g. fullscreen). +// For Android and Desktop a plain global value is used (no context loss). +#ifdef EMSCRIPTEN +ZL::Game* g_game = nullptr; +#else ZL::Game game; +#endif void MainLoop() { +#ifdef EMSCRIPTEN + if (g_game) g_game->update(); +#else game.update(); +#endif } +#ifdef EMSCRIPTEN + +EM_BOOL onWebGLContextLost(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) { + 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(); + return EM_TRUE; +} + +// Resize the canvas, notify SDL, and push a synthetic SDL_WINDOWEVENT_RESIZED +// so Game::update()'s existing handler updates Environment::width/height and clears caches. +static void applyResize(int w, int h) { + if (w <= 0 || h <= 0) return; + // Resize the actual WebGL canvas — without this the rendered pixels stay at + // the original size no matter what Environment::width/height say. + emscripten_set_canvas_element_size("#canvas", w, h); + if (ZL::Environment::window) + SDL_SetWindowSize(ZL::Environment::window, w, h); + SDL_Event e = {}; + e.type = SDL_WINDOWEVENT; + e.window.event = SDL_WINDOWEVENT_RESIZED; + e.window.data1 = w; + e.window.data2 = h; + SDL_PushEvent(&e); +} + +EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) { + // Use the event's window dimensions — querying the canvas element would + // return its old fixed size (e.g. 1280x720) before it has been resized. + applyResize(e->windowInnerWidth, e->windowInnerHeight); + return EM_FALSE; +} + +EM_BOOL onFullscreenChanged(int /*eventType*/, const EmscriptenFullscreenChangeEvent* e, void* /*userData*/) { + if (e->isFullscreen) { + // e->screenWidth/screenHeight comes from screen.width/screen.height in JS, + // which on mobile browsers returns physical pixels (e.g. 2340x1080), + // causing the canvas to extend far off-screen. window.innerWidth/innerHeight + // always gives CSS logical pixels and is correct on both desktop and mobile. + int w = EM_ASM_INT({ return window.innerWidth; }); + int h = EM_ASM_INT({ return window.innerHeight; }); + applyResize(w, h); + } + // Exiting fullscreen: the browser fires a window resize event next, + // which onWindowResized handles automatically. + return EM_FALSE; +} + +#endif + + #ifdef __ANDROID__ extern "C" int SDL_main(int argc, char* argv[]) { @@ -34,7 +106,7 @@ extern "C" int SDL_main(int argc, char* argv[]) { __android_log_print(ANDROID_LOG_INFO, "Game", "Display resolution: %dx%d", ZL::Environment::width, ZL::Environment::height); - + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); @@ -52,13 +124,13 @@ extern "C" int SDL_main(int argc, char* argv[]) { ZL::Environment::width, ZL::Environment::height, SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN ); - + if (!ZL::Environment::window) { __android_log_print(ANDROID_LOG_ERROR, "Game", "Failed to create window: %s", SDL_GetError()); SDL_Quit(); return 1; } - + SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window); if (!ctx) { __android_log_print(ANDROID_LOG_ERROR, "Game", "SDL_GL_CreateContext failed: %s", SDL_GetError()); @@ -94,7 +166,7 @@ extern "C" int SDL_main(int argc, char* argv[]) { } return 0; - + } @@ -103,8 +175,8 @@ extern "C" int SDL_main(int argc, char* argv[]) { int main(int argc, char *argv[]) { try { - - + + constexpr int CONST_WIDTH = 1280; constexpr int CONST_HEIGHT = 720; @@ -142,6 +214,20 @@ int main(int argc, char *argv[]) { SDL_GL_MakeCurrent(win, glContext); ZL::Environment::window = win; + + g_game = new ZL::Game(); + g_game->setup(); + + // Re-create the game object when the WebGL context is lost and restored + // (this happens e.g. when the user toggles fullscreen in the browser). + 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); + + emscripten_set_main_loop(MainLoop, 0, 1); #else if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { SDL_Log("SDL init failed: %s", SDL_GetError()); @@ -161,13 +247,9 @@ int main(int argc, char *argv[]) { SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window); SDL_GL_MakeCurrent(ZL::Environment::window, ctx); -#endif game.setup(); -#ifdef EMSCRIPTEN - emscripten_set_main_loop(MainLoop, 0, 1); -#else while (!game.shouldExit()) { game.update(); SDL_Delay(2); @@ -182,4 +264,4 @@ int main(int argc, char *argv[]) { return 0; } -#endif \ No newline at end of file +#endif diff --git a/src/network/ClientState.cpp b/src/network/ClientState.cpp index fb6b822..e33c965 100644 --- a/src/network/ClientState.cpp +++ b/src/network/ClientState.cpp @@ -1,4 +1,4 @@ -#include "ClientState.h" +#include "ClientState.h" uint32_t fnv1a_hash(const std::string& data) { uint32_t hash = 0x811c9dc5; diff --git a/src/network/ClientState.h b/src/network/ClientState.h index 02ab5f5..2624679 100644 --- a/src/network/ClientState.h +++ b/src/network/ClientState.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include #define _USE_MATH_DEFINES @@ -19,7 +19,7 @@ constexpr float ROTATION_SENSITIVITY = 0.002f; constexpr float PLANET_RADIUS = 20000.f; constexpr float PLANET_ALIGN_ZONE = 1.05f; -constexpr float PLANET_ANGULAR_ACCEL = 0.01f; // ??????? ??? ???????? +constexpr float PLANET_ANGULAR_ACCEL = 0.01f; constexpr float PLANET_MAX_ANGULAR_VELOCITY = 10.f; constexpr float PITCH_LIMIT = static_cast(M_PI) / 9.f;//18.0f; @@ -41,7 +41,7 @@ struct ClientState { std::string nickname = "Player"; int shipType = 0; - // ??? ??????? ???? + std::chrono::system_clock::time_point lastUpdateServerTime; void simulate_physics(size_t delta); diff --git a/src/network/LocalClient.cpp b/src/network/LocalClient.cpp index d7c08af..0321249 100644 --- a/src/network/LocalClient.cpp +++ b/src/network/LocalClient.cpp @@ -1,4 +1,4 @@ -#include "LocalClient.h" +#include "LocalClient.h" #include #include #include diff --git a/src/network/LocalClient.h b/src/network/LocalClient.h index 5227bf5..438dede 100644 --- a/src/network/LocalClient.h +++ b/src/network/LocalClient.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "NetworkInterface.h" #include diff --git a/src/network/WebSocketClient.cpp b/src/network/WebSocketClient.cpp index bdf0232..d19463a 100644 --- a/src/network/WebSocketClient.cpp +++ b/src/network/WebSocketClient.cpp @@ -1,4 +1,4 @@ -#ifdef NETWORK +#ifdef NETWORK #include "WebSocketClient.h" #include diff --git a/src/network/WebSocketClient.h b/src/network/WebSocketClient.h index 5ccb45c..bd01ba2 100644 --- a/src/network/WebSocketClient.h +++ b/src/network/WebSocketClient.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #ifdef NETWORK diff --git a/src/network/WebSocketClientBase.cpp b/src/network/WebSocketClientBase.cpp index c4f74b3..6253f94 100644 --- a/src/network/WebSocketClientBase.cpp +++ b/src/network/WebSocketClientBase.cpp @@ -1,4 +1,4 @@ -#ifdef NETWORK +#ifdef NETWORK #include "WebSocketClientBase.h" #include diff --git a/src/network/WebSocketClientBase.h b/src/network/WebSocketClientBase.h index 8d3f97e..3784700 100644 --- a/src/network/WebSocketClientBase.h +++ b/src/network/WebSocketClientBase.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "NetworkInterface.h" #include