space-game001/src/Game.cpp
Vladislav Khorev 63c9d7b35b Adding scripts
2026-04-03 19:46:43 +03:00

572 lines
20 KiB
C++

#include "Game.h"
#include "AnimatedModel.h"
#include "BoneAnimatedModel.h"
#include "planet/PlanetData.h"
#include "utils/Utils.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
#include "render/TextureManager.h"
#include "TextModel.h"
#include <random>
#include <cmath>
#include <algorithm>
#include <functional>
#ifdef __ANDROID__
#include <android/log.h>
#endif
#ifdef EMSCRIPTEN
#include <emscripten.h>
#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<Texture>(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<Texture>(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<Texture>(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<Texture>(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<Texture>(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<Texture>(CreateTextureDataFromPng("resources/viola.png", CONST_ZIP_FILE));
// Player (Viola)
player = std::make_unique<Character>();
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<Texture>(CreateTextureDataFromPng("resources/w/default_skin001.png", CONST_ZIP_FILE));
auto npc01 = std::make_unique<Character>();
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<float>(Environment::width) / static_cast<float>(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::milliseconds>(
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<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleDown(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
}
else if (event.type == SDL_FINGERUP) {
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleUp(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
}
else if (event.type == SDL_FINGERMOTION) {
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleMotion(static_cast<int64_t>(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<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleDown(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
}
else if (event.type == SDL_FINGERUP) {
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleUp(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
}
else if (event.type == SDL_FINGERMOTION) {
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleMotion(static_cast<int64_t>(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<int>((float)event.button.x / Environment::width * Environment::projectionWidth);
int my = static_cast<int>((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<int>((float)event.motion.x / Environment::width * Environment::projectionWidth);
int my = static_cast<int>((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