#include "Game.h" #include "AnimatedModel.h" #include "BoneAnimatedModel.h" #include "planet/PlanetData.h" #include "utils/Utils.h" #include "render/OpenGlExtensions.h" #include #include "render/TextureManager.h" #include "TextModel.h" #include #include #include #include #ifdef __ANDROID__ #include #endif #ifdef NETWORK #ifdef EMSCRIPTEN #include "network/WebSocketClientEmscripten.h" #else #include "network/WebSocketClient.h" #endif #endif #ifdef EMSCRIPTEN #include #endif #include "network/LocalClient.h" #include "network/ClientState.h" namespace ZL { #ifdef EMSCRIPTEN const char* CONST_ZIP_FILE = "resources.zip"; #else //const char* CONST_ZIP_FILE = "C:\\Work\\Projects\\space-game001\\resources.zip"; 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) , newTickCount(0) , lastTickCount(0) , menuManager(renderer) , space(renderer, taskManager, mainThreadHandler, networkClient, menuManager) { } 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. SDL_Quit(); #endif } void Game::setup() { glContext = SDL_GL_CreateContext(ZL::Environment::window); Environment::computeProjectionDimensions(); ZL::BindOpenGlFunctions(); ZL::CheckGlError(); renderer.InitOpenGL(); #ifdef EMSCRIPTEN // 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 loadingMesh.data = CreateRect2D({ Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, { Environment::projectionWidth * 0.5f, Environment::projectionHeight * 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; emscripten_async_wget("resources.zip", "resources.zip", onResourcesZipLoaded, onResourcesZipError); #else mainThreadHandler.EnqueueMainThreadTask([this]() { this->setupPart2(); }); #endif } void Game::setupPart2() { #ifdef EMSCRIPTEN renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/defaultAtmosphere.vertex", "resources/shaders/defaultAtmosphere_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/planet_bake.vertex", "resources/shaders/planet_bake_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/planet_stone.vertex", "resources/shaders/planet_stone_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/planet_land.vertex", "resources/shaders/planet_land_web.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("spark", "resources/shaders/spark.vertex", "resources/shaders/spark_web.fragment", CONST_ZIP_FILE); #else renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/defaultAtmosphere.vertex", "resources/shaders/defaultAtmosphere_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/planet_bake.vertex", "resources/shaders/planet_bake_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/planet_stone.vertex", "resources/shaders/planet_stone_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/planet_land.vertex", "resources/shaders/planet_land_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("spark", "resources/shaders/spark.vertex", "resources/shaders/spark_desktop.fragment", CONST_ZIP_FILE); #endif menuManager.setupMenu(); menuManager.onSingleplayerPressed = [this](const std::string& nickname, int shipType) { Environment::shipState.nickname = nickname; Environment::shipState.shipType = shipType; auto localClient = new LocalClient; ClientState st = Environment::shipState; st.id = localClient->GetClientId(); localClient->setLocalPlayerState(st); networkClient = std::unique_ptr(localClient); networkClient->Connect("", 0); lastTickCount = 0; spaceGameStarted = 1; }; menuManager.onMultiplayerPressed = [this](const std::string& nickname, int shipType) { Environment::shipState.nickname = nickname; Environment::shipState.shipType = shipType; #ifdef EMSCRIPTEN networkClient = std::make_unique(); networkClient->Connect("localhost", 8081); #else networkClient = std::make_unique(taskManager.getIOContext()); networkClient->Connect("localhost", 8081); #endif if (networkClient) { std::string joinMsg = std::string("JOIN:") + nickname + ":" + std::to_string(shipType); networkClient->Send(joinMsg); std::cerr << "Sent JOIN: " << joinMsg << std::endl; } lastTickCount = 0; spaceGameStarted = 1; }; space.setup(); loadingCompleted = true; } void Game::drawUI() { static const std::string defaultShaderName = "default"; static const std::string envShaderName = "env"; static const std::string vPositionName = "vPosition"; static const std::string vTexCoordName = "vTexCoord"; static const std::string textureUniformName = "Texture"; glClear(GL_DEPTH_BUFFER_BIT); renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); renderer.EnableVertexAttribArray(vPositionName); renderer.EnableVertexAttribArray(vTexCoordName); renderer.DisableVertexAttribArray(vPositionName); renderer.DisableVertexAttribArray(vTexCoordName); glEnable(GL_BLEND); menuManager.uiManager.draw(renderer); glDisable(GL_BLEND); renderer.shaderManager.PopShader(); CheckGlError(); } void Game::drawUnderMainMenu() { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); } void Game::drawScene() { if (!loadingCompleted) { drawLoading(); } else { if (spaceGameStarted) { space.drawScene(); } else { drawUnderMainMenu(); } drawUI(); } CheckGlError(); } void Game::drawLoading() { static const std::string defaultShaderName = "default"; static const std::string vPositionName = "vPosition"; static const std::string vTexCoordName = "vTexCoord"; static const std::string textureUniformName = "Texture"; glClear(GL_DEPTH_BUFFER_BIT); renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); renderer.EnableVertexAttribArray(vPositionName); renderer.EnableVertexAttribArray(vTexCoordName); float width = Environment::projectionWidth; float height = Environment::projectionHeight; renderer.PushProjectionMatrix( 0, width, 0, height, -10, 10); renderer.PushMatrix(); renderer.LoadIdentity(); glBindTexture(GL_TEXTURE_2D, loadingTexture->getTexID()); renderer.DrawVertexRenderStruct(loadingMesh); renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.DisableVertexAttribArray(vPositionName); renderer.DisableVertexAttribArray(vTexCoordName); renderer.shaderManager.PopShader(); CheckGlError(); } int64_t Game::getSyncTimeMs() { int64_t localNow = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); // Добавляем смещение, полученное от сервера if (networkClient) { return localNow + networkClient->getTimeOffset(); } else { return localNow; } } void Game::processTickCount() { if (lastTickCount == 0) { lastTickCount = getSyncTimeMs(); lastTickCount = (lastTickCount / 50) * 50; return; } newTickCount = getSyncTimeMs(); newTickCount = (newTickCount / 50) * 50; if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) { int64_t delta = newTickCount - lastTickCount; if (delta > CONST_MAX_TIME_INTERVAL) { //throw std::runtime_error("Synchronization is lost"); } if (spaceGameStarted) { space.processTickCount(newTickCount, delta); } menuManager.uiManager.update(static_cast(delta)); lastTickCount = newTickCount; } } void Game::render() { SDL_GL_MakeCurrent(ZL::Environment::window, glContext); ZL::CheckGlError(); glClearColor(0.0f, 1.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); drawScene(); processTickCount(); SDL_GL_SwapWindow(ZL::Environment::window); } void Game::update() { SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { Environment::exitGameLoop = true; } if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) { // Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях Environment::width = event.window.data1; Environment::height = event.window.data2; Environment::computeProjectionDimensions(); menuManager.uiManager.updateAllLayouts(); std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl; space.clearTextRendererCache(); } #ifdef __ANDROID__ if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) { Environment::exitGameLoop = true; } #endif #ifdef __ANDROID__ if (event.type == SDL_FINGERDOWN) { int mx = static_cast(event.tfinger.x * Environment::projectionWidth); int my = static_cast(event.tfinger.y * Environment::projectionHeight); handleDown(mx, my); } else if (event.type == SDL_FINGERUP) { int mx = static_cast(event.tfinger.x * Environment::projectionWidth); int my = static_cast(event.tfinger.y * Environment::projectionHeight); handleUp(mx, my); } else if (event.type == SDL_FINGERMOTION) { int mx = static_cast(event.tfinger.x * Environment::projectionWidth); int my = static_cast(event.tfinger.y * Environment::projectionHeight); handleMotion(mx, my); } #else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) { // Преобразуем экранные пиксели в проекционные единицы int mx = static_cast((float)event.button.x / Environment::width * Environment::projectionWidth); int my = static_cast((float)event.button.y / Environment::height * Environment::projectionHeight); if (event.type == SDL_MOUSEBUTTONDOWN) handleDown(mx, my); else handleUp(mx, my); } else if (event.type == SDL_MOUSEMOTION) { int mx = static_cast((float)event.motion.x / Environment::width * Environment::projectionWidth); int my = static_cast((float)event.motion.y / Environment::height * Environment::projectionHeight); handleMotion(mx, my); } /*if (event.type == SDL_MOUSEBUTTONDOWN) { int mx = event.button.x; int my = event.button.y; handleDown(mx, my); } if (event.type == SDL_MOUSEBUTTONUP) { int mx = event.button.x; int my = event.button.y; handleUp(mx, my); } if (event.type == SDL_MOUSEMOTION) { int mx = event.motion.x; int my = event.motion.y; handleMotion(mx, my); }*/ if (event.type == SDL_MOUSEWHEEL) { static const float zoomstep = 2.0f; if (event.wheel.y > 0) { Environment::zoom -= zoomstep; } else if (event.wheel.y < 0) { Environment::zoom += zoomstep; } if (Environment::zoom < zoomstep) { Environment::zoom = zoomstep; } } // Обработка ввода текста if (event.type == SDL_KEYDOWN) { if (event.key.keysym.sym == SDLK_BACKSPACE) { menuManager.uiManager.onKeyBackspace(); } } if (event.type == SDL_TEXTINPUT) { // Пропускаем ctrl+c и другие команды if ((event.text.text[0] & 0x80) == 0) { // ASCII символы menuManager.uiManager.onKeyPress((unsigned char)event.text.text[0]); } } if (event.type == SDL_KEYUP) { if (event.key.keysym.sym == SDLK_a) { //Environment::shipState.position = { 9466.15820, 1046.00159, 18531.2090 }; } } #endif } render(); if (networkClient) { #ifndef NETWORK auto localClient = dynamic_cast(networkClient.get()); if (localClient) { localClient->setLocalPlayerState(Environment::shipState); } #endif networkClient->Poll(); } mainThreadHandler.processMainThreadTasks(); if (spaceGameStarted) { space.update(); } } void Game::handleDown(int mx, int my) { int uiX = mx; int uiY = Environment::projectionHeight - my; menuManager.uiManager.onMouseDown(uiX, uiY); bool uiHandled = false; for (const auto& button : menuManager.uiManager.findButton("") ? std::vector>{} : std::vector>{}) { (void)button; } auto pressedSlider = [&]() -> std::shared_ptr { for (const auto& slider : menuManager.uiManager.findSlider("") ? std::vector>{} : std::vector>{}) { (void)slider; } return nullptr; }(); if (!menuManager.uiManager.isUiInteraction()) { if (spaceGameStarted) { space.handleDown(mx, my); } } } void Game::handleUp(int mx, int my) { int uiX = mx; int uiY = Environment::projectionHeight - my; menuManager.uiManager.onMouseUp(uiX, uiY); if (!menuManager.uiManager.isUiInteraction()) { if (spaceGameStarted) { space.handleUp(mx, my); } } } void Game::handleMotion(int mx, int my) { int uiX = mx; int uiY = Environment::projectionHeight - my; menuManager.uiManager.onMouseMove(uiX, uiY); if (!menuManager.uiManager.isUiInteraction()) { if (spaceGameStarted) { space.handleMotion(mx, my); } } } } // namespace ZL