#include "Engine.h" // ============================================================================ // Standard library // ============================================================================ #include #include #include #include #include #include #include #include #include // ============================================================================ // OpenGL // ============================================================================ #include #include // ============================================================================ // Math // ============================================================================ #include #include #include #include // ============================================================================ // Third party // ============================================================================ #include #include FT_FREETYPE_H // ============================================================================ // Engine core // ============================================================================ #include "common.h" #include "Window.h" #include "Camera.h" #include "Keyboard.h" #include "Mouse.h" #include "Physics.h" #include "RenderSystem.h" #include "SoundSystem.h" #include "ResourceManager.h" #include "tFunctions.h" #include "Debug.h" // Rendering #include "shaderClass.h" #include "VBO.h" #include "VAO.h" #include "EBO.h" #include "FBO.h" #include "texture.h" #include "Material.h" #include "Mesh.h" #include "Model.h" #include "Cubemap.h" #include "QuadVertices.h" #include "Particle.h" #include "Lighting.h" #include "DirLight.h" // Scene #include "Folder.h" #include "Sound.h" #include "Gui.h" #include "font.h" // Character / animation #include "Rig.h" #include "Animator.h" #include "Character.h" #include "CharacterManager.h" #include "Ragdoll.h" #include "Console.h" #include #include #include "CharacterMind.h" #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" // ============================================================================ // Constructor - window, callbacks, systems, shaders // ============================================================================ Engine::Engine() { engine = this; glfwSetFramebufferSizeCallback(window->handle, framebuffer_size_callback); glfwSetCursorPosCallback(window->handle, mouse_callback); glfwSetScrollCallback(window->handle, scroll_callback); glfwSetKeyCallback(window->handle, key_callback); glfwSetCharCallback(window->handle, char_callback); glfwSetMouseButtonCallback(window->handle, mouse_button_callback); glfwSwapInterval(1); int fbw, fbh; glfwGetFramebufferSize(window->handle, &fbw, &fbh); framebuffer_size_callback(window->handle, fbw, fbh); rendersystem = new RenderSystem(); rendersystem->Initialize(); physicsengine = new Physics(); physicsengine->SetGravity(physicsengine->GetGravity() * 1.25f); mainf = new Folder(); camera = new Camera(); camera->t.TranslateTo({ 0.0f, 5.0f, 0.0f }); rendersystem->RigShader = new Shader("shaders/rig.vert", "shaders/default.frag"); rendersystem->MeshShader = new Shader("shaders/default.vert", "shaders/default.frag"); rendersystem->GeometryShader = new Shader("shaders/geometry.vert", "shaders/geometry.frag"); rendersystem->GeometryRigShader = new Shader("shaders/geometry_rig.vert", "shaders/geometry.frag"); rendersystem->DeferredLightingShader = new Shader("shaders/deferred_lighting.vert", "shaders/deferred_lighting.frag"); rendersystem->GBufferDebugShader = new Shader("shaders/gbuffer_debug.vert", "shaders/gbuffer_debug.frag"); rendersystem->BloomExtractShader = new Shader("shaders/ScreenShader.vert", "shaders/bloom_extract.frag"); rendersystem->BloomBlurShader = new Shader("shaders/ScreenShader.vert", "shaders/bloom_blur.frag"); rendersystem->MeshShadowShader = new Shader("shaders/meshshadow.vert", "shaders/shadow.frag"); rendersystem->RigShadowShader = new Shader("shaders/rigshadow.vert", "shaders/shadow.frag"); rendersystem->SkyboxShader = new Shader("shaders/skybox.vert", "shaders/skybox.frag"); rendersystem->ScreenShader = new Shader("shaders/ScreenShader.vert", "shaders/ScreenShader.frag"); rendersystem->ImageBoxShader = new Shader("shaders/ImageBox.vert", "shaders/ImageBox.frag"); rendersystem->BoxShader = new Shader("shaders/Box.vert", "shaders/Box.frag"); rendersystem->Text2DShader = new Shader("shaders/2DText.vert", "shaders/2DText.frag"); rendersystem->ParticleShader = new Shader("shaders/Particle.vert", "shaders/Particle.frag"); rendersystem->HitboxShader = new Shader("shaders/Hitbox.vert", "shaders/Hitbox.frag"); } // ============================================================================ // Initiate - scene setup + game loop // ============================================================================ void Engine::Initiate() { Font* arial = resourcemanager->GetFont("assets/ft/arial.ttf"); Texture* blanktex = resourcemanager->GetTexture("assets/blanktexture.png"); charactermanager = new CharacterManager(); charactermanager->LoadRig("humanoid", "assets/rig.glb"); charactermanager->LoadAnimation("walk", "assets/walkanim.glb", "humanoid"); charactermanager->LoadAnimation("idle", "assets/idle.glb", "humanoid"); // ------------------------------------------------------------------------ // Player character // ------------------------------------------------------------------------ // Idle is the base pose, walking fades in on top of it while moving Character& testcharacter = *charactermanager->CreateCharacter("character", "humanoid", "idle"); testcharacter.body->SetWalkAnimation(charactermanager->GetAnimation("walk")); testcharacter.castshadow = true; testcharacter.colorOverride = glm::vec4(0.62f, 0.64f, 0.68f, 1.0f); testcharacter.roughness = 0.65f; testcharacter.body->t.ScaleBy({ 0.7f, 2.5f, 0.7f }); testcharacter.body->t.TranslateTo({ 0.0f, 1.5f, 0.0f }); testcharacter.body->animator->loops = true; testcharacter.BindToRenderSystem(); testcharacter.CreatePhysicsBody(*physicsengine, 80.0f); // ------------------------------------------------------------------------ // NPC // ------------------------------------------------------------------------ Character& npc = *charactermanager->CreateCharacter("npc", "humanoid", "idle"); npc.body->SetWalkAnimation(charactermanager->GetAnimation("walk")); npc.castshadow = true; npc.colorOverride = glm::vec4(0.10f, 0.20f, 0.48f, 1.0f); npc.metallic = 0.1f; npc.roughness = 0.5f; npc.body->t.ScaleBy({ 0.7f, 2.5f, 0.7f }); npc.body->t.TranslateTo({ 3.0f, 1.25f, 3.0f }); npc.BindToRenderSystem(); npc.CreatePhysicsBody(*physicsengine, 80.0f); testcharacter.body->SetWalking(false); npc.body->SetWalking(false); // The keyboard is the player's mind: it turns WASD into actions for whichever character it's in. // The camera follows that character too, C moves both to the other one. KeyboardMind keyboardMind(*camera); Character* controlled = &testcharacter; controlled->mind = &keyboardMind; // ------------------------------------------------------------------------ // Console commands. help and clear are built in. // ------------------------------------------------------------------------ console = new Console(arial); // From the camera, out through a point on the screen in pixels auto MouseRay = [&](const glm::vec2& mousePos) { const glm::mat4 inverseViewProjection = glm::inverse(camera->GetProjectionMatrix(0.05f, 2000.0f) * camera->GetViewMatrix()); const glm::vec2 ndc(mousePos.x / window->width * 2.0f - 1.0f, 1.0f - mousePos.y / window->height * 2.0f); glm::vec4 nearPoint = inverseViewProjection * glm::vec4(ndc, -1.0f, 1.0f); glm::vec4 farPoint = inverseViewProjection * glm::vec4(ndc, 1.0f, 1.0f); nearPoint /= nearPoint.w; farPoint /= farPoint.w; Ray ray; ray.origin = glm::vec3(nearPoint); ray.direction = glm::normalize(glm::vec3(farPoint - nearPoint)); return ray; }; // Debug cubes G throws, each a mesh and the physics body moving it. Cleared once they fall off the world. struct DebugCube { Mesh* mesh = nullptr; p* body = nullptr; }; std::vector debugCubes; auto SpawnDebugCube = [&](const glm::vec3& position, const glm::vec3& velocity) { DebugCube cube; cube.mesh = CreateCubeMesh(); cube.mesh->name = "debug_cube"; cube.mesh->colorOverride = glm::vec4(1.0f, 0.4f, 0.1f, 1.0f); cube.mesh->roughness = 0.7f; cube.mesh->castshadow = true; cube.mesh->t.ScaleTo(0.3f); cube.mesh->t.RotateToQuaternion(glm::quat(1.0f, 0.0f, 0.0f, 0.0f)); cube.mesh->t.TranslateTo(position); cube.mesh->BindToRenderSystem(); cube.body = new p(cube.mesh); cube.body->mass = 2.0f; cube.body->restitution = 0.1f; cube.body->friction = 0.7f; cube.body->maxSpeed = 40.0f; cube.body->linearvelocity = velocity; physicsengine->AddObject(cube.body); debugCubes.push_back(cube); }; auto ParseFloat = [](const std::string& text, float& value) { try { size_t used = 0; value = std::stof(text, &used); return used == text.size(); } catch (...) { return false; } }; console->Register("tp", "tp ", "teleports you", [&](const Console::Arguments& args) { glm::vec3 position; if (args.size() < 3 || !ParseFloat(args[0], position.x) || !ParseFloat(args[1], position.y) || !ParseFloat(args[2], position.z)) { console->Print("Usage: tp "); return; } controlled->body->t.TranslateTo(position); controlled->body->physics->linearvelocity = glm::vec3(0.0f); controlled->body->physics->WakeUp(); console->Print("Teleported"); }); console->Register("gravity", "gravity [strength]", "shows or sets how hard things fall, in m/s^2", [&](const Console::Arguments& args) { float strength = 0.0f; if (!args.empty()) { if (!ParseFloat(args[0], strength)) { console->Print("Usage: gravity [strength]"); return; } physicsengine->SetGravity(glm::vec3(0.0f, -strength, 0.0f)); } std::ostringstream text; text << "Gravity is " << std::fixed << std::setprecision(2) << -physicsengine->GetGravity().y << " m/s^2"; console->Print(text.str()); }); console->Register("quit", "quit", "closes the game", [&](const Console::Arguments&) { glfwSetWindowShouldClose(window->handle, 1); }); // ------------------------------------------------------------------------ // 3D audio // ------------------------------------------------------------------------ Sound* music = new Sound(); music->name = "music"; music->LoadSound("assets/fs.wav", true); music->Update3DPosition(0.0f, 0.0f, 0.0f); music->PlayTrack(true); mainf->AddChild(music); // ------------------------------------------------------------------------ // GUI // ------------------------------------------------------------------------ TextBox* fpsCounter = new TextBox(); fpsCounter->font = arial; fpsCounter->text = ""; fpsCounter->fontsize = 0.5f; fpsCounter->Color = { 0.0f, 0.0f, 0.0f }; fpsCounter->t2d.position = { 1.0f, 0.0f, 0.0f, 0.0f }; fpsCounter->t2d.center = { 1.0f, 0.0f }; fpsCounter->textCenter = { 0.0f, 0.5f }; fpsCounter->t2d.size = { 0.1f, 0.1f, 0.0f, 0.0f }; fpsCounter->BindToRenderSystem(); struct MenuButton { Box* box; Box* accent; TextBox* label; glm::vec3 color; glm::vec3 hoverColor; float opacity; float labelX; float labelSlide; // how far the label moves right on hover bool hovered = false; float hover = 0.0f; // eased 0-1 }; enum class MenuScreen { Main, Paused, Settings, Graphics }; MenuScreen menuScreen = MenuScreen::Main; MenuScreen settingsReturn = MenuScreen::Main; bool menuActive = true; bool prevMouseDown = false; std::vector menuItems; // Everything hangs off a left side panel so the live scene stays visible. // Items are positioned in pixels from the left edge and the vertical center of the window. const float menuPanelWidth = 460.0f; const float menuMargin = 48.0f; const float menuContentWidth = 364.0f; const glm::vec3 menuAccent(0.96f, 0.58f, 0.16f); const glm::vec3 menuText(0.95f, 0.97f, 1.0f); const glm::vec3 menuMutedText(0.62f, 0.68f, 0.77f); const glm::vec3 menuButtonColor(0.11f, 0.14f, 0.19f); const glm::vec3 menuButtonHover(0.20f, 0.25f, 0.33f); Box* menuVeil = new Box(); menuVeil->Color = { 0.02f, 0.03f, 0.05f }; menuVeil->t2d.position = { 0.0f, 0.0f, 0.0f, 0.0f }; menuVeil->t2d.size = { 1.0f, 1.0f, 0.0f, 0.0f }; menuVeil->BindToRenderSystem(); menuItems.push_back(menuVeil); Box* menuPanel = new Box(); menuPanel->Color = { 0.03f, 0.04f, 0.06f }; menuPanel->t2d.position = { 0.0f, 0.0f, 0.0f, 0.0f }; menuPanel->t2d.size = { 0.0f, 1.0f, menuPanelWidth, 0.0f }; menuPanel->BindToRenderSystem(); menuItems.push_back(menuPanel); Box* menuPanelEdge = new Box(); menuPanelEdge->Color = menuAccent; menuPanelEdge->t2d.position = { 0.0f, 0.0f, menuPanelWidth, 0.0f }; menuPanelEdge->t2d.size = { 0.0f, 1.0f, 3.0f, 0.0f }; menuPanelEdge->BindToRenderSystem(); menuItems.push_back(menuPanelEdge); auto MakeMenuText = [&](const char* text, float x, float y, float width, float height, float fontsize, glm::vec3 color, float alignX = 0.0f) { TextBox* label = new TextBox(); label->font = arial; label->text = text; label->fontsize = fontsize; label->Color = color; label->t2d.position = { 0.0f, 0.5f, x, y }; label->t2d.center = { 0.0f, 0.5f }; label->t2d.size = { 0.0f, 0.0f, width, height }; label->textCenter = { alignX, 0.5f }; label->BindToRenderSystem(); menuItems.push_back(label); return label; }; TextBox* menuTitle = MakeMenuText("EIIFWHRN", menuMargin, -235.0f, 400.0f, 100.0f, 1.6f, menuText); TextBox* menuSubtitle = MakeMenuText("MAIN MENU", menuMargin, -165.0f, menuContentWidth, 36.0f, 0.5f, menuAccent); auto MakeMenuButton = [&](const char* text, float x, float y, float width, float height, float fontsize, glm::vec3 color, glm::vec3 hoverColor, float opacity = 0.6f, bool compact = false) { Box* box = new Box(); box->Color = color; box->t2d.position = { 0.0f, 0.5f, x, y }; box->t2d.center = { 0.0f, 0.5f }; box->t2d.size = { 0.0f, 0.0f, width, height }; box->rounding = 0.05f; box->BindToRenderSystem(); menuItems.push_back(box); Box* accent = new Box(); accent->Color = menuAccent; accent->t2d.position = { 0.0f, 0.5f, x, y }; accent->t2d.center = { 0.0f, 0.5f }; accent->t2d.size = { 0.0f, 0.0f, 0.0f, height }; accent->BindToRenderSystem(); menuItems.push_back(accent); // Compact buttons hold a single centered symbol, the rest are left aligned and slide on hover const float padding = compact ? 0.0f : 20.0f; TextBox* label = MakeMenuText(text, x + padding, y, width - padding * 2.0f, height, fontsize, menuText, compact ? 0.5f : 0.0f); return MenuButton{ box, accent, label, color, hoverColor, opacity, x + padding, compact ? 0.0f : 8.0f }; }; const float mainButtonHeight = 60.0f; MenuButton playButton = MakeMenuButton("PLAY", menuMargin, -60.0f, menuContentWidth, mainButtonHeight, 0.75f, glm::vec3(0.82f, 0.45f, 0.10f), menuAccent, 0.95f); MenuButton settingsButton = MakeMenuButton("SETTINGS", menuMargin, 12.0f, menuContentWidth, mainButtonHeight, 0.75f, menuButtonColor, menuButtonHover); MenuButton quitButton = MakeMenuButton("QUIT", menuMargin, 84.0f, menuContentWidth, mainButtonHeight, 0.75f, menuButtonColor, glm::vec3(0.42f, 0.14f, 0.16f)); // Settings rows: LABEL [-] VALUE [+] const float stepperDownX = menuMargin + 176.0f; const float stepperValueX = stepperDownX + 44.0f; const float stepperUpX = menuMargin + menuContentWidth - 44.0f; const float stepperValueWidth = stepperUpX - stepperValueX; const float sensRowY = -120.0f; const float fovRowY = -64.0f; TextBox* sensLabel = MakeMenuText("SENSITIVITY", menuMargin, sensRowY, 172.0f, 44.0f, 0.5f, menuMutedText); TextBox* sensValue = MakeMenuText("", stepperValueX, sensRowY, stepperValueWidth, 44.0f, 0.5f, menuText, 0.5f); TextBox* fovLabel = MakeMenuText("FIELD OF VIEW", menuMargin, fovRowY, 172.0f, 44.0f, 0.5f, menuMutedText); TextBox* fovValue = MakeMenuText("", stepperValueX, fovRowY, stepperValueWidth, 44.0f, 0.5f, menuText, 0.5f); MenuButton sensitivityDown = MakeMenuButton("-", stepperDownX, sensRowY, 44.0f, 44.0f, 0.7f, menuButtonColor, menuButtonHover, 0.6f, true); MenuButton sensitivityUp = MakeMenuButton("+", stepperUpX, sensRowY, 44.0f, 44.0f, 0.7f, menuButtonColor, menuButtonHover, 0.6f, true); MenuButton fovDown = MakeMenuButton("-", stepperDownX, fovRowY, 44.0f, 44.0f, 0.7f, menuButtonColor, menuButtonHover, 0.6f, true); MenuButton fovUp = MakeMenuButton("+", stepperUpX, fovRowY, 44.0f, 44.0f, 0.7f, menuButtonColor, menuButtonHover, 0.6f, true); MenuButton hitboxToggle = MakeMenuButton("HITBOXES: OFF", menuMargin, -18.0f, menuContentWidth, 40.0f, 0.6f, menuButtonColor, menuButtonHover); MenuButton graphicsButton = MakeMenuButton("GRAPHICS", menuMargin, 26.0f, menuContentWidth, 40.0f, 0.6f, menuButtonColor, menuButtonHover); MenuButton resetButton = MakeMenuButton("RESET DEFAULTS", menuMargin, 70.0f, menuContentWidth, 40.0f, 0.6f, menuButtonColor, menuButtonHover); MenuButton bloomToggle = MakeMenuButton("", menuMargin, -128.0f, menuContentWidth, 44.0f, 0.6f, menuButtonColor, menuButtonHover); MenuButton shadowToggle = MakeMenuButton("", menuMargin, -78.0f, menuContentWidth, 44.0f, 0.6f, menuButtonColor, menuButtonHover); MenuButton lightShadowToggle = MakeMenuButton("", menuMargin, -28.0f, menuContentWidth, 44.0f, 0.6f, menuButtonColor, menuButtonHover); MenuButton vsyncToggle = MakeMenuButton("", menuMargin, 22.0f, menuContentWidth, 44.0f, 0.6f, menuButtonColor, menuButtonHover); MenuButton cullingToggle = MakeMenuButton("", menuMargin, 72.0f, menuContentWidth, 44.0f, 0.6f, menuButtonColor, menuButtonHover); MenuButton backButton = MakeMenuButton("BACK", menuMargin, 120.0f, menuContentWidth, 50.0f, 0.6f, glm::vec3(0.82f, 0.45f, 0.10f), menuAccent, 0.95f); std::vector menuButtons = { &playButton, &settingsButton, &quitButton, &sensitivityDown, &sensitivityUp, &fovDown, &fovUp, &hitboxToggle, &graphicsButton, &resetButton, &backButton, &bloomToggle, &shadowToggle, &lightShadowToggle, &vsyncToggle, &cullingToggle }; auto SetButtonVisible = [](MenuButton& button, bool visible) { button.box->Opacity = visible ? button.opacity : 0.0f; button.accent->Opacity = visible ? 1.0f : 0.0f; button.label->Opacity = visible ? 1.0f : 0.0f; }; auto ApplyMenuLayout = [&]() { const bool main = menuScreen == MenuScreen::Main; const bool paused = menuScreen == MenuScreen::Paused; const bool settings = menuScreen == MenuScreen::Settings; const bool graphics = menuScreen == MenuScreen::Graphics; const bool mainButtons = main || paused; menuVeil->Opacity = 0.22f; menuPanel->Opacity = 0.82f; menuPanelEdge->Opacity = 1.0f; menuTitle->Opacity = 1.0f; menuSubtitle->Opacity = 1.0f; menuSubtitle->text = main ? "MAIN MENU" : (paused ? "PAUSED" : (graphics ? "GRAPHICS" : "SETTINGS")); playButton.label->text = paused ? "RESUME" : "PLAY"; quitButton.label->text = paused ? "QUIT TO MENU" : "QUIT"; SetButtonVisible(playButton, mainButtons); SetButtonVisible(settingsButton, mainButtons); SetButtonVisible(quitButton, mainButtons); SetButtonVisible(sensitivityDown, settings); SetButtonVisible(sensitivityUp, settings); SetButtonVisible(fovDown, settings); SetButtonVisible(fovUp, settings); SetButtonVisible(hitboxToggle, settings); SetButtonVisible(graphicsButton, settings); SetButtonVisible(resetButton, settings); SetButtonVisible(backButton, settings || graphics); auto ShowToggle = [&](MenuButton& button, const char* name, bool enabled) { SetButtonVisible(button, graphics); button.label->text = std::string(name) + (enabled ? ": ON" : ": OFF"); button.color = enabled ? glm::vec3(0.82f, 0.45f, 0.10f) : menuButtonColor; }; ShowToggle(bloomToggle, "BLOOM", rendersystem->graphics.bloom); ShowToggle(shadowToggle, "SUN SHADOWS", rendersystem->graphics.shadows); ShowToggle(lightShadowToggle, "LIGHT SHADOWS", rendersystem->graphics.lightShadows); ShowToggle(vsyncToggle, "VSYNC", rendersystem->graphics.vsync); ShowToggle(cullingToggle, "CULLING", rendersystem->graphics.culling); sensLabel->Opacity = settings ? 1.0f : 0.0f; sensValue->Opacity = settings ? 1.0f : 0.0f; fovLabel->Opacity = settings ? 1.0f : 0.0f; fovValue->Opacity = settings ? 1.0f : 0.0f; hitboxToggle.label->text = showHitboxes ? "HITBOXES: ON" : "HITBOXES: OFF"; hitboxToggle.color = showHitboxes ? glm::vec3(0.82f, 0.45f, 0.10f) : menuButtonColor; sensValue->text = std::to_string((int)std::lround(StartupCmPer360)) + " CM"; fovValue->text = std::to_string((int)std::lround(camera->FOV)); }; auto OpenMenu = [&](MenuScreen screen) { menuActive = true; menuScreen = screen; testcharacter.body->SetWalking(false); npc.body->SetWalking(false); fpsCounter->Opacity = 0.0f; camera->lockedcursor = false; glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL); prevMouseDown = mouse->IsLeftDown(); }; auto ResumeGame = [&]() { menuActive = false; for (Box* item : menuItems) item->Opacity = 0.0f; fpsCounter->Opacity = 1.0f; camera->lockedcursor = true; glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); int ww, wh; glfwGetWindowSize(window->handle, &ww, &wh); glfwSetCursorPos(window->handle, ww * 0.5, wh * 0.5); }; fpsCounter->Opacity = 0.0f; camera->lockedcursor = false; glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL); // ------------------------------------------------------------------------ // Baseplate, a flat slab with its top at y = 0 // ------------------------------------------------------------------------ Mesh* baseplate = CreateCubeMesh(); baseplate->name = "baseplate"; baseplate->t.ScaleTo({ 200.0f, 2.0f, 200.0f }); baseplate->t.TranslateTo({ 0.0f, -1.0f, 0.0f }); baseplate->t.RotateToQuaternion(glm::quat(1.0f, 0.0f, 0.0f, 0.0f)); baseplate->surface = SurfaceTexture::Tiled(blanktex, 8.0f); baseplate->BindToRenderSystem(); mainf->AddChild(baseplate); p* baseplateBody = new p(baseplate); baseplateBody->velocity = false; baseplateBody->friction = 0.7f; physicsengine->AddObject(baseplateBody); // ------------------------------------------------------------------------ // Light cubes // ------------------------------------------------------------------------ auto AddLightCube = [&](const char* name, const glm::vec3& position, float size, const glm::vec3& color) -> Mesh* { Mesh* cube = CreateCubeMesh(); cube->name = name; cube->colorOverride = glm::vec4(color, 1.0f); cube->emissive = 1.0f; cube->t.ScaleTo(size); cube->t.TranslateTo(position); cube->BindToRenderSystem(); mainf->AddChild(cube); return cube; }; const glm::vec3 skyLightColor(0.45f, 0.65f, 1.0f); const glm::vec3 skyLightPosition(-6.0f, 7.0f, -6.0f); Mesh* skyLightCube = AddLightCube("spot_light_cube", skyLightPosition, 1.5f, skyLightColor); rendersystem->lighting->AddSpotLight(skyLightPosition, { 0.0f, -1.0f, 0.0f }, skyLightColor, 1200.0f, 25.0f, 18.0f, 26.0f); const glm::vec3 pointLightPosition(6.0f, 3.0f, -6.0f); const glm::vec3 pointLightColor(1.0f, 0.35f, 0.05f); Mesh* pointLightCube = AddLightCube("point_light_cube", pointLightPosition, 0.8f, pointLightColor); rendersystem->lighting->AddPointLight(pointLightPosition, pointLightColor, 300.0f, 30.0f); // ------------------------------------------------------------------------ // Ground check + respawn // ------------------------------------------------------------------------ auto IsGrounded = [&](Character& character) -> bool { Ray downRay; downRay.origin = character.body->t.GetTranslation(); downRay.direction = glm::vec3(0.0f, -1.0f, 0.0f); const float halfHeight = character.body->t.GetScale().y * 0.5f; for (p* obj : physicsengine->GetObjects()) { if (obj->pointer == character.body) continue; if (obj->collisionGroup != 0 && obj->collisionGroup == character.GetCollisionGroup()) continue; // anything else of its own if (!obj->collision) continue; auto hit = IsRayInT(downRay, obj->pointer->t); if (hit.has_value()) { if (glm::distance(downRay.origin, hit.value()) < halfHeight + 0.15f) return true; } } return false; }; std::mt19937 respawnRng(std::random_device{}()); auto RandomSpawn = [&]() -> glm::vec3 { std::uniform_real_distribution spread(-14.0f, 14.0f); return { spread(respawnRng), 15.0f, spread(respawnRng) }; }; auto UpdateHitboxDebug = [&]() { std::vector lines; if (showHitboxes) { for (Character* character : charactermanager->GetCharacters()) { if (!character->body) continue; if (character->ragdoll) { for (Ragdoll::Part* part : character->ragdoll->GetParts()) lines.push_back({ part->transform.t.GetMatrix(), glm::vec3(0.2f, 0.9f, 0.35f) }); continue; } lines.push_back({ character->body->t.GetMatrix(), glm::vec3(0.2f, 0.9f, 0.35f) }); } } rendersystem->SetHitboxDebugLines(lines); }; // ======================================================================== // Game loop // ======================================================================== while (!glfwWindowShouldClose(window->handle)) { frame++; static double prevFrameTime = glfwGetTime(); const double frameNow = glfwGetTime(); const float deltatime = glm::clamp(float(frameNow - prevFrameTime), 0.001f, 1.0f / 60.0f) * (144.0f / float(fps)); prevFrameTime = frameNow; const float refDt = 1.0f / float(fps); static bool prevSwapDown = false; const bool swapDown = keyboard->IsKeyDown(GLFW_KEY_C); if (swapDown && !prevSwapDown && !menuActive) { // The keyboard moves over, the one left behind has no mind and stands still controlled->mind = nullptr; controlled = controlled == &testcharacter ? &npc : &testcharacter; controlled->mind = &keyboardMind; } prevSwapDown = swapDown; static bool prevEscapeDown = false; const bool escapeDown = keyboard->IsKeyDown(GLFW_KEY_ESCAPE); const bool escapePressed = escapeDown && !prevEscapeDown; prevEscapeDown = escapeDown; if (escapePressed) { if (!menuActive) OpenMenu(MenuScreen::Paused); else if (menuScreen == MenuScreen::Settings) menuScreen = settingsReturn; else if (menuScreen == MenuScreen::Graphics) menuScreen = MenuScreen::Settings; else if (menuScreen == MenuScreen::Paused) ResumeGame(); } // -------------------------------------------------------------------- // Mouse look // -------------------------------------------------------------------- int wheelSteps = 0; // up is positive while (Mouse::Event buffer = mouse->Read()) { if (buffer.GetType() == Mouse::Event::Type::WheelUp) wheelSteps++; else if (buffer.GetType() == Mouse::Event::Type::WheelDown) wheelSteps--; if (menuActive || console->IsOpen()) continue; if (buffer.GetType() == Mouse::Event::Type::Move && camera->lockedcursor) { float xoffset = mouse->GetX() - mouse->GetLastX(); float yoffset = mouse->GetLastY() - mouse->GetY(); camera->ProcessMouseMovement(xoffset, yoffset); } } const glm::vec2 mousePos((float)mouse->GetX(), (float)mouse->GetY()); // -------------------------------------------------------------------- // Debug: G throws a cube at the cursor, or straight ahead with the cursor locked // -------------------------------------------------------------------- static bool prevLaunchDown = false; const bool launchDown = keyboard->IsKeyDown(GLFW_KEY_G); if (launchDown && !prevLaunchDown && !menuActive) { const float launchSpeed = 40.0f; const glm::vec2 aim = camera->lockedcursor ? glm::vec2(window->width * 0.5f, window->height * 0.5f) : mousePos; const Ray ray = MouseRay(aim); // The camera sits behind the player and looks over them, so start past them rather than hitting them on the way out const float pastPlayer = glm::dot(controlled->body->t.GetTranslation() - ray.origin, ray.direction) + 1.0f; const glm::vec3 start = ray.origin + ray.direction * std::max(pastPlayer, 1.0f); SpawnDebugCube(start, ray.direction * launchSpeed); } prevLaunchDown = launchDown; // -------------------------------------------------------------------- // Cursor. Locked for looking around, unless Alt has freed it or the console needs the mouse. // The menus handle it themselves. // -------------------------------------------------------------------- static bool mouseFreed = false; static bool prevAltDown = false; const bool altDown = keyboard->IsKeyDown(GLFW_KEY_LEFT_ALT) || keyboard->IsKeyDown(GLFW_KEY_RIGHT_ALT); if (altDown && !prevAltDown && !menuActive) mouseFreed = !mouseFreed; prevAltDown = altDown; if (!menuActive) { const bool wantLocked = !mouseFreed && !console->IsOpen(); if (camera->lockedcursor != wantLocked) { camera->lockedcursor = wantLocked; glfwSetInputMode(window->handle, GLFW_CURSOR, wantLocked ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL); if (wantLocked) { int ww, wh; glfwGetWindowSize(window->handle, &ww, &wh); glfwSetCursorPos(window->handle, ww * 0.5, wh * 0.5); // Otherwise the jump back to the middle reads as one big mouse movement and the camera snaps mouse->OnMouseMove((unsigned int)(ww / 2), (unsigned int)(wh / 2)); mouse->OnMouseMove((unsigned int)(ww / 2), (unsigned int)(wh / 2)); } } } if (console->IsOpen() && wheelSteps != 0) console->Scroll(wheelSteps * 3); console->Update(deltatime); if (menuActive) { ApplyMenuLayout(); const bool liveBackdrop = menuScreen == MenuScreen::Main || ((menuScreen == MenuScreen::Settings || menuScreen == MenuScreen::Graphics) && settingsReturn == MenuScreen::Main); if (liveBackdrop) { static float menuAngle = 0.6f; menuAngle += deltatime * 0.12f; const glm::vec3 menuTarget(0.0f, 1.4f, 0.0f); const glm::vec3 menuCamera = menuTarget + glm::vec3(std::sin(menuAngle) * 7.5f, 2.6f, std::cos(menuAngle) * 7.5f); camera->t.TranslateTo(menuCamera); camera->t.RotateToQuaternion(LookAt(menuTarget - menuCamera)); camera->Step(deltatime); charactermanager->Step(deltatime); } soundsystem->SetListenerPosition( camera->t.GetTranslation(), camera->t.GetFrontVector(), camera->t.GetUpVector() ); soundsystem->Update(); const glm::vec2 mousePos((float)mouse->GetX(), (float)mouse->GetY()); const float hoverEase = 1.0f - std::exp(-12.0f * deltatime); for (MenuButton* button : menuButtons) { t2d_package& t2d = button->box->t2d; t2d.Recalculate(); button->hovered = button->box->Opacity > 0.0f && mousePos.x > t2d.pixelposition.x && mousePos.y > t2d.pixelposition.y && mousePos.x < t2d.pixelposition.x + t2d.pixelsize.x && mousePos.y < t2d.pixelposition.y + t2d.pixelsize.y; button->hover += ((button->hovered ? 1.0f : 0.0f) - button->hover) * hoverEase; button->box->Color = glm::mix(button->color, button->hoverColor, button->hover); button->accent->t2d.size.z = 5.0f * button->hover; button->label->t2d.position.z = button->labelX + button->labelSlide * button->hover; } const bool mouseDown = mouse->IsLeftDown(); if (mouseDown && !prevMouseDown) { if (menuScreen == MenuScreen::Settings) { if (sensitivityDown.hovered) { StartupCmPer360 = glm::clamp(StartupCmPer360 + 2.0f, 4.0f, 80.0f); camera->SetSensitivityCm(StartupCmPer360, StartupMouseDpi); } else if (sensitivityUp.hovered) { StartupCmPer360 = glm::clamp(StartupCmPer360 - 2.0f, 4.0f, 80.0f); camera->SetSensitivityCm(StartupCmPer360, StartupMouseDpi); } else if (fovDown.hovered) { camera->FOV = glm::clamp(camera->FOV - 5.0f, 60.0f, 130.0f); } else if (fovUp.hovered) { camera->FOV = glm::clamp(camera->FOV + 5.0f, 60.0f, 130.0f); } else if (hitboxToggle.hovered) { showHitboxes = !showHitboxes; } else if (graphicsButton.hovered) { menuScreen = MenuScreen::Graphics; } else if (resetButton.hovered) { StartupCmPer360 = 16.0f; StartupMouseDpi = 800.0f; camera->SetSensitivityCm(StartupCmPer360, StartupMouseDpi); camera->FOV = 110.0f; showHitboxes = false; } else if (backButton.hovered) { menuScreen = settingsReturn; } } else if (menuScreen == MenuScreen::Graphics) { GraphicsSettings& graphics = rendersystem->graphics; if (bloomToggle.hovered) { graphics.bloom = !graphics.bloom; } else if (shadowToggle.hovered) { graphics.shadows = !graphics.shadows; } else if (lightShadowToggle.hovered) { graphics.lightShadows = !graphics.lightShadows; } else if (vsyncToggle.hovered) { graphics.vsync = !graphics.vsync; glfwSwapInterval(graphics.vsync ? 1 : 0); } else if (cullingToggle.hovered) { graphics.culling = !graphics.culling; } else if (backButton.hovered) { menuScreen = MenuScreen::Settings; } } else if (playButton.hovered) { ResumeGame(); } else if (settingsButton.hovered) { settingsReturn = menuScreen; menuScreen = MenuScreen::Settings; } else if (quitButton.hovered) { if (menuScreen == MenuScreen::Main) glfwSetWindowShouldClose(window->handle, 1); else menuScreen = MenuScreen::Main; } } prevMouseDown = mouseDown; UpdateHitboxDebug(); rendersystem->Render(*camera); glfwPollEvents(); continue; } // -------------------------------------------------------------------- // Characters: minds decide actions, bodies carry them out // -------------------------------------------------------------------- keyboardMind.enabled = !console->IsOpen(); // typing isn't walking charactermanager->Think(); for (Character* character : charactermanager->GetCharacters()) { if (character->ragdoll) continue; character->body->ApplyMovement(IsGrounded(*character), 1.0f, deltatime, refDt); } charactermanager->Step(deltatime); float physicsAlpha = 0.0f; // how far this frame is between the last tick and the next { // Physics ticks a fixed 30 times a second of real time, whatever the frame rate. // deltatime is real time sped up by timeScale, so a tick covers that much more game time. const float tickRate = 30.0f; const float tickSeconds = 1.0f / tickRate; const float timeScale = 144.0f / float(fps); const float tickDt = tickSeconds * timeScale; const int substepsPerTick = std::max(1, (int)std::ceil(tickDt * 120.0f)); // keeps fast things from passing through const int maxTicksPerFrame = 4; static float physicsAccumulator = 0.0f; physicsAccumulator += deltatime / timeScale; int ticks = 0; while (physicsAccumulator >= tickSeconds && ticks < maxTicksPerFrame) { physicsengine->Tick(tickDt, substepsPerTick); physicsAccumulator -= tickSeconds; ticks++; } // Fell too far behind, a long hitch. Drop the backlog rather than spending frames catching up. if (physicsAccumulator >= tickSeconds) physicsAccumulator = 0.0f; physicsAlpha = physicsAccumulator / tickSeconds; } // Nothing reads the contacts physics records yet, so they're let go every frame physicsengine->ClearImpacts(); // Debug cubes that fell off the world go for (int i = (int)debugCubes.size() - 1; i >= 0; i--) { if (debugCubes[i].mesh->t.GetTranslationY() > -50.0f) continue; physicsengine->RemoveP(debugCubes[i].body); delete debugCubes[i].body; delete debugCubes[i].mesh; // takes itself out of the render system debugCubes.erase(debugCubes.begin() + i); } for (Character* character : charactermanager->GetCharacters()) { p* body = character->body->physics; if (body && body->pointer->t.GetTranslation().y < -20.0f) { body->pointer->t.TranslateTo(RandomSpawn()); body->linearvelocity = glm::vec3(0.0f); body->WakeUp(); } } // Drawn partway to the next tick from here until after Render, so nothing may move bodies in between physicsengine->ApplyInterpolation(physicsAlpha); // -------------------------------------------------------------------- // Third person camera // -------------------------------------------------------------------- { const glm::vec3 playerPos = controlled->body->t.GetTranslation(); const glm::vec3 target = playerPos + glm::vec3(0.0f, 0.8f, 0.0f); // Scroll zooms, unless something on screen is using the wheel. Eases toward the new distance. static float zoomTarget = 5.0f; static float zoomDistance = 5.0f; if (!console->IsOpen() && wheelSteps != 0) zoomTarget = glm::clamp(zoomTarget * std::pow(0.85f, (float)wheelSteps), 1.5f, 15.0f); zoomDistance += (zoomTarget - zoomDistance) * (1.0f - std::exp(-12.0f * deltatime)); glm::vec3 cameraPos = target - camera->t.GetFrontVector() * zoomDistance; const glm::vec3 cameraRay = cameraPos - target; const float desiredDistance = glm::length(cameraRay); const glm::vec3 cameraDir = desiredDistance > 0.0001f ? cameraRay / desiredDistance : glm::vec3(0.0f); Ray cameraSweep; cameraSweep.origin = target; cameraSweep.direction = cameraDir; float nearestHit = desiredDistance; for (p* obj : physicsengine->GetObjects()) { if (obj->pointer == controlled->body || !obj->collision) continue; if (obj->collisionGroup != 0) // flailing ragdoll limbs would make the camera jump continue; auto hit = IsRayInT(cameraSweep, obj->pointer->t); if (!hit.has_value()) continue; const float hitDistance = glm::dot(hit.value() - target, cameraDir); if (hitDistance > 0.001f && hitDistance < nearestHit) nearestHit = hitDistance; } const float cameraPadding = 0.3f; cameraPos = target + cameraDir * glm::max(nearestHit - cameraPadding, 0.05f); camera->t.TranslateTo(cameraPos); } camera->Step(deltatime); Sound* music = static_cast(mainf->GetFirstChildOfName("music")); if (music) music->Update3DPosition(0.0f, 0.0f, 0.0f); soundsystem->SetListenerPosition( camera->t.GetTranslation(), camera->t.GetFrontVector(), camera->t.GetUpVector() ); soundsystem->Update(); // -------------------------------------------------------------------- // FPS counter // -------------------------------------------------------------------- { static double lastTime = glfwGetTime(); static int frameCount = 0; static int measuredFps = 0; static float accumulator = 0.0f; double now = glfwGetTime(); accumulator += (float)(now - lastTime); lastTime = now; frameCount++; if (accumulator >= 0.25f) { measuredFps = (int)(frameCount / accumulator); frameCount = 0; accumulator = 0.0f; } fpsCounter->text = std::to_string(measuredFps) + " FPS"; } // -------------------------------------------------------------------- // Render // -------------------------------------------------------------------- UpdateHitboxDebug(); rendersystem->Render(*camera); physicsengine->RemoveInterpolation(); // back to the real positions before anything moves them glfwPollEvents(); } } // ============================================================================ // GLFW callbacks // ============================================================================ void framebuffer_size_callback(GLFWwindow* window, int w, int h) { if (w <= 0 || h <= 0) return; ::window->width = w; ::window->height = h; glViewport(0, 0, w, h); if (engine && engine->rendersystem) engine->rendersystem->Resize(w, h); } void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { Console* console = engine ? engine->console : nullptr; if (console && key == GLFW_KEY_GRAVE_ACCENT && action == GLFW_PRESS) { console->Toggle(); return; } // While typing, keys belong to the console. Releases still go through, so nothing held stays stuck down. if (console && console->IsOpen()) { if (action == GLFW_PRESS || action == GLFW_REPEAT) console->OnKey(key); else if (action == GLFW_RELEASE) keyboard->KeyUp(key); return; } if (key == GLFW_KEY_F11 && action == GLFW_PRESS) { static int wx = 100, wy = 100, ww = 1920, wh = 1080; if (glfwGetWindowMonitor(window)) { glfwSetWindowMonitor(window, nullptr, wx, wy, ww, wh, 0); } else { glfwGetWindowPos(window, &wx, &wy); glfwGetWindowSize(window, &ww, &wh); GLFWmonitor* monitor = glfwGetPrimaryMonitor(); const GLFWvidmode* mode = glfwGetVideoMode(monitor); glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height, mode->refreshRate); } glfwSwapInterval(engine && engine->rendersystem && !engine->rendersystem->graphics.vsync ? 0 : 1); } if (key == GLFW_KEY_H && action == GLFW_PRESS && engine && engine->rendersystem) engine->rendersystem->CycleDebugView(); if (action == GLFW_PRESS) keyboard->KeyDown(key); else if (action == GLFW_RELEASE) keyboard->KeyUp(key); } void char_callback(GLFWwindow* window, unsigned int codepoint) { if (engine && engine->console && engine->console->IsOpen()) engine->console->OnChar(codepoint); } void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) { // Mouse counts wheel notches in Windows units, 120 to a notch, and carries the remainder for touchpads mouse->OnWheelDelta(mouse->GetX(), mouse->GetY(), (int)std::lround(yoffset * 120.0)); } void mouse_callback(GLFWwindow* window, double xposIn, double yposIn) { mouse->OnMouseMove(static_cast(xposIn), static_cast(yposIn)); if (!engine || !engine->camera || !engine->camera->lockedcursor) return; int ww, wh; glfwGetWindowSize(window, &ww, &wh); const double cx = ww / 2.0; const double cy = wh / 2.0; if (std::abs(xposIn - cx) < 0.5 && std::abs(yposIn - cy) < 0.5) return; glfwSetCursorPos(window, cx, cy); mouse->OnMouseMove((float)cx, (float)cy); } void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) { if (button == GLFW_MOUSE_BUTTON_RIGHT) { if (action == GLFW_PRESS) mouse->RightDown(); else mouse->RightUp(); } else if (button == GLFW_MOUSE_BUTTON_LEFT) { if (action == GLFW_PRESS) mouse->LeftDown(); else mouse->LeftUp(); } }