#include "Engine.h" // ============================================================================ // Standard library // ============================================================================ #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" #include "TexturePainter.h" // Character / animation #include "Rig.h" #include "Animator.h" #include "Character.h" #include "CharacterManager.h" #include "Ragdoll.h" #include "DefaultBodyComponents.h" #include "BodyComponentActions.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); 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"); // ------------------------------------------------------------------------ // Player character // ------------------------------------------------------------------------ Character& testcharacter = *charactermanager->CreateCharacter("character", "humanoid", "walk"); charactermanager->SetControlledCharacter("character"); 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(); p* characterBody = new p(testcharacter.body); characterBody->mass = 80.0f; characterBody->lockRotation = true; characterBody->friction = 0.0f; physicsengine->AddObject(characterBody); // ------------------------------------------------------------------------ // NPC // ------------------------------------------------------------------------ Character& npc = *charactermanager->CreateCharacter("npc", "humanoid", "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(); testcharacter.SetWalking(false); npc.SetWalking(false); npc.EnableRagdoll(*physicsengine); // ------------------------------------------------------------------------ // 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, Drawer }; 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.15f; 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 drawerButton = MakeMenuButton("TEXTURE DRAWER", menuMargin, 12.0f, menuContentWidth, mainButtonHeight, 0.75f, menuButtonColor, menuButtonHover); MenuButton settingsButton = MakeMenuButton("SETTINGS", menuMargin, 84.0f, menuContentWidth, mainButtonHeight, 0.75f, menuButtonColor, menuButtonHover); MenuButton quitButton = MakeMenuButton("QUIT", menuMargin, 156.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); // Texture drawer. Filled in further down, once the things it paints on exist. struct DrawableSurface { const char* name; SurfaceTexture* surface; glm::vec4* colorOverride; bool hasCanvas = false; }; std::vector drawableSurfaces; int selectedSurface = 0; int selectedSwatch = 0; TexturePainter painter; bool painting = false; // The first edit swaps in a private copy, so painting never touches the shared image file. // A colour override would hide the paint, so that goes too. auto EnsureCanvas = [&](DrawableSurface& drawable) { if (drawable.hasCanvas) return; SurfaceTexture& surface = *drawable.surface; surface.texture = surface.texture ? resourcemanager->CopyTexture(*surface.texture) : resourcemanager->CreateTexture(256, 256); drawable.colorOverride->w = 0.0f; drawable.hasCanvas = true; }; const float surfaceRowY = -120.0f; const float swatchRowY = -64.0f; const float brushRowY = -8.0f; MenuButton surfacePrevious = MakeMenuButton("<", menuMargin, surfaceRowY, 44.0f, 44.0f, 0.6f, menuButtonColor, menuButtonHover, 0.6f, true); MenuButton surfaceNext = MakeMenuButton(">", stepperUpX, surfaceRowY, 44.0f, 44.0f, 0.6f, menuButtonColor, menuButtonHover, 0.6f, true); TextBox* surfaceName = MakeMenuText("", menuMargin + 44.0f, surfaceRowY, menuContentWidth - 88.0f, 44.0f, 0.5f, menuText, 0.5f); const glm::vec3 swatchColors[] = { { 0.0f, 0.0f, 0.0f }, { 1.0f, 1.0f, 1.0f }, { 0.90f, 0.15f, 0.15f }, { 0.96f, 0.58f, 0.16f }, { 0.20f, 0.75f, 0.30f }, { 0.20f, 0.45f, 0.95f } }; const float swatchSize = 52.0f; const float swatchSpacing = 60.0f; std::vector swatches; swatches.reserve(std::size(swatchColors)); for (int i = 0; i < (int)std::size(swatchColors); i++) { swatches.push_back(MakeMenuButton("", menuMargin + swatchSpacing * i, swatchRowY, swatchSize, 44.0f, 0.5f, swatchColors[i], glm::mix(swatchColors[i], glm::vec3(1.0f), 0.2f), 1.0f, true)); } Box* swatchMarker = new Box(); swatchMarker->Color = menuAccent; swatchMarker->t2d.center = { 0.0f, 0.5f }; swatchMarker->t2d.size = { 0.0f, 0.0f, swatchSize, 4.0f }; swatchMarker->BindToRenderSystem(); menuItems.push_back(swatchMarker); TextBox* brushLabel = MakeMenuText("BRUSH SIZE", menuMargin, brushRowY, 172.0f, 44.0f, 0.5f, menuMutedText); TextBox* brushValue = MakeMenuText("", stepperValueX, brushRowY, stepperValueWidth, 44.0f, 0.5f, menuText, 0.5f); MenuButton brushDown = MakeMenuButton("-", stepperDownX, brushRowY, 44.0f, 44.0f, 0.7f, menuButtonColor, menuButtonHover, 0.6f, true); MenuButton brushUp = MakeMenuButton("+", stepperUpX, brushRowY, 44.0f, 44.0f, 0.7f, menuButtonColor, menuButtonHover, 0.6f, true); MenuButton clearButton = MakeMenuButton("CLEAR", menuMargin, 52.0f, menuContentWidth, 50.0f, 0.6f, menuButtonColor, menuButtonHover); // The canvas sits in the middle of the space right of the panel const float canvasSize = 640.0f; Box* canvasBorder = new Box(); canvasBorder->Color = menuAccent; canvasBorder->t2d.position = { 0.5f, 0.5f, menuPanelWidth * 0.5f, 0.0f }; canvasBorder->t2d.center = { 0.5f, 0.5f }; canvasBorder->t2d.size = { 0.0f, 0.0f, canvasSize + 8.0f, canvasSize + 8.0f }; canvasBorder->BindToRenderSystem(); menuItems.push_back(canvasBorder); Box* canvasBlank = new Box(); // shows through until the surface has a texture canvasBlank->t2d.position = canvasBorder->t2d.position; canvasBlank->t2d.center = { 0.5f, 0.5f }; canvasBlank->t2d.size = { 0.0f, 0.0f, canvasSize, canvasSize }; canvasBlank->BindToRenderSystem(); menuItems.push_back(canvasBlank); ImageBox* canvasImage = new ImageBox(); canvasImage->t2d.position = canvasBorder->t2d.position; canvasImage->t2d.center = { 0.5f, 0.5f }; canvasImage->t2d.size = { 0.0f, 0.0f, canvasSize, canvasSize }; canvasImage->BindToRenderSystem(); menuItems.push_back(canvasImage); std::vector menuButtons = { &playButton, &drawerButton, &settingsButton, &quitButton, &sensitivityDown, &sensitivityUp, &fovDown, &fovUp, &hitboxToggle, &graphicsButton, &resetButton, &backButton, &bloomToggle, &shadowToggle, &lightShadowToggle, &vsyncToggle, &cullingToggle, &surfacePrevious, &surfaceNext, &brushDown, &brushUp, &clearButton }; for (MenuButton& swatch : swatches) menuButtons.push_back(&swatch); TextBox* menuHint = new TextBox(); menuHint->font = arial; menuHint->fontsize = 0.42f; menuHint->Color = menuMutedText; menuHint->t2d.position = { 0.0f, 1.0f, menuMargin, -28.0f }; menuHint->t2d.center = { 0.0f, 1.0f }; menuHint->t2d.size = { 0.0f, 0.0f, menuContentWidth, 110.0f }; menuHint->textCenter = { 0.0f, 1.0f }; menuHint->BindToRenderSystem(); menuItems.push_back(menuHint); 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 drawer = menuScreen == MenuScreen::Drawer; 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; menuHint->Opacity = 1.0f; menuSubtitle->text = main ? "MAIN MENU" : (paused ? "PAUSED" : (drawer ? "TEXTURE DRAWER" : (graphics ? "GRAPHICS" : "SETTINGS"))); playButton.label->text = paused ? "RESUME" : "PLAY"; quitButton.label->text = paused ? "QUIT TO MENU" : "QUIT"; menuHint->text = drawer ? "DRAG ON THE CANVAS TO DRAW\nESC TO GO BACK" : (settings || graphics ? "ESC TO GO BACK" : "WASD TO MOVE\nESC FOR MENU"); SetButtonVisible(playButton, mainButtons); SetButtonVisible(drawerButton, mainButtons); SetButtonVisible(settingsButton, mainButtons); SetButtonVisible(quitButton, mainButtons); SetButtonVisible(surfacePrevious, drawer); SetButtonVisible(surfaceNext, drawer); SetButtonVisible(brushDown, drawer); SetButtonVisible(brushUp, drawer); SetButtonVisible(clearButton, drawer); for (MenuButton& swatch : swatches) SetButtonVisible(swatch, drawer); Texture* canvasTexture = drawableSurfaces[selectedSurface].surface->texture; canvasImage->surface = SurfaceTexture::Stretched(canvasTexture); canvasImage->Opacity = drawer && canvasTexture ? 1.0f : 0.0f; canvasBlank->Opacity = drawer ? 1.0f : 0.0f; canvasBorder->Opacity = drawer ? 1.0f : 0.0f; surfaceName->Opacity = drawer ? 1.0f : 0.0f; surfaceName->text = drawableSurfaces[selectedSurface].name; brushLabel->Opacity = drawer ? 1.0f : 0.0f; brushValue->Opacity = drawer ? 1.0f : 0.0f; brushValue->text = std::to_string(painter.brushSize); swatchMarker->Opacity = drawer ? 1.0f : 0.0f; swatchMarker->t2d.position = { 0.0f, 0.5f, menuMargin + swatchSpacing * selectedSwatch, swatchRowY + 28.0f }; SetButtonVisible(sensitivityDown, settings); SetButtonVisible(sensitivityUp, settings); SetButtonVisible(fovDown, settings); SetButtonVisible(fovUp, settings); SetButtonVisible(hitboxToggle, settings); SetButtonVisible(graphicsButton, settings); SetButtonVisible(resetButton, settings); SetButtonVisible(backButton, settings || drawer || 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; charactermanager->ClearInput(); testcharacter.SetWalking(false); npc.SetWalking(false); fpsCounter->Opacity = 0.0f; camera->lockedcursor = false; glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL); prevMouseDown = mouse->IsLeftDown(); }; auto ResumeGame = [&]() { menuActive = false; charactermanager->ClearInput(); 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 // ------------------------------------------------------------------------ Mesh* floor = CreateCubeMesh(); floor->name = "baseplate"; floor->surface = SurfaceTexture::Tiled(blanktex, 8.0f); floor->roughness = 0.9f; floor->t.ScaleTo({ 200.0f, 2.0f, 200.0f }); floor->t.TranslateTo({ 0.0f, -1.0f, 0.0f }); floor->BindToRenderSystem(); mainf->AddChild(floor); p* floorBody = new p(floor); floorBody->velocity = false; physicsengine->AddObject(floorBody); const BoundingBox floorBounds = floor->t.GetRotationlessAABB(); // ------------------------------------------------------------------------ // 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); drawableSurfaces.push_back({ "BASEPLATE", &floor->surface, &floor->colorOverride }); drawableSurfaces.push_back({ "PLAYER", &testcharacter.surface, &testcharacter.colorOverride }); drawableSurfaces.push_back({ "DUMMY", &npc.surface, &npc.colorOverride }); drawableSurfaces.push_back({ "SKY LIGHT CUBE", &skyLightCube->surface, &skyLightCube->colorOverride }); drawableSurfaces.push_back({ "POINT LIGHT CUBE", &pointLightCube->surface, &pointLightCube->colorOverride }); // ------------------------------------------------------------------------ // Ground check + respawn // ------------------------------------------------------------------------ auto IsGrounded = [&]() -> bool { Ray downRay; downRay.origin = testcharacter.body->t.GetTranslation(); downRay.direction = glm::vec3(0.0f, -1.0f, 0.0f); const float halfHeight = testcharacter.body->t.GetScale().y * 0.5f; for (p* obj : physicsengine->GetObjects()) { if (obj->pointer == testcharacter.body) continue; 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 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 == MenuScreen::Drawer) menuScreen = settingsReturn; else if (menuScreen == MenuScreen::Graphics) menuScreen = MenuScreen::Settings; else if (menuScreen == MenuScreen::Paused) ResumeGame(); } // -------------------------------------------------------------------- // Mouse look // -------------------------------------------------------------------- while (Mouse::Event buffer = mouse->Read()) { if (menuActive) 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); } else if (buffer.GetType() == Mouse::Event::Type::RPress) { camera->lockedcursor = !camera->lockedcursor; if (camera->lockedcursor) { 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); } else { glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL); } } } if (menuActive) { ApplyMenuLayout(); const bool liveBackdrop = menuScreen == MenuScreen::Main || ((menuScreen == MenuScreen::Settings || menuScreen == MenuScreen::Graphics || menuScreen == MenuScreen::Drawer) && 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 (menuScreen == MenuScreen::Drawer) { const int surfaceCount = (int)drawableSurfaces.size(); if (surfacePrevious.hovered) { selectedSurface = (selectedSurface + surfaceCount - 1) % surfaceCount; } else if (surfaceNext.hovered) { selectedSurface = (selectedSurface + 1) % surfaceCount; } else if (brushDown.hovered) { painter.brushSize = glm::max(painter.brushSize - 2, 2); } else if (brushUp.hovered) { painter.brushSize = glm::min(painter.brushSize + 2, 64); } else if (clearButton.hovered) { DrawableSurface& drawable = drawableSurfaces[selectedSurface]; EnsureCanvas(drawable); painter.Clear(*drawable.surface->texture); } else if (backButton.hovered) { menuScreen = settingsReturn; } for (int i = 0; i < (int)swatches.size(); i++) { if (swatches[i].hovered) { selectedSwatch = i; painter.color = swatchColors[i]; } } } else if (playButton.hovered) { ResumeGame(); } else if (drawerButton.hovered) { settingsReturn = menuScreen; menuScreen = MenuScreen::Drawer; } 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; } } // A stroke has to start on the canvas, but carries on if the mouse wanders off it canvasImage->t2d.Recalculate(); const glm::vec2 canvasUV = (mousePos - canvasImage->t2d.pixelposition) / canvasImage->t2d.pixelsize; if (!mouseDown || menuScreen != MenuScreen::Drawer) { painting = false; } else if (!prevMouseDown) { painting = canvasUV.x >= 0.0f && canvasUV.x <= 1.0f && canvasUV.y >= 0.0f && canvasUV.y <= 1.0f; } if (painting) { DrawableSurface& drawable = drawableSurfaces[selectedSurface]; EnsureCanvas(drawable); painter.Stroke(*drawable.surface->texture, glm::clamp(canvasUV, glm::vec2(0.0f), glm::vec2(1.0f))); } else { painter.EndStroke(); } prevMouseDown = mouseDown; UpdateHitboxDebug(); rendersystem->Render(*camera); glfwPollEvents(); continue; } const glm::vec3 camFront = camera->t.GetFrontVector(); const glm::vec3 camRight = camera->t.GetRightVector(); const float movespeed = 3.0f * 1.3f * 0.6f; // -------------------------------------------------------------------- // WASD movement // -------------------------------------------------------------------- const glm::vec3 inputDirection = testcharacter.GetMovementDirection(camFront, camRight); const bool hasMoveInput = glm::length(inputDirection) > 0.0f; if (hasMoveInput) testcharacter.SetTurnDirection(inputDirection); testcharacter.SetWalking(hasMoveInput); charactermanager->Step(deltatime); const glm::vec3 inputVel = hasMoveInput ? testcharacter.GetForwardDirection() * movespeed : glm::vec3(0.0f); const float accel = 40.0f; const float damp = 0.75f; const float airControl = 0.5f; const float groundTopSpeed = movespeed * accel * refDt * damp / (1.0f - damp); if (IsGrounded()) { const float dampStep = std::pow(damp, deltatime / refDt); const float accelScale = refDt * damp * (1.0f - dampStep) / (deltatime * dampStep * (1.0f - damp)); characterBody->linearvelocity.x *= dampStep; characterBody->linearvelocity.z *= dampStep; characterBody->linearvelocity.x += inputVel.x * accel * deltatime * accelScale; characterBody->linearvelocity.z += inputVel.z * accel * deltatime * accelScale; } else if (glm::length(inputVel) > 0.0f) { const glm::vec3 wishDir = glm::normalize(inputVel); const float current = glm::dot(glm::vec3(characterBody->linearvelocity.x, 0.0f, characterBody->linearvelocity.z), wishDir); const float add = glm::clamp(groundTopSpeed - current, 0.0f, accel * airControl * movespeed * deltatime); characterBody->linearvelocity.x += wishDir.x * add; characterBody->linearvelocity.z += wishDir.z * add; } { const int substeps = std::max(1, (int)std::ceil(deltatime * 120.0f)); const float subDt = deltatime / float(substeps); for (int i = 0; i < substeps; i++) physicsengine->Step(subDt); } npc.ragdoll->Update(deltatime); if (testcharacter.body->t.GetTranslation().y < -20.0f) { testcharacter.body->t.TranslateTo(RandomSpawn()); characterBody->linearvelocity = glm::vec3(0.0f); characterBody->WakeUp(); } // -------------------------------------------------------------------- // Third person camera // -------------------------------------------------------------------- { const glm::vec3 playerPos = testcharacter.body->t.GetTranslation(); const glm::vec3 target = playerPos + glm::vec3(0.0f, 0.8f, 0.0f); glm::vec3 cameraPos = target - camera->t.GetFrontVector() * 5.0f; const bool playerOffFloor = playerPos.x < floorBounds.min.x || playerPos.x > floorBounds.max.x || playerPos.z < floorBounds.min.z || playerPos.z > floorBounds.max.z; const bool ignoreFloorCollision = playerOffFloor || playerPos.y < floorBounds.max.y; 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 == testcharacter.body || !obj->collision) continue; if (obj->collisionGroup != 0) // flailing ragdoll limbs would make the camera jump continue; if (obj == floorBody && ignoreFloorCollision) 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); 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) { 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); if (key >= GLFW_KEY_A && key <= GLFW_KEY_Z && engine && engine->charactermanager) { const char letter = (char)('A' + (key - GLFW_KEY_A)); if (action == GLFW_PRESS) engine->charactermanager->PressInput(letter); else if (action == GLFW_RELEASE) engine->charactermanager->ReleaseInput(letter); } } void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) { } 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(); } }