Adapt for web

This commit is contained in:
Vladislav Khorev 2026-02-27 22:31:07 +03:00
parent 7418bbbe27
commit 9f82e7a2e6
14 changed files with 189 additions and 40 deletions

View File

@ -115,12 +115,15 @@ set(EMSCRIPTEN_FLAGS
target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2") 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 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"
"--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 . DESTINATION .
) )
# Если вам все еще нужен сам resources.zip отдельно в папке public: # resources.zip is served separately and downloaded asynchronously at runtime
#install(FILES "${RESOURCES_ZIP}" DESTINATION .) install(FILES "${RESOURCES_ZIP}" DESTINATION .)
add_custom_command(TARGET space-game001 POST_BUILD add_custom_command(TARGET space-game001 POST_BUILD
COMMAND ${CMAKE_COMMAND} --install . COMMAND ${CMAKE_COMMAND} --install .

View File

@ -23,6 +23,10 @@
#endif #endif
#endif #endif
#ifdef EMSCRIPTEN
#include <emscripten.h>
#endif
#include "network/LocalClient.h" #include "network/LocalClient.h"
#include "network/ClientState.h" #include "network/ClientState.h"
@ -36,6 +40,22 @@ namespace ZL
const char* CONST_ZIP_FILE = ""; const char* CONST_ZIP_FILE = "";
#endif #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() Game::Game()
: window(nullptr) : window(nullptr)
, glContext(nullptr) , glContext(nullptr)
@ -53,7 +73,11 @@ namespace ZL
if (window) { if (window) {
SDL_DestroyWindow(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(); SDL_Quit();
#endif
} }
void Game::setup() { void Game::setup() {
@ -64,21 +88,30 @@ namespace ZL
renderer.InitOpenGL(); renderer.InitOpenGL();
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE); // These shaders and loading.png are preloaded separately (not from zip),
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE); // 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<Texture>(CreateTextureDataFromPng("resources/loading.png", ""));
#else #else
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_desktop.fragment", CONST_ZIP_FILE); 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); renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_desktop.fragment", CONST_ZIP_FILE);
loadingTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE));
#endif #endif
loadingTexture = std::make_unique<Texture>(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.data = CreateRect2D({ Environment::width * 0.5, Environment::height * 0.5 }, { Environment::width * 0.5, Environment::height*0.5 }, 3);
loadingMesh.RefreshVBO(); 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]() { mainThreadHandler.EnqueueMainThreadTask([this]() {
this->setupPart2(); this->setupPart2();
}); });
#endif
} }
@ -322,6 +355,8 @@ namespace ZL
// Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях // Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях
Environment::width = event.window.data1; Environment::width = event.window.data1;
Environment::height = event.window.data2; Environment::height = event.window.data2;
std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl;
space.clearTextRendererCache(); space.clearTextRendererCache();
} }
#endif #endif

View File

@ -54,6 +54,12 @@ namespace ZL {
void handleUp(int mx, int my); void handleUp(int mx, int my);
void handleMotion(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_Window* window;
SDL_GLContext glContext; SDL_GLContext glContext;

View File

@ -183,8 +183,8 @@ namespace ZL {
} }
}); });
uiManager.setButtonCallback("multiplayerButton2", [this](const std::string& name) { uiManager.setButtonCallback("multiplayerButton2", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
std::cerr << "Multiplayer button pressed → opening multiplayer menu\n"; /*std::cerr << "Multiplayer button pressed → opening multiplayer menu\n";
uiManager.startAnimationOnNode("playButton", "buttonsExit"); uiManager.startAnimationOnNode("playButton", "buttonsExit");
uiManager.startAnimationOnNode("settingsButton", "buttonsExit"); uiManager.startAnimationOnNode("settingsButton", "buttonsExit");
@ -219,6 +219,37 @@ namespace ZL {
} }
else { else {
std::cerr << "Failed to load multiplayer menu\n"; 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) { uiManager.setButtonCallback("exitButton", [](const std::string& name) {

View File

@ -616,13 +616,11 @@ namespace ZL
} }
void Space::drawRemoteShips() { void Space::drawRemoteShips() {
// Используем те же константы имен для шейдеров, что и в drawShip
static const std::string defaultShaderName = "default"; static const std::string defaultShaderName = "default";
static const std::string vPositionName = "vPosition"; static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord"; static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture"; static const std::string textureUniformName = "Texture";
// Активируем шейдер и текстуру (предполагаем, что меш у всех одинаковый)
renderer.shaderManager.PushShader(defaultShaderName); renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i(textureUniformName, 0);
@ -633,10 +631,6 @@ namespace ZL
static_cast<float>(Environment::width) / static_cast<float>(Environment::height), static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
// Биндим текстуру корабля один раз для <20>?сех правильных игроков
// ?????????: ?????? ???????? ?????????? ?????? ????? ? ??????????? ?? ClientState.shipType
// Если сервер прислал коробки, применяем их однократно вместо локальной генерации
if (!serverBoxesApplied && networkClient) { if (!serverBoxesApplied && networkClient) {
auto sboxes = networkClient->getServerBoxes(); auto sboxes = networkClient->getServerBoxes();
auto destroyedFlags = networkClient->getServerBoxDestroyedFlags(); auto destroyedFlags = networkClient->getServerBoxDestroyedFlags();
@ -664,7 +658,6 @@ namespace ZL
} }
} }
// Итерируемся по актуальным данным из extrapolateRemotePlayers
for (auto const& [id, remotePlayer] : remotePlayerStates) { for (auto const& [id, remotePlayer] : remotePlayerStates) {
const ClientState& playerState = remotePlayer; const ClientState& playerState = remotePlayer;
@ -674,7 +667,7 @@ namespace ZL
renderer.LoadIdentity(); renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); 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.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position); renderer.TranslateMatrix(-Environment::shipState.position);
@ -682,7 +675,6 @@ namespace ZL
Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition; Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition;
renderer.TranslateMatrix(relativePos); renderer.TranslateMatrix(relativePos);
// 3. Поворот врага
renderer.RotateMatrix(playerState.rotation); renderer.RotateMatrix(playerState.rotation);
if (playerState.shipType == 1 && cargoTexture) { if (playerState.shipType == 1 && cargoTexture) {

View File

@ -5,15 +5,87 @@
#include <android/log.h> #include <android/log.h>
#endif #endif
#ifdef EMSCRIPTEN
#include <emscripten.h>
#include <emscripten/html5.h>
#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; ZL::Game game;
#endif
void MainLoop() { void MainLoop() {
#ifdef EMSCRIPTEN
if (g_game) g_game->update();
#else
game.update(); 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__ #ifdef __ANDROID__
extern "C" int SDL_main(int argc, char* argv[]) { extern "C" int SDL_main(int argc, char* argv[]) {
@ -142,6 +214,20 @@ int main(int argc, char *argv[]) {
SDL_GL_MakeCurrent(win, glContext); SDL_GL_MakeCurrent(win, glContext);
ZL::Environment::window = win; 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 #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());
@ -161,13 +247,9 @@ int main(int argc, char *argv[]) {
SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window); SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window);
SDL_GL_MakeCurrent(ZL::Environment::window, ctx); SDL_GL_MakeCurrent(ZL::Environment::window, ctx);
#endif
game.setup(); game.setup();
#ifdef EMSCRIPTEN
emscripten_set_main_loop(MainLoop, 0, 1);
#else
while (!game.shouldExit()) { while (!game.shouldExit()) {
game.update(); game.update();
SDL_Delay(2); SDL_Delay(2);

View File

@ -1,4 +1,4 @@
#include "ClientState.h" #include "ClientState.h"
uint32_t fnv1a_hash(const std::string& data) { uint32_t fnv1a_hash(const std::string& data) {
uint32_t hash = 0x811c9dc5; uint32_t hash = 0x811c9dc5;

View File

@ -1,4 +1,4 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <Eigen/Dense> #include <Eigen/Dense>
#define _USE_MATH_DEFINES #define _USE_MATH_DEFINES
@ -19,7 +19,7 @@ constexpr float ROTATION_SENSITIVITY = 0.002f;
constexpr float PLANET_RADIUS = 20000.f; constexpr float PLANET_RADIUS = 20000.f;
constexpr float PLANET_ALIGN_ZONE = 1.05f; 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 PLANET_MAX_ANGULAR_VELOCITY = 10.f;
constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f; constexpr float PITCH_LIMIT = static_cast<float>(M_PI) / 9.f;//18.0f;
@ -41,7 +41,7 @@ struct ClientState {
std::string nickname = "Player"; std::string nickname = "Player";
int shipType = 0; int shipType = 0;
// ??? ??????? ????
std::chrono::system_clock::time_point lastUpdateServerTime; std::chrono::system_clock::time_point lastUpdateServerTime;
void simulate_physics(size_t delta); void simulate_physics(size_t delta);

View File

@ -1,4 +1,4 @@
#include "LocalClient.h" #include "LocalClient.h"
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
#include <algorithm> #include <algorithm>

View File

@ -1,4 +1,4 @@
#pragma once #pragma once
#include "NetworkInterface.h" #include "NetworkInterface.h"
#include <queue> #include <queue>

View File

@ -1,4 +1,4 @@
#ifdef NETWORK #ifdef NETWORK
#include "WebSocketClient.h" #include "WebSocketClient.h"
#include <iostream> #include <iostream>

View File

@ -1,4 +1,4 @@
#pragma once #pragma once
#ifdef NETWORK #ifdef NETWORK

View File

@ -1,4 +1,4 @@
#ifdef NETWORK #ifdef NETWORK
#include "WebSocketClientBase.h" #include "WebSocketClientBase.h"
#include <iostream> #include <iostream>

View File

@ -1,4 +1,4 @@
#pragma once #pragma once
#include "NetworkInterface.h" #include "NetworkInterface.h"
#include <vector> #include <vector>