#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 EMSCRIPTEN #include #endif #include "GameConstants.h" namespace ZL { #ifdef EMSCRIPTEN const char* CONST_ZIP_FILE = "resources.zip"; #else const char* CONST_ZIP_FILE = ""; #endif float x = 0; float y = 0; float z = 0; #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() : newTickCount(0) , lastTickCount(0) , menuManager(renderer) { } Game::~Game() { #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() { Environment::width = Environment::CONST_DEFAULT_WIDTH; Environment::height = Environment::CONST_DEFAULT_HEIGHT; 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", ""); #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); #endif loadingTexture = std::make_unique(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE)); float minDimension; float width = Environment::projectionWidth; float height = Environment::projectionHeight; if (width >= height) { minDimension = height; } else { minDimension = width; } loadingMesh.data = CreateRect2D({ 0.0f, 0.0f }, { minDimension*0.5f, minDimension*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 } 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 roomTexture = std::make_unique(CreateTextureDataFromPng("resources/w/room005.png", CONST_ZIP_FILE)); roomMesh.data = LoadFromTextFile02("resources/w/room001.txt", CONST_ZIP_FILE); roomMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitY())).toRotationMatrix()); roomMesh.RefreshVBO(); fireboxTexture = std::make_unique(CreateTextureDataFromPng("resources/w/Cube001.png", CONST_ZIP_FILE)); fireboxMesh.data = LoadFromTextFile02("resources/w/firebox.txt", CONST_ZIP_FILE); fireboxMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitY())).toRotationMatrix()); fireboxMesh.RefreshVBO(); inaiTexture = std::make_unique(CreateTextureDataFromPng("resources/w/inai001.png", CONST_ZIP_FILE)); inaiMesh.data = LoadFromTextFile02("resources/w/inai001.txt", CONST_ZIP_FILE); inaiMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitY())).toRotationMatrix()); inaiMesh.data.Move({ 2.5, 1.4, -9.9 }); inaiMesh.RefreshVBO(); benchTexture = std::make_unique(CreateTextureDataFromPng("resources/w/bench001opt.png", CONST_ZIP_FILE)); benchMesh.data = LoadFromTextFile02("resources/w/bench002opt.txt", CONST_ZIP_FILE); benchMesh.data.Scale(3.f); benchMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix()); benchMesh.data.Move({ -2.1, 0.5, -7.9 }); benchMesh.RefreshVBO(); auto violaTexture = std::make_shared(CreateTextureDataFromPng("resources/viola.png", CONST_ZIP_FILE)); // Player (Viola) player = std::make_unique(); player->loadAnimation(AnimationState::IDLE, "resources/idleviola_uv010.txt", CONST_ZIP_FILE); player->loadAnimation(AnimationState::WALK, "resources/walkviola_uv010.txt", CONST_ZIP_FILE); player->setTexture(violaTexture); player->walkSpeed = 3.0f; player->rotationSpeed = 8.0f; player->modelScale = 0.12f; player->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5f, Eigen::Vector3f::UnitX())) * Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitZ())); auto defaultTexture = std::make_shared(CreateTextureDataFromPng("resources/w/default_skin001.png", CONST_ZIP_FILE)); auto npc01 = std::make_unique(); npc01->loadAnimation(AnimationState::IDLE, "resources/w/default_idle002.txt", CONST_ZIP_FILE); npc01->loadAnimation(AnimationState::WALK, "resources/w/default_walk001.txt", CONST_ZIP_FILE); npc01->setTexture(defaultTexture); npc01->walkSpeed = 1.5f; npc01->rotationSpeed = 2.0f; npc01->modelScale = 0.01f; npc01->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())); /* Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5f, Eigen::Vector3f::UnitX())) * Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitZ()));*/ npc01->position = Eigen::Vector3f(0.f, 0.f, -45.f); npc01->setTarget(npc01->position); npcs.push_back(std::move(npc01)); loadingCompleted = true; scriptEngine.init(this); } void Game::drawUI() { glClear(GL_DEPTH_BUFFER_BIT); renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); glEnable(GL_BLEND); menuManager.uiManager.draw(renderer); glDisable(GL_BLEND); renderer.shaderManager.PopShader(); CheckGlError(); } void Game::drawGame() { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, static_cast(Environment::width) / static_cast(Environment::height), Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); renderer.PushMatrix(); renderer.LoadIdentity(); renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); //renderer.TranslateMatrix({ 0, -6.f, 0 }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero(); renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID()); renderer.DrawVertexRenderStruct(roomMesh); glBindTexture(GL_TEXTURE_2D, fireboxTexture->getTexID()); renderer.DrawVertexRenderStruct(fireboxMesh); glBindTexture(GL_TEXTURE_2D, inaiTexture->getTexID()); renderer.DrawVertexRenderStruct(inaiMesh); glBindTexture(GL_TEXTURE_2D, benchTexture->getTexID()); renderer.DrawVertexRenderStruct(benchMesh); if (player) player->draw(renderer); for (auto& npc : npcs) npc->draw(renderer); renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); } void Game::drawScene() { glViewport(0, 0, Environment::width, Environment::height); if (!loadingCompleted) { drawLoading(); } else { drawGame(); drawUI(); } CheckGlError(); } void Game::drawLoading() { glClear(GL_DEPTH_BUFFER_BIT); renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); float width = Environment::projectionWidth; float height = Environment::projectionHeight; renderer.PushProjectionMatrix( -width * 0.5f, width*0.5f, -height * 0.5f, height * 0.5f, -10, 10); renderer.PushMatrix(); renderer.LoadIdentity(); glBindTexture(GL_TEXTURE_2D, loadingTexture->getTexID()); renderer.DrawVertexRenderStruct(loadingMesh); renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); CheckGlError(); } int64_t Game::getSyncTimeMs() { int64_t localNow = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); 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; lastTickCount = newTickCount; if (player) player->update(delta); for (auto& npc : npcs) npc->update(delta); } } void Game::render() { ZL::CheckGlError(); glClearColor(0.0f, 0.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(static_cast(event.tfinger.fingerId), 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(static_cast(event.tfinger.fingerId), 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(static_cast(event.tfinger.fingerId), mx, my); } #else // Emscripten on mobile browser: handle real touch events with per-finger IDs. // SDL_HINT_TOUCH_MOUSE_EVENTS="0" is set in main.cpp so these don't // also fire SDL_MOUSEBUTTONDOWN, preventing double-processing. #ifdef EMSCRIPTEN if (event.type == SDL_FINGERDOWN) { int mx = static_cast(event.tfinger.x * Environment::projectionWidth); int my = static_cast(event.tfinger.y * Environment::projectionHeight); handleDown(static_cast(event.tfinger.fingerId), 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(static_cast(event.tfinger.fingerId), 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(static_cast(event.tfinger.fingerId), mx, my); } #endif if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) { if (event.button.button == SDL_BUTTON_LEFT) { // Преобразуем экранные пиксели в проекционные единицы 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(ZL::UiManager::MOUSE_FINGER_ID, mx, my); // Unproject click to ground plane (y=0) for Viola's walk target float ndcX = 2.0f * event.button.x / Environment::width - 1.0f; float ndcY = 1.0f - 2.0f * event.button.y / Environment::height; float aspect = (float)Environment::width / (float)Environment::height; float tanHalfFov = tan(CAMERA_FOV_Y * 0.5f); float cosAzim = cos(cameraAzimuth), sinAzim = sin(cameraAzimuth); float cosIncl = cos(cameraInclination), sinIncl = sin(cameraInclination); Eigen::Vector3f camRight(cosAzim, 0.f, sinAzim); Eigen::Vector3f camForward(sinAzim * cosIncl, -sinIncl, -cosAzim * cosIncl); Eigen::Vector3f camUp(sinAzim * sinIncl, cosIncl, -cosAzim * sinIncl); const Eigen::Vector3f& playerPos = player ? player->position : Eigen::Vector3f::Zero(); Eigen::Vector3f camPos = playerPos + Eigen::Vector3f(-sinAzim * cosIncl, sinIncl, cosAzim * cosIncl) * Environment::zoom; Eigen::Vector3f rayDir = (camForward + camRight * (ndcX * aspect * tanHalfFov) + camUp * (ndcY * tanHalfFov)).normalized(); if (rayDir.y() < -0.001f && player) { float t = -camPos.y() / rayDir.y(); Eigen::Vector3f hit = camPos + rayDir * t; player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z())); } } else { handleUp(ZL::UiManager::MOUSE_FINGER_ID, mx, my); } } else if (event.button.button == SDL_BUTTON_RIGHT) { if (event.type == SDL_MOUSEBUTTONDOWN) { rightMouseDown = true; lastMouseX = event.button.x; lastMouseY = event.button.y; } else { rightMouseDown = false; } } } 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(ZL::UiManager::MOUSE_FINGER_ID, mx, my); if (rightMouseDown) { int dx = event.motion.x - lastMouseX; int dy = event.motion.y - lastMouseY; lastMouseX = event.motion.x; lastMouseY = event.motion.y; const float sensitivity = 0.005f; cameraAzimuth += dx * sensitivity; cameraInclination += dy * sensitivity; const float minInclination = M_PI * 30.f / 180.f; const float maxInclination = M_PI * 0.5f; cameraInclination = max(minInclination, min(maxInclination, cameraInclination)); } } 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_r) { std::cout << "Camera position: x=" << x << " y=" << y << " z=" << z << std::endl; } } if (event.type == SDL_KEYUP) { /*benchMesh.data.Move({-x, -y, 0}); if (event.key.keysym.sym == SDLK_a) { x = x - 0.1; } if (event.key.keysym.sym == SDLK_d) { x = x + 0.1; } if (event.key.keysym.sym == SDLK_w) { y = y - 0.1; } if (event.key.keysym.sym == SDLK_s) { y = y + 0.1; } benchMesh.data.Move({ x, y, 0 }); benchMesh.RefreshVBO();*/ } #endif } render(); mainThreadHandler.processMainThreadTasks(); } void Game::handleDown(int64_t fingerId, int mx, int my) { int uiX = mx; int uiY = Environment::projectionHeight - my; menuManager.uiManager.onTouchDown(fingerId, uiX, uiY); } void Game::handleUp(int64_t fingerId, int mx, int my) { int uiX = mx; int uiY = Environment::projectionHeight - my; // Check BEFORE onTouchUp erases the finger from the map. // If this finger started on a UI element, don't notify space — // otherwise space would think the ship-control finger was released. bool wasUiInteraction = menuManager.uiManager.isUiInteractionForFinger(fingerId); menuManager.uiManager.onTouchUp(fingerId, uiX, uiY); } void Game::handleMotion(int64_t fingerId, int mx, int my) { int uiX = mx; int uiY = Environment::projectionHeight - my; // Check before onTouchMove so the "started on UI" state is preserved // regardless of what onTouchMove does internally. bool wasUiInteraction = menuManager.uiManager.isUiInteractionForFinger(fingerId); menuManager.uiManager.onTouchMove(fingerId, uiX, uiY); } } // namespace ZL