Files
dt/Engine.cpp
T
2026-11-03 20:05:02 -05:00

1082 lines
47 KiB
C++

#include "Engine.h"
// ============================================================================
// Standard library
// ============================================================================
#include <iostream>
#include <vector>
#include <random>
#include <string>
#include <memory>
#include <type_traits>
#include <algorithm>
#include <cfloat>
#include <cmath>
// ============================================================================
// OpenGL
// ============================================================================
#include <glad/glad.h>
#include <GLFW/glfw3.h>
// ============================================================================
// Math
// ============================================================================
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/quaternion.hpp>
// ============================================================================
// Third party
// ============================================================================
#include <ft2build.h>
#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 <iomanip>
#include <sstream>
#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<DebugCube> 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 <x> <y> <z>", "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 <x> <y> <z>");
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<Box*> 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<MenuButton*> 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<float> spread(-14.0f, 14.0f);
return { spread(respawnRng), 15.0f, spread(respawnRng) };
};
auto UpdateHitboxDebug = [&]() {
std::vector<HitboxDebugLine> 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<Sound*>(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<float>(xposIn), static_cast<float>(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();
}
}