#include "Engine.h" // ============================================================================ // Standard library // ============================================================================ #include #include #include #include #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" #include "Hud.h" #include "Hitbox.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(); hitboxsystem = new HitboxSystem(); 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("fiststance", "assets/fiststance.glb", "humanoid"); charactermanager->LoadAnimation("fistm1c1", "assets/fistm1c1.glb", "humanoid"); charactermanager->LoadAnimation("fistm1c2", "assets/fistm1c2.glb", "humanoid"); charactermanager->LoadAnimation("fistm1c3", "assets/fistm1c3.glb", "humanoid"); charactermanager->LoadAnimation("fistm1c4", "assets/fistm1c4.glb", "humanoid"); charactermanager->LoadAnimation("testsword", "assets/testswordanim.glb", "humanoid"); // brings the R_Weapon bone with it charactermanager->LoadAnimation("flinch1", "assets/flinch1.glb", "humanoid"); // ------------------------------------------------------------------------ // Character templates, the kinds of fighter there are // ------------------------------------------------------------------------ // Test: fights with its fists, a four punch left click combo, the last one sending them flying CharacterTemplate& testTemplate = *charactermanager->AddTemplate("test"); testTemplate.displayName = "Test"; testTemplate.rig = charactermanager->GetRig("humanoid"); testTemplate.idleAnimation = charactermanager->GetAnimation("fiststance"); testTemplate.walkAnimation = charactermanager->GetAnimation("walk"); testTemplate.flinchAnimation = charactermanager->GetAnimation("flinch1"); testTemplate.comboName = "Punch"; { const char* animations[4] = { "fistm1c1", "fistm1c2", "fistm1c3", "fistm1c4" }; for (int i = 0; i < 4; i++) { AttackMove punch; punch.animation = charactermanager->GetAnimation(animations[i]); punch.duration = 0.5f; // seconds, the animation's sped up to fit punch.lastFrame = 120.0f; // 1 to 60 the punch, 60 to 120 back to normal punch.hitStartFrame = 40.0f; punch.hitEndFrame = 60.0f; punch.recoveryFrame = 60.0f; punch.damage = 8.0f; punch.knockback = 2.0f; punch.stun = 0.4f; // just long enough that the next punch lands before they can answer testTemplate.combo.push_back(punch); } testTemplate.combo.back().damage = 14.0f; testTemplate.combo.back().knockback = 10.0f; testTemplate.combo.back().knockbackUp = 5.0f; testTemplate.combo.back().stun = 0.9f; } // Rifleman: just a dude with a rifle on his weapon bone. No attacks yet. // Falls back on the fist stance until rifleidle.glb is in assets, and holds nothing until rifle.glb is. CharacterTemplate& riflemanTemplate = *charactermanager->AddTemplate("rifleman"); riflemanTemplate.displayName = "Rifleman"; riflemanTemplate.rig = charactermanager->GetRig("humanoid"); riflemanTemplate.idleAnimation = std::filesystem::exists("assets/rifleidle.glb") ? charactermanager->LoadAnimation("rifleidle", "assets/rifleidle.glb", "humanoid") // brings the R_Weapon bone with it : charactermanager->GetAnimation("fiststance"); riflemanTemplate.walkAnimation = std::filesystem::exists("assets/riflewalk.glb") ? charactermanager->LoadAnimation("riflewalk", "assets/riflewalk.glb", "humanoid") // walks without letting go of the rifle : charactermanager->GetAnimation("walk"); riflemanTemplate.flinchAnimation = charactermanager->GetAnimation("flinch1"); if (std::filesystem::exists("assets/riflejump.glb")) riflemanTemplate.jumpAnimation = charactermanager->LoadAnimation("riflejump", "assets/riflejump.glb", "humanoid"); riflemanTemplate.jumpDelay = 10.0f / 60.0f; // leaves the ground on frame 10, at 60 fps if (std::filesystem::exists("assets/riflefrontdash.glb")) riflemanTemplate.dashAnimation = charactermanager->LoadAnimation("riflefrontdash", "assets/riflefrontdash.glb", "humanoid"); riflemanTemplate.dashDelay = 0.0f; // moves from the first frame to frame 20, at 60 fps, free after that riflemanTemplate.dashTime = 20.0f / 60.0f; riflemanTemplate.dashDistance = 5.0f; { // 1: a single shot from the hip, hitting the first thing under the crosshair Ability shoot; shoot.name = "Shoot"; shoot.description = "Plant your feet and fire a single shot straight ahead. Turn with the camera to aim before it goes off. Stuns what it hits."; shoot.effect = Ability::Effect::Shot; shoot.animation = charactermanager->LoadAnimation("rifleshoot", "assets/placeholdershootanim.glb", "humanoid"); shoot.lastFrame = 40.0f; // 0.67 s at 60 fps, played at its own speed shoot.fireFrame = 30.0f; shoot.cooldown = 1.5f; // real seconds from the end of one shot to the next shoot.moveSpeedMultiplier = 0.0f; // plants its feet shoot.pivots = true; // but turns with the camera, to aim before it fires shoot.damage = 15.0f; shoot.range = 100.0f; shoot.knockback = 1.5f; shoot.stun = 0.8f; shoot.sound = "assets/gunshot.mp3"; shoot.screenshake = 0.45f; // a punch landing is 0.3, the combo finisher 0.6 if (std::filesystem::exists("assets/rifleexecutionhit.glb")) // whoever it kills goes down like this, filmed by its Camera bone shoot.killAnimation = charactermanager->LoadAnimation("rifleexecutionhit", "assets/rifleexecutionhit.glb", "humanoid"); shoot.killSounds = { { 40.0f, "assets/dramaticwoosh.mp3" }, { 60.0f, "assets/glassbreak.mp3" } }; for (const Ability::KillSound& sound : shoot.killSounds) soundsystem->LoadSound(sound.file, false); // heard as they are, not from anywhere in the world soundsystem->SetDistances(soundsystem->LoadSound(shoot.sound, true), 6.0f, 200.0f); // loud, heard across the map riflemanTemplate.abilities.push_back(shoot); // 2: a switchblade out of nowhere into whoever's in front. It stays in them a while, bleeding them. Ability stab; stab.name = "Stab"; stab.description = "Pull a switchblade and stab whoever is right in front of you. The knife stays in them, making them bleed. Stuns what it hits."; stab.effect = Ability::Effect::Stab; stab.animation = charactermanager->LoadAnimation("riflestab", "assets/riflestab.glb", "humanoid"); // brings the Knife bone stab.lastFrame = 60.0f; // 1 s at 60 fps, played at its own speed stab.fireFrame = 40.0f; stab.hitExitFrame = 50.0f; // landing it, it's over 10 frames early stab.cooldown = 3.0f; stab.moveSpeedMultiplier = 0.0f; stab.lungeDistance = 1.5f; // steps in over the first 15 frames stab.lungeStartFrame = 0.0f; stab.lungeEndFrame = 15.0f; stab.damage = 10.0f; stab.range = 1.1f; // meters out in front stab.hitboxSize = glm::vec2(0.8f, 1.2f); stab.knockback = 0.5f; stab.stun = 0.8f; stab.prop = "assets/switchblade.glb"; stab.propBone = "Knife"; stab.propTurn = glm::vec3(90.0f, 0.0f, 0.0f); // modelled along Blender +Y, like the other weapons stab.bleedTime = 4.0f; stab.bleedDamage = 4.0f; riflemanTemplate.abilities.push_back(stab); // 3: steps in swinging the rifle round through everyone in front, and a moment later they're pinned down heavy Ability sweep; sweep.name = "Sweep"; sweep.description = "Step in and swing the rifle through everyone in front of you. A moment later, whoever it hit is weighed down by heavy gravity: slowed and unable to jump."; sweep.effect = Ability::Effect::Sweep; if (std::filesystem::exists("assets/riflesweep.glb")) sweep.animation = charactermanager->LoadAnimation("riflesweep", "assets/riflesweep.glb", "humanoid"); else sweep.duration = 2.0f; // no animation yet, so it just takes this long sweep.lastFrame = 0.0f; // frames counted at 60 a second, whatever length the animation is sweep.pivots = true; // aim it during the wind up, sweep.pivotEndFrame = 35.0f; // locked in once it hits sweep.lungeDistance = 3.5f; // stepping in over frames 0 to 20 sweep.lungeStartFrame = 0.0f; sweep.lungeEndFrame = 20.0f; sweep.fireFrame = 35.0f; sweep.missExitFrame = 60.0f; // whiffing it, it's over here rather than carrying on to pin anyone down sweep.cooldown = 6.0f; sweep.moveSpeedMultiplier = 0.0f; sweep.damage = 12.0f; sweep.range = 1.8f; // meters out in front sweep.hitboxSize = glm::vec2(3.2f, 1.4f); // wide, to catch more than one sweep.knockback = 2.0f; sweep.stun = 0.8f; sweep.knockdownTime = 2.0f; // takes their legs out sweep.screenshake = 0.3f; sweep.appliesStatus = true; sweep.status = StatusEffect::Type::HeavyGravity; sweep.statusFrame = 110.0f; sweep.statusTime = 5.0f; riflemanTemplate.abilities.push_back(sweep); } if (std::filesystem::exists("assets/rifle.glb")) { riflemanTemplate.weapon.mesh = resourcemanager->GetMeshData("assets/rifle.glb"); riflemanTemplate.weapon.texture = resourcemanager->GetModelTexture("assets/rifle.glb"); glm::mat4 muzzle; riflemanTemplate.weapon.hasMuzzle = resourcemanager->GetModelMarker("assets/rifle.glb", "Muzzle", muzzle); riflemanTemplate.weapon.muzzle = glm::vec3(muzzle[3]); // Modelled along Blender +Y like the greatsword, and bones run along their own +Y. See weaponturn. riflemanTemplate.weapon.turn = glm::vec3(90.0f, 0.0f, 0.0f); riflemanTemplate.weapon.metallic = 0.5f; riflemanTemplate.weapon.roughness = 0.55f; } // Dummy: stands there in the plain idle and takes hits. Not one you can pick. CharacterTemplate& dummyTemplate = *charactermanager->AddTemplate("dummy"); dummyTemplate.displayName = "Dummy"; dummyTemplate.selectable = false; dummyTemplate.healsOnDeath = true; // straight back to full, so it can take hits forever dummyTemplate.rig = charactermanager->GetRig("humanoid"); dummyTemplate.idleAnimation = charactermanager->LoadAnimation("idle", "assets/idle.glb", "humanoid"); dummyTemplate.flinchAnimation = charactermanager->GetAnimation("flinch1"); // Until they get left click moves of their own, the rest punch like Test. Everyone falls over the same way. Animation* knockdownAnimation = std::filesystem::exists("assets/knockdown.glb") ? charactermanager->LoadAnimation("knockdown", "assets/knockdown.glb", "humanoid") : nullptr; for (CharacterTemplate* other : charactermanager->GetTemplates()) { if (!other->knockdownAnimation) other->knockdownAnimation = knockdownAnimation; if (!other->combo.empty()) continue; other->combo = testTemplate.combo; other->comboName = testTemplate.comboName; } // ------------------------------------------------------------------------ // Player character // ------------------------------------------------------------------------ Character& testcharacter = *charactermanager->CreateCharacterFromTemplate("character", "test"); testcharacter.displayName = "Player"; 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.BindToRenderSystem(); testcharacter.CreatePhysicsBody(*physicsengine, 80.0f); // ------------------------------------------------------------------------ // NPC // ------------------------------------------------------------------------ Character& npc = *charactermanager->CreateCharacterFromTemplate("npc", "test"); 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); // ------------------------------------------------------------------------ // Enemy: a rifleman with a mind of its own, after the player. Its mind is made once the map's size is known. // ------------------------------------------------------------------------ Character& enemy = *charactermanager->CreateCharacterFromTemplate("enemy", "rifleman"); enemy.displayName = "Rifle Guy"; enemy.castshadow = true; enemy.colorOverride = glm::vec4(0.48f, 0.12f, 0.10f, 1.0f); enemy.roughness = 0.6f; enemy.body->t.ScaleBy({ 0.7f, 2.5f, 0.7f }); enemy.body->t.TranslateTo({ 10.0f, 1.25f, -10.0f }); enemy.BindToRenderSystem(); enemy.CreatePhysicsBody(*physicsengine, 80.0f); // ------------------------------------------------------------------------ // Training dummy, stands still and talks. Right click it. // ------------------------------------------------------------------------ Character& dummy = *charactermanager->CreateCharacterFromTemplate("dummy", "dummy"); dummy.displayName = "Training Dummy"; dummy.castshadow = true; dummy.colorOverride = glm::vec4(0.72f, 0.60f, 0.38f, 1.0f); dummy.roughness = 0.9f; dummy.body->t.ScaleBy({ 0.7f, 2.5f, 0.7f }); dummy.body->t.TranslateTo({ -3.0f, 1.25f, 4.0f }); dummy.BindToRenderSystem(); dummy.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. 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; }; 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->minDistance = 40.0f; // in the middle of the map but filling it, only fading off well past the edges music->maxDistance = 150.0f; 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(); // Made before the menus so those cover it Hud* hud = new Hud(arial, resourcemanager->GetTexture("assets/placeholderhealthbar.png"), resourcemanager->GetTexture("assets/placeholderhealthbarframe.png")); // ------------------------------------------------------------------------ // Health // ------------------------------------------------------------------------ auto Hurt = [&](Character& character, float amount) { return character.TakeDamage(amount); }; // Something landing on target: the damage, a flinch and a shove. Knocked down, the ragdoll gets the shove. // The camera shakes by shake when it was the player's. auto LandHit = [&](const Character* attacker, Character& target, float damage, const glm::vec3& shove, float shake, float stun) { Hurt(target, damage); target.Flinch(); // nothing if that was the hit that put them down target.Stun(stun); if (target.ragdoll) { for (Ragdoll::Part* part : target.ragdoll->GetParts()) { part->body->linearvelocity += shove; part->body->frozen = false; } } else if (p* body = target.body->physics) { body->linearvelocity += shove; body->WakeUp(); } if (attacker == controlled) camera->AddScreenshake(shake); }; // By name, not case sensitive. No name is whoever you're controlling. auto FindCharacter = [&](const Console::Arguments& args, size_t index) -> Character* { if (args.size() <= index) return controlled; std::string wanted = args[index]; std::transform(wanted.begin(), wanted.end(), wanted.begin(), [](unsigned char c) { return (char)std::tolower(c); }); for (Character* character : charactermanager->GetCharacters()) { std::string name = character->name; std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return (char)std::tolower(c); }); if (name == wanted) return character; } console->Print("Nobody called " + args[index]); return nullptr; }; auto DescribeHealth = [](const Character& character) { if (character.IsDead()) return character.GetDisplayName() + " is down"; return character.GetDisplayName() + " has " + std::to_string((int)std::ceil(character.GetHealth())) + " / " + std::to_string((int)std::ceil(character.maxHealth)) + " health"; }; console->Register("health", "health", "shows everyone's health", [&](const Console::Arguments&) { for (Character* character : charactermanager->GetCharacters()) console->Print(" " + character->name + ": " + DescribeHealth(*character)); }); console->Register("damage", "damage [character]", "hurts you, or someone else", [&](const Console::Arguments& args) { float amount = 0.0f; if (args.empty() || !ParseFloat(args[0], amount) || amount < 0.0f) { console->Print("Usage: damage [character]"); return; } if (Character* character = FindCharacter(args, 1)) { Hurt(*character, amount); console->Print(DescribeHealth(*character)); } }); console->Register("heal", "heal [amount] [character]", "heals you, or someone else, fully without an amount", [&](const Console::Arguments& args) { float amount = FLT_MAX; if (!args.empty() && (!ParseFloat(args[0], amount) || amount < 0.0f)) { console->Print("Usage: heal [amount] [character]"); return; } if (Character* character = FindCharacter(args, 1)) { if (character->IsDead()) console->Print(character->GetDisplayName() + " is down, revive them first"); else character->Heal(amount); console->Print(DescribeHealth(*character)); } }); console->Register("kill", "kill [character]", "drops you, or someone else", [&](const Console::Arguments& args) { if (Character* character = FindCharacter(args, 0)) { character->Die(); console->Print(DescribeHealth(*character)); } }); console->Register("revive", "revive [character]", "gets someone back up where they fell, at full health", [&](const Console::Arguments& args) { if (Character* character = FindCharacter(args, 0)) { character->Revive(); console->Print(DescribeHealth(*character)); } }); 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, Characters }; MenuScreen menuScreen = MenuScreen::Main; MenuScreen settingsReturn = MenuScreen::Main; MenuScreen charactersReturn = 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, -96.0f, menuContentWidth, mainButtonHeight, 0.75f, glm::vec3(0.82f, 0.45f, 0.10f), menuAccent, 0.95f); MenuButton charactersButton = MakeMenuButton("CHARACTER", menuMargin, -24.0f, menuContentWidth, mainButtonHeight, 0.75f, menuButtonColor, menuButtonHover); MenuButton settingsButton = MakeMenuButton("SETTINGS", menuMargin, 48.0f, menuContentWidth, mainButtonHeight, 0.75f, menuButtonColor, menuButtonHover); MenuButton quitButton = MakeMenuButton("QUIT", menuMargin, 120.0f, menuContentWidth, mainButtonHeight, 0.75f, menuButtonColor, glm::vec3(0.42f, 0.14f, 0.16f)); // Character select: a button for every template you can pick, the player's own lit up std::vector fighters; for (CharacterTemplate* candidate : charactermanager->GetTemplates()) if (candidate->selectable) fighters.push_back(candidate); std::vector fighterButtons; fighterButtons.reserve(fighters.size()); for (size_t i = 0; i < fighters.size(); i++) { std::string title = fighters[i]->displayName.empty() ? fighters[i]->name : fighters[i]->displayName; std::transform(title.begin(), title.end(), title.begin(), [](unsigned char c) { return (char)std::toupper(c); }); fighterButtons.push_back(MakeMenuButton(title.c_str(), menuMargin, -128.0f + 56.0f * (float)i, menuContentWidth, 48.0f, 0.6f, menuButtonColor, menuButtonHover)); } // 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, &charactersButton, &settingsButton, &quitButton, &sensitivityDown, &sensitivityUp, &fovDown, &fovUp, &hitboxToggle, &graphicsButton, &resetButton, &backButton, &bloomToggle, &shadowToggle, &lightShadowToggle, &vsyncToggle, &cullingToggle }; for (MenuButton& button : fighterButtons) menuButtons.push_back(&button); 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 characters = menuScreen == MenuScreen::Characters; 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" : (characters ? "CHARACTER SELECT" : "SETTINGS"))); playButton.label->text = paused ? "RESUME" : "PLAY"; quitButton.label->text = paused ? "QUIT TO MENU" : "QUIT"; SetButtonVisible(playButton, mainButtons); SetButtonVisible(charactersButton, 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 || characters); for (size_t i = 0; i < fighterButtons.size(); i++) { SetButtonVisible(fighterButtons[i], characters); fighterButtons[i].color = controlled->characterTemplate == fighters[i] ? glm::vec3(0.82f, 0.45f, 0.10f) : menuButtonColor; } 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; hud->Update(0.0f, nullptr); 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); // ------------------------------------------------------------------------ // Map, a slab whose top is the floor at y = 0, reaching mapHalfSize out from the middle each way. // It's flat, so its box is all the collision it needs. // ------------------------------------------------------------------------ const std::string mapPath = "assets/map1.glb"; const float mapScale = 2.0f; // across the ground, the file's 16 by 16 made bigger const float mapHalfSize = 16.0f * mapScale; Mesh* map = nullptr; if (const MeshData* mapData = resourcemanager->GetMeshData(mapPath)) { map = new Mesh(mapData->vertices, mapData->indices); // a fresh mesh keeps where the file put it map->surface = SurfaceTexture::Stretched(resourcemanager->GetModelTexture(mapPath)); // Spread out across x and z only, so its top stays the floor at y = 0 const glm::vec3 center = map->t.GetTranslation(); const glm::vec3 size = map->t.GetScale(); map->t.ScaleTo({ size.x * mapScale, size.y, size.z * mapScale }); map->t.TranslateTo({ center.x * mapScale, center.y, center.z * mapScale }); } else { // Couldn't load it, a plain slab the same size stands in map = CreateCubeMesh(); map->t.ScaleTo({ mapHalfSize * 2.0f, 2.0f, mapHalfSize * 2.0f }); map->t.TranslateTo({ 0.0f, -1.0f, 0.0f }); map->surface = SurfaceTexture::Tiled(blanktex, 8.0f); } map->name = "map"; map->t.RotateToQuaternion(glm::quat(1.0f, 0.0f, 0.0f, 0.0f)); map->BindToRenderSystem(); mainf->AddChild(map); p* mapBody = new p(map); mapBody->velocity = false; mapBody->friction = 0.7f; physicsengine->AddObject(mapBody); RiflemanMind enemyMind(&testcharacter, mapHalfSize); enemy.mind = &enemyMind; // ------------------------------------------------------------------------ // Whatever each character's template has it carry, on the bone it's carried by. One mesh per character and // template, made the first time it's needed and kept, so switching back and forth doesn't load it again. struct HeldWeapon { Mesh* mesh = nullptr; glm::vec3 center = glm::vec3(0.0f); // its box around its own origin, which goes on the bone glm::vec3 size = glm::vec3(1.0f); bool shown = false; }; std::map, HeldWeapon> heldWeapons; // Takes the held model's own space to the world this frame: the bone, turned by the weapon's turn. // False if it isn't holding anything there right now. auto GetWeaponGrip = [&](const Character& character, glm::mat4& grip) { const CharacterTemplate* from = character.characterTemplate; glm::mat4 bone; if (!from || !character.GetBoneTransform(from->weapon.bone, bone)) return false; grip = bone * glm::mat4_cast(glm::quat(glm::radians(from->weapon.turn))); return true; }; auto UpdateHeldWeapons = [&]() { for (Character* character : charactermanager->GetCharacters()) { const CharacterTemplate* from = character->characterTemplate; if (!from || !from->weapon.mesh || heldWeapons.count({ character, from })) continue; HeldWeapon held; held.mesh = new Mesh(from->weapon.mesh->vertices, from->weapon.mesh->indices); held.mesh->name = character->name + "_" + from->name + "_weapon"; held.center = held.mesh->t.GetTranslation(); // a fresh mesh's box is where the file had it held.size = held.mesh->t.GetScale(); held.mesh->surface = SurfaceTexture::Stretched(from->weapon.texture); if (!from->weapon.texture) held.mesh->colorOverride = glm::vec4(0.22f, 0.22f, 0.24f, 1.0f); held.mesh->metallic = from->weapon.metallic; held.mesh->roughness = from->weapon.roughness; held.mesh->castshadow = true; heldWeapons[{ character, from }] = held; } for (auto& [key, held] : heldWeapons) { Character* character = key.first; const CharacterTemplate* from = key.second; glm::mat4 grip; const bool holding = character->characterTemplate == from && GetWeaponGrip(*character, grip); if (holding) { const glm::vec3 scale(glm::length(glm::vec3(grip[0])), glm::length(glm::vec3(grip[1])), glm::length(glm::vec3(grip[2]))); const glm::mat3 rotation(glm::vec3(grip[0]) / scale.x, glm::vec3(grip[1]) / scale.y, glm::vec3(grip[2]) / scale.z); held.mesh->t.TranslateTo(glm::vec3(grip * glm::vec4(held.center, 1.0f))); held.mesh->t.RotateToQuaternion(glm::normalize(glm::quat_cast(rotation))); held.mesh->t.ScaleTo(held.size * scale); } if (holding != held.shown) { if (holding) held.mesh->BindToRenderSystem(); else rendersystem->RemoveRenderable(held.mesh); held.shown = holding; } } }; console->Register("weaponturn", "weaponturn [x y z]", "shows or turns what your character holds on its bone, in degrees", [&](const Console::Arguments& args) { CharacterTemplate* from = controlled->characterTemplate ? charactermanager->GetTemplate(controlled->characterTemplate->name) : nullptr; if (!from || !from->weapon.mesh) { console->Print("Your character isn't holding anything"); return; } glm::vec3 turn; if (args.size() >= 3) { if (!ParseFloat(args[0], turn.x) || !ParseFloat(args[1], turn.y) || !ParseFloat(args[2], turn.z)) { console->Print("Usage: weaponturn [x y z]"); return; } from->weapon.turn = turn; } std::ostringstream text; text << std::fixed << std::setprecision(1) << "Weapon turned " << from->weapon.turn.x << " " << from->weapon.turn.y << " " << from->weapon.turn.z; console->Print(text.str()); }); // ------------------------------------------------------------------------ // Shots: a hitbox from the gun straight out in front of the shooter, with a tracer along it for a moment // ------------------------------------------------------------------------ struct Tracer { Mesh* mesh = nullptr; float life = 0.0f; // real seconds left, 0 when it's free }; std::vector tracers; const float tracerTime = 0.06f; auto ShowTracer = [&](const glm::vec3& from, const glm::vec3& direction, float length) { Tracer* tracer = nullptr; for (Tracer& candidate : tracers) if (candidate.life <= 0.0f) { tracer = &candidate; break; } if (!tracer) { tracers.push_back(Tracer()); tracer = &tracers.back(); tracer->mesh = CreateCubeMesh(); tracer->mesh->name = "tracer"; tracer->mesh->colorOverride = glm::vec4(1.0f, 0.86f, 0.55f, 1.0f); tracer->mesh->emissive = 1.0f; tracer->mesh->castshadow = false; } // A thin box from end to end, its length along its own z tracer->mesh->t.ScaleTo({ 0.025f, 0.025f, length }); tracer->mesh->t.RotateToQuaternion(LookAt(direction)); tracer->mesh->t.TranslateTo(from + direction * (length * 0.5f)); if (tracer->life <= 0.0f) tracer->mesh->BindToRenderSystem(); tracer->life = tracerTime; }; // Sparks out of the muzzle: a quick burst of glowing bits flying out along the shot ParticleEmitter* muzzleSparks = new ParticleEmitter(); muzzleSparks->name = "muzzle sparks"; muzzleSparks->surface = SurfaceTexture::Stretched(blanktex); muzzleSparks->opaque = false; // particles are only drawn in the translucent pass muzzleSparks->color = glm::vec4(1.8f, 1.3f, 0.6f, 1.0f); // over 1 so they glow muzzleSparks->size = glm::vec3(0.06f); muzzleSparks->speed = 7.0f; muzzleSparks->lifespan = 0.14f; // real seconds muzzleSparks->emitangle = glm::vec3(0.7f, 0.7f, 0.0f); // radians of spread muzzleSparks->facecamera = true; muzzleSparks->BindToRenderSystem(); const int sparksPerShot = 14; auto UpdateTracers = [&](float dt) { muzzleSparks->Step(dt); for (Tracer& tracer : tracers) { if (tracer.life <= 0.0f) continue; tracer.life -= dt; if (tracer.life <= 0.0f) { tracer.life = 0.0f; rendersystem->RemoveRenderable(tracer.mesh); } } }; auto FireShot = [&](Character& shooter, const Ability& ability) { // From the muzzle if its gun has one marked, the grip if it doesn't, otherwise about chest height in front of it glm::mat4 grip; const HeldModel* weapon = shooter.characterTemplate ? &shooter.characterTemplate->weapon : nullptr; const glm::vec3 origin = weapon && GetWeaponGrip(shooter, grip) ? glm::vec3(grip * glm::vec4(weapon->hasMuzzle ? weapon->muzzle : glm::vec3(0.0f), 1.0f)) : shooter.body->t.GetTranslation() + glm::vec3(0.0f, 0.4f, 0.0f) + shooter.GetForwardDirection() * 0.4f; // Straight out the way it faces, level. Using the ability turned it to face where it was aimed. const glm::vec3 direction = shooter.GetForwardDirection(); // A long thin hitbox from the gun out along the shot, through the hitbox system like a punch's. // Its own swing id, so it hits each character once. Hitbox shot; shot.owner = &shooter; shot.swing = CharacterCombat::NewSwingId(); shot.box.ScaleTo({ ability.hitboxSize.x, ability.hitboxSize.y, ability.range }); shot.box.RotateToQuaternion(LookAt(direction)); shot.box.TranslateTo(origin + direction * (ability.range * 0.5f)); shot.damage = ability.damage; shot.knockback = ability.knockback; shot.stun = ability.stun; shot.killAnimation = ability.killAnimation; shot.killAbility = &ability; hitboxsystem->Add(shot); ShowTracer(origin, direction, ability.range); if (&shooter == controlled && ability.screenshake > 0.0f) camera->AddScreenshake(ability.screenshake); muzzleSparks->t.TranslateTo(origin); muzzleSparks->t.RotateToQuaternion(LookAt(direction)); for (int i = 0; i < sparksPerShot; i++) muzzleSparks->Emit(); // Its own channel every time, so quick shots overlap rather than cutting each other off if (!ability.sound.empty()) if (SoundData* bang = soundsystem->LoadSound(ability.sound, true)) soundsystem->PlaySound3D(bang, origin, false); }; // ------------------------------------------------------------------------ // Props: models abilities show on their animation's own bones, like a knife, and knives left stuck in people // ------------------------------------------------------------------------ struct PropModel { Mesh* mesh = nullptr; glm::vec3 center = glm::vec3(0.0f); // its box around the model's origin, which goes on the bone glm::vec3 size = glm::vec3(1.0f); bool shown = false; }; auto MakeProp = [&](const std::string& path) { PropModel prop; const MeshData* data = resourcemanager->GetMeshData(path); if (!data) return prop; prop.mesh = new Mesh(data->vertices, data->indices); prop.mesh->name = path; prop.center = prop.mesh->t.GetTranslation(); // a fresh mesh's box is where the file had it prop.size = prop.mesh->t.GetScale(); prop.mesh->surface = SurfaceTexture::Stretched(resourcemanager->GetModelTexture(path)); if (!prop.mesh->surface.texture) prop.mesh->colorOverride = glm::vec4(0.55f, 0.56f, 0.6f, 1.0f); prop.mesh->metallic = 0.7f; prop.mesh->roughness = 0.35f; prop.mesh->castshadow = true; return prop; }; // Puts it where grip takes its model's origin, or hides it auto ShowProp = [&](PropModel& prop, bool show, const glm::mat4& grip) { if (!prop.mesh) return; if (show) { const glm::vec3 scale(glm::length(glm::vec3(grip[0])), glm::length(glm::vec3(grip[1])), glm::length(glm::vec3(grip[2]))); const glm::mat3 rotation(glm::vec3(grip[0]) / scale.x, glm::vec3(grip[1]) / scale.y, glm::vec3(grip[2]) / scale.z); prop.mesh->t.TranslateTo(glm::vec3(grip * glm::vec4(prop.center, 1.0f))); prop.mesh->t.RotateToQuaternion(glm::normalize(glm::quat_cast(rotation))); prop.mesh->t.ScaleTo(prop.size * scale); } if (show != prop.shown) { if (show) prop.mesh->BindToRenderSystem(); else rendersystem->RemoveRenderable(prop.mesh); prop.shown = show; } }; // Where an ability's prop goes this frame, if the ability's animation is playing with its bone auto GetPropGrip = [&](const Character& user, const Ability& ability, glm::mat4& grip) { glm::mat4 bone; if (ability.prop.empty() || !user.GetActionBoneTransform(ability.animation, ability.propBone, bone)) return false; grip = bone * glm::mat4_cast(glm::quat(glm::radians(ability.propTurn))); return true; }; // Where a character's body is and which way it faces, without its box's size, for sticking things to it auto GetBodyFrame = [](const Character& character) { return character.body->t.GetTranslationMatrix() * character.body->t.GetRotationMatrix(); }; std::map, PropModel> handProps; // by who's holding it and what it is // Knives stuck in people, bleeding them until they come out struct StuckProp { PropModel model; Character* owner = nullptr; // who put it there unsigned int use = 0; // which use of their ability, so they stop holding it Character* target = nullptr; int targetDeaths = 0; // comes out when they go down glm::mat4 local = glm::mat4(1.0f); // from their body frame to its grip float timeLeft = 0.0f; // real seconds float bleedDamage = 0.0f; // a second float bleedOwed = 0.0f; // damage built up that hasn't been dealt yet float dripOwed = 0.0f; // particles built up that haven't come out yet }; std::vector stuckProps; // Blood: dark red drops falling off the wound ParticleEmitter* bleedParticles = new ParticleEmitter(); bleedParticles->name = "bleed"; bleedParticles->opaque = false; bleedParticles->surface = SurfaceTexture::Stretched(blanktex); bleedParticles->color = glm::vec4(0.55f, 0.02f, 0.03f, 1.0f); bleedParticles->size = glm::vec3(0.045f); bleedParticles->speed = 1.6f; bleedParticles->lifespan = 0.6f; // real seconds bleedParticles->emitangle = glm::vec3(1.4f, 1.4f, 0.0f); bleedParticles->gravity = glm::vec3(0.0f, -9.0f, 0.0f); bleedParticles->facecamera = true; bleedParticles->BindToRenderSystem(); const float dripsPerSecond = 30.0f; const int dripsOnHit = 18; auto Bleed = [&](const glm::vec3& at, const glm::vec3& outward, int count) { bleedParticles->t.TranslateTo(at); bleedParticles->t.RotateToQuaternion(LookAt(glm::length(outward) > 0.001f ? outward : glm::vec3(0.0f, 1.0f, 0.0f))); for (int i = 0; i < count; i++) bleedParticles->Emit(); }; // Who each use of an ability has hit, for giving them its status later on struct AbilityHit { Character* user = nullptr; unsigned int use = 0; Character* target = nullptr; }; std::vector abilityHits; // Puts the ability's status on target now, or remembers them to get it at its statusFrame auto HitWithStatus = [&](Character& user, const Ability& ability, unsigned int use, Character& target) { if (!ability.appliesStatus || target.IsDead()) return; if (ability.statusFrame <= 0.0f) target.AddStatus(ability.status, ability.statusTime); else abilityHits.push_back({ &user, use, &target }); }; // Its statusFrame has come: everyone this use of it hit gets its status auto ApplyAbilityStatus = [&](Character& user, const Ability& ability) { const unsigned int use = user.body->combat.GetAbilityUse(); for (const AbilityHit& hit : abilityHits) if (hit.user == &user && hit.use == use && !hit.target->IsDead()) hit.target->AddStatus(ability.status, ability.statusTime); }; // A wide box in front of it for this one frame, hitting everyone in it auto FireSweep = [&](Character& user, const Ability& ability) { const glm::vec3 forward = user.GetForwardDirection(); Hitbox sweep; sweep.owner = &user; sweep.swing = CharacterCombat::NewSwingId(); sweep.box.ScaleTo({ ability.hitboxSize.x, ability.hitboxSize.y, ability.range }); sweep.box.RotateToQuaternion(LookAt(forward)); sweep.box.TranslateTo(user.body->t.GetTranslation() + glm::vec3(0.0f, 0.3f, 0.0f) + forward * (0.3f + ability.range * 0.5f)); sweep.damage = ability.damage; sweep.knockback = ability.knockback; sweep.stun = ability.stun; sweep.knockdown = ability.knockdownTime; sweep.hitsKnockedDown = ability.hitsKnockedDown; const unsigned int use = user.body->combat.GetAbilityUse(); sweep.onHit = [&, &user = user, ability, use](Character& target) { user.body->combat.AbilityLanded(); HitWithStatus(user, ability, use, target); }; hitboxsystem->Add(sweep); if (ability.screenshake > 0.0f && &user == controlled) camera->AddScreenshake(ability.screenshake); }; auto FireStab = [&](Character& user, const Ability& ability) { // A box just in front of it, like a punch's, out for this one frame const glm::vec3 forward = user.GetForwardDirection(); Hitbox stab; stab.owner = &user; stab.swing = CharacterCombat::NewSwingId(); stab.box.ScaleTo({ ability.hitboxSize.x, ability.hitboxSize.y, ability.range }); stab.box.RotateToQuaternion(LookAt(forward)); stab.box.TranslateTo(user.body->t.GetTranslation() + glm::vec3(0.0f, 0.3f, 0.0f) + forward * (0.3f + ability.range * 0.5f)); stab.damage = ability.damage; stab.knockback = ability.knockback; stab.stun = ability.stun; // Landing it leaves the knife where it went in, at the first one it hits const unsigned int use = user.body->combat.GetAbilityUse(); stab.onHit = [&, &user = user, ability, use](Character& target) { if (user.body->combat.HasAbilityLanded()) return; user.body->combat.AbilityLanded(); if (target.IsDead()) return; // that put them down, there's nothing to stick it in glm::mat4 grip; if (!GetPropGrip(user, ability, grip)) { // No knife bone to go by, so just into their middle, pointing the way it was stabbed const glm::vec3 into = glm::normalize(target.body->t.GetTranslation() - user.body->t.GetTranslation() + glm::vec3(0.0f, 0.001f, 0.0f)); grip = glm::translate(glm::mat4(1.0f), target.body->t.GetTranslation() + glm::vec3(0.0f, 0.3f, 0.0f) - into * 0.25f) * glm::mat4_cast(LookAt(into)) * glm::scale(glm::mat4(1.0f), glm::vec3(user.visualScale)); } StuckProp stuck; stuck.model = MakeProp(ability.prop); stuck.owner = &user; stuck.use = use; stuck.target = ⌖ stuck.targetDeaths = target.GetDeaths(); stuck.local = glm::inverse(GetBodyFrame(target)) * grip; stuck.timeLeft = ability.bleedTime; stuck.bleedDamage = ability.bleedDamage; stuckProps.push_back(stuck); const glm::vec3 wound(grip[3]); Bleed(wound, wound - target.body->t.GetTranslation(), dripsOnHit); }; hitboxsystem->Add(stab); }; // Every frame: knives in hands, knives in people, and the bleeding // Heavy gravity: pale streaks falling around them from above their head, like they're being pushed down ParticleEmitter* gravityLines = new ParticleEmitter(); gravityLines->name = "gravity lines"; gravityLines->opaque = false; gravityLines->surface = SurfaceTexture::Stretched(blanktex); gravityLines->color = glm::vec4(0.75f, 0.7f, 1.4f, 0.55f); gravityLines->size = glm::vec3(0.025f, 0.7f, 1.0f); // thin and tall, facing the camera gravityLines->speed = 9.0f; gravityLines->lifespan = 0.3f; // real seconds gravityLines->emitangle = glm::vec3(0.0f); gravityLines->facecamera = true; gravityLines->upright = true; // straight down in the world, however the camera's tilted gravityLines->BindToRenderSystem(); const float gravityLinesPerSecond = 28.0f; std::map gravityLinesOwed; // lines built up that haven't come out yet, by who they're on std::uniform_real_distribution unitRandom(0.0f, 1.0f); std::mt19937 gravityLineRng(1234); // ...and a hum on them, looped for as long as it lasts const std::string gravitySoundFile = "assets/gravity.mp3"; SoundData* gravitySound = std::filesystem::exists(gravitySoundFile) ? soundsystem->LoadSound(gravitySoundFile, true) : nullptr; soundsystem->SetDistances(gravitySound, 2.0f, 30.0f); // a hum around them, gone a little way off std::map gravitySounds; // by who it's on auto UpdateStatusVisuals = [&](float dt) { for (Character* character : charactermanager->GetCharacters()) { const bool heavy = !character->IsDead() && character->HasStatus(StatusEffect::Type::HeavyGravity); auto sound = gravitySounds.find(character); if (heavy && gravitySound) { bool playing = false; if (sound != gravitySounds.end() && sound->second) sound->second->isPlaying(&playing); if (playing) soundsystem->SetChannelPosition(sound->second, character->body->t.GetTranslation()); else gravitySounds[character] = soundsystem->PlaySound3D(gravitySound, character->body->t.GetTranslation(), true); } else if (sound != gravitySounds.end()) { if (sound->second) sound->second->stop(); gravitySounds.erase(sound); } if (!heavy) { gravityLinesOwed.erase(character); continue; } float& owed = gravityLinesOwed[character]; owed += gravityLinesPerSecond * dt; const glm::vec3 centre = character->body->t.GetTranslation(); const float halfHeight = character->body->t.GetScale().y * 0.5f; // Not LookAt, which can't point straight down. They face the camera anyway, only their velocity matters. gravityLines->t.RotateToQuaternion(glm::quat(1.0f, 0.0f, 0.0f, 0.0f)); for (; owed >= 1.0f; owed -= 1.0f) { // Somewhere in a ring around them, starting a little over their head const float angle = unitRandom(gravityLineRng) * 6.2831853f; const float radius = 0.35f + unitRandom(gravityLineRng) * 0.35f; const float above = halfHeight + 0.3f + unitRandom(gravityLineRng) * 0.6f; gravityLines->t.TranslateTo(centre + glm::vec3(std::cos(angle) * radius, above, std::sin(angle) * radius)); gravityLines->Emit(); gravityLines->particles.back()->linearvelocity = glm::vec3(0.0f, -gravityLines->speed, 0.0f); } } gravityLines->Step(dt); }; auto UpdateProps = [&](float dt) { // In the hand while its ability plays, until it's left in someone for (Character* character : charactermanager->GetCharacters()) { const Ability* ability = character->body->combat.GetCurrentAbility(); for (auto& [key, prop] : handProps) if (key.first == character && (!ability || ability->prop != key.second)) ShowProp(prop, false, glm::mat4(1.0f)); if (!ability || ability->prop.empty()) continue; PropModel& prop = handProps[{ character, ability->prop }]; if (!prop.mesh) prop = MakeProp(ability->prop); bool leftBehind = false; for (const StuckProp& stuck : stuckProps) if (stuck.owner == character && stuck.use == character->body->combat.GetAbilityUse()) leftBehind = true; glm::mat4 grip; const bool holding = !leftBehind && GetPropGrip(*character, *ability, grip); ShowProp(prop, holding, grip); } // Stuck ones ride their target, bleeding them, until they run out or the target goes down for (auto it = stuckProps.begin(); it != stuckProps.end(); ) { StuckProp& stuck = *it; stuck.timeLeft -= dt; const bool over = stuck.timeLeft <= 0.0f || stuck.target->GetDeaths() != stuck.targetDeaths; if (over) { ShowProp(stuck.model, false, glm::mat4(1.0f)); it = stuckProps.erase(it); continue; } const glm::mat4 grip = GetBodyFrame(*stuck.target) * stuck.local; ShowProp(stuck.model, true, grip); // Whole points of damage at a time, so the health bar ticks rather than crawls stuck.bleedOwed += stuck.bleedDamage * dt; if (stuck.bleedOwed >= 1.0f) { const float damage = std::floor(stuck.bleedOwed); stuck.bleedOwed -= damage; Hurt(*stuck.target, damage); } stuck.dripOwed += dripsPerSecond * dt; const int drips = (int)stuck.dripOwed; stuck.dripOwed -= (float)drips; if (drips > 0) { const glm::vec3 wound(grip[3]); Bleed(wound, wound - stuck.target->body->t.GetTranslation(), drips); } ++it; } bleedParticles->Step(dt); }; // Debug: B plays the sword test, with the greatsword on the R_Weapon bone for as long as it lasts // ------------------------------------------------------------------------ Animation* swordTest = charactermanager->GetAnimation("testsword"); const std::string greatswordPath = "assets/greatsword.glb"; Mesh* greatsword = nullptr; glm::vec3 greatswordCenter(0.0f); // its box around its own origin, which is the grip glm::vec3 greatswordSize(1.0f); // Degrees. Weapons are modelled blade along +Y in Blender, the same way bones point. Exporting turns the mesh's // +Y into -Z but leaves bones' own axes alone, so this turns it back onto the bone. Any weapon made that way needs it. glm::vec3 greatswordTurn(90.0f, 0.0f, 0.0f); bool greatswordShown = false; if (const MeshData* data = resourcemanager->GetMeshData(greatswordPath, "Cube")) { greatsword = new Mesh(data->vertices, data->indices); greatsword->name = "greatsword"; greatswordCenter = greatsword->t.GetTranslation(); // a fresh mesh's box is where the file had it greatswordSize = greatsword->t.GetScale(); greatsword->surface = SurfaceTexture::Stretched(resourcemanager->GetModelTexture(greatswordPath)); if (!greatsword->surface.texture) greatsword->colorOverride = glm::vec4(0.6f, 0.6f, 0.62f, 1.0f); greatsword->metallic = 0.8f; greatsword->roughness = 0.4f; greatsword->castshadow = true; } console->Register("swordturn", "swordturn [x y z]", "shows or turns the greatsword on its bone, in degrees", [&](const Console::Arguments& args) { glm::vec3 turn; if (args.size() >= 3) { if (!ParseFloat(args[0], turn.x) || !ParseFloat(args[1], turn.y) || !ParseFloat(args[2], turn.z)) { console->Print("Usage: swordturn [x y z]"); return; } greatswordTurn = turn; } std::ostringstream text; text << std::fixed << std::setprecision(1) << "Greatsword turned " << greatswordTurn.x << " " << greatswordTurn.y << " " << greatswordTurn.z; console->Print(text.str()); }); // ------------------------------------------------------------------------ // 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(-(mapHalfSize - 2.0f), mapHalfSize - 2.0f); return { spread(respawnRng), 6.0f, spread(respawnRng) }; // dropped in from a little above the floor }; auto UpdateHitboxDebug = [&]() { std::vector lines; if (showHitboxes) hitboxsystem->AppendDebugLines(charactermanager->GetCharacters(), lines); rendersystem->SetHitboxDebugLines(lines); }; // ======================================================================== // Game loop // ======================================================================== // ------------------------------------------------------------------------ // Executions: someone killed by a move with a killAnimation plays it out before getting back up, and when the player // is the killer or the killed, the camera rides the animation's Camera bone meanwhile // ------------------------------------------------------------------------ struct Execution { Character* victim = nullptr; Animation* animation = nullptr; float timeLeft = 0.0f; // real seconds float time = 0.0f; // real seconds since it started bool filmed = false; // the camera's following its Camera bone std::vector sounds; // still to play }; std::vector executions; auto IsFilming = [&]() { for (const Execution& execution : executions) if (execution.filmed) return true; return false; }; auto StartExecution = [&](Character& victim, const Character* killer, Animation* animation, const Ability* ability) { if (!animation || victim.HasStatus(StatusEffect::Type::Executed)) return; victim.AddStatus(StatusEffect::Type::Executed); // until it's over, see UpdateExecutions victim.body->combat.Reset(); victim.body->CancelDash(); victim.body->SetWalking(false); if (killer) { const glm::vec3 toKiller = killer->body->t.GetTranslation() - victim.body->t.GetTranslation(); victim.body->Face(toKiller, true); } victim.body->PlayAction(animation, 1.0f, true); if (p* box = victim.body->physics) box->linearvelocity = glm::vec3(0.0f, std::min(box->linearvelocity.y, 0.0f), 0.0f); Execution execution; execution.victim = &victim; execution.animation = animation; const float ticksPerSecond = animation->GetTicksPerSecond() > 0.0f ? animation->GetTicksPerSecond() : 25.0f; execution.timeLeft = animation->GetDuration() / ticksPerSecond; execution.filmed = (&victim == controlled || killer == controlled) && !IsFilming(); if (ability && execution.filmed) execution.sounds = ability->killSounds; executions.push_back(execution); }; auto UpdateExecutions = [&](float dt) { for (auto it = executions.begin(); it != executions.end(); ) { Execution& execution = *it; Character& victim = *execution.victim; execution.timeLeft -= dt; execution.time += dt; // Its sounds as their frames come for (auto sound = execution.sounds.begin(); sound != execution.sounds.end(); ) { if (execution.time < sound->frame / 60.0f) { ++sound; continue; } soundsystem->PlaySound(soundsystem->LoadSound(sound->file, false)); sound = execution.sounds.erase(sound); } // Held where it went down, the animation moving it rather than anything pushing it about if (p* box = victim.body->physics) { box->linearvelocity.x = 0.0f; box->linearvelocity.z = 0.0f; } if (execution.timeLeft > 0.0f) { ++it; continue; } // Over: up again somewhere else, or for those that heal on death, right where they are victim.RemoveStatus(StatusEffect::Type::Executed); if (Animator* action = victim.body->actionAnimator; action && action->m_CurrentAnimation == execution.animation) { action->holdsAtEnd = false; action->StopAnimation(); } if (victim.IsDead()) { victim.body->t.TranslateTo(RandomSpawn()); victim.Revive(); } if (execution.filmed) camera->RestoreLook(); it = executions.erase(it); } }; // The camera on the filmed execution's Camera bone, looking down its -Z with its +Y up, like a Blender camera auto FilmExecution = [&]() { for (const Execution& execution : executions) { if (!execution.filmed) continue; glm::mat4 bone; if (!execution.victim->GetActionBoneTransform(execution.animation, "Camera", bone)) return false; const glm::vec3 forward = glm::normalize(-glm::vec3(bone[2])); const glm::vec3 right = glm::normalize(glm::cross(glm::vec3(bone[1]), forward)); const glm::vec3 up = glm::cross(forward, right); camera->t.TranslateTo(glm::vec3(bone[3])); camera->t.RotateToQuaternion(glm::normalize(glm::quat_cast(glm::mat3(right, up, forward)))); return true; } return false; }; 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) * GetGameTimeScale(); const float realDeltatime = glm::clamp(float(frameNow - prevFrameTime), 0.0f, 0.1f); // for UI timing, no speedup 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; // Debug: B plays the sword test on the action layer static bool prevSwordDown = false; const bool swordDown = keyboard->IsKeyDown(GLFW_KEY_B); if (swordDown && !prevSwordDown && !menuActive && swordTest && !controlled->ragdoll) { controlled->body->Face(camera->t.GetFrontVector(), true); // swings the way the camera looks controlled->body->PlayAction(swordTest); } prevSwordDown = swordDown; 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::Characters) menuScreen = charactersReturn; 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 && !IsFilming()) { float xoffset = mouse->GetX() - mouse->GetLastX(); float yoffset = mouse->GetLastY() - mouse->GetY(); camera->ProcessMouseMovement(xoffset, yoffset); } } // -------------------------------------------------------------------- // 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); // Freed in the middle, under the crosshair, rather than wherever GLFW last saw it 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) || (menuScreen == MenuScreen::Characters && charactersReturn == 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::Characters) { for (size_t i = 0; i < fighterButtons.size(); i++) if (fighterButtons[i].hovered) controlled->ApplyTemplate(*fighters[i]); if (backButton.hovered) menuScreen = charactersReturn; } else if (playButton.hovered) { ResumeGame(); } else if (charactersButton.hovered) { charactersReturn = menuScreen; menuScreen = MenuScreen::Characters; } 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; UpdateHeldWeapons(); UpdateHitboxDebug(); rendersystem->Render(*camera); glfwPollEvents(); continue; } // -------------------------------------------------------------------- // Characters: minds decide actions, bodies carry them out // -------------------------------------------------------------------- keyboardMind.enabled = !console->IsOpen() && !IsFilming(); // typing isn't walking, and a cutscene isn't played keyboardMind.mouseEnabled = camera->lockedcursor; // with the cursor out, clicks are for menus charactermanager->Think(); for (Character* character : charactermanager->GetCharacters()) { if (character->ragdoll) continue; character->ApplyMovement(IsGrounded(*character), deltatime, refDt); } charactermanager->Step(deltatime); // Hitboxes landing: damage, and a shove away from whoever put them out. Knocked down, the ragdoll gets the shove. // Abilities going off this frame, before the hitboxes are resolved so theirs count this frame for (Character* user : charactermanager->GetCharacters()) { for (const AbilityFired& use : user->body->combat.TakeFired()) { const Ability& ability = user->body->combat.abilities[use.index]; if (use.status) ApplyAbilityStatus(*user, ability); else if (ability.effect == Ability::Effect::Shot) FireShot(*user, ability); else if (ability.effect == Ability::Effect::Stab) FireStab(*user, ability); else if (ability.effect == Ability::Effect::Sweep) FireSweep(*user, ability); } } // Hits are only remembered while the use that made them is still going abilityHits.erase(std::remove_if(abilityHits.begin(), abilityHits.end(), [](const AbilityHit& hit) { return !hit.user->body->combat.GetCurrentAbility() || hit.user->body->combat.GetAbilityUse() != hit.use; }), abilityHits.end()); UpdateTracers(realDeltatime); UpdateProps(realDeltatime); UpdateStatusVisuals(realDeltatime); charactermanager->AddAttackHitboxes(*hitboxsystem); hitboxsystem->Resolve(charactermanager->GetCharacters(), [&](const Hitbox& hitbox, Character& target) { const Character* attacker = hitbox.owner; glm::vec3 away = target.body->t.GetTranslation() - (attacker ? attacker->body->t.GetTranslation() : hitbox.box.GetTranslation()); away.y = 0.0f; away = glm::length(away) > 0.001f ? glm::normalize(away) : glm::vec3(0.0f, 0.0f, 1.0f); const glm::vec3 shove = away * hitbox.knockback + glm::vec3(0.0f, hitbox.knockbackUp, 0.0f); // Worked out before the damage, since one that heals on death is never left dead to tell by const bool lethal = hitbox.killAnimation && !target.IsDead() && hitbox.damage >= target.GetHealth(); LandHit(attacker, target, hitbox.damage, shove, hitbox.knockbackUp > 0.0f ? 0.6f : 0.3f, hitbox.stun); if (lethal) { StartExecution(target, attacker, hitbox.killAnimation, hitbox.killAbility); return; } if (hitbox.knockdown > 0.0f) target.Knockdown(hitbox.knockdown); if (hitbox.onHit) hitbox.onHit(target); }, realDeltatime); 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 = GetGameTimeScale(); 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(); // Dying puts anyone straight back on their feet somewhere else, at full health, once any execution's played out UpdateExecutions(realDeltatime); for (Character* character : charactermanager->GetCharacters()) { if (!character->IsDead() || character->HasStatus(StatusEffect::Type::Executed)) continue; character->body->t.TranslateTo(RandomSpawn()); character->Revive(); } 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 // -------------------------------------------------------------------- { // Down, the body box stays put and the ragdoll's pelvis is where they really are glm::vec3 playerPos = controlled->body->t.GetTranslation(); if (controlled->ragdoll && !controlled->ragdoll->GetParts().empty()) playerPos = controlled->ragdoll->GetParts()[0]->transform.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); // An execution being filmed takes it over FilmExecution(); } camera->Step(deltatime); Sound* music = static_cast(mainf->GetFirstChildOfName("music")); if (music) music->Update3DPosition(0.0f, 0.0f, 0.0f); // Heard from the character's head rather than the camera behind it, so things next to them sound next to them, // turned the way the camera looks so left and right match the screen const glm::vec3 ears = controlled && controlled->body ? controlled->body->t.GetTranslation() + glm::vec3(0.0f, controlled->body->t.GetScale().y * 0.35f, 0.0f) : camera->t.GetTranslation(); soundsystem->SetListenerPosition( ears, camera->t.GetFrontVector(), camera->t.GetUpVector() ); soundsystem->Update(); // -------------------------------------------------------------------- // HUD. After the camera, since the health bars over heads follow them on screen. // -------------------------------------------------------------------- { // With the cursor out, the moves list tells what's under it const glm::vec2 cursor((float)mouse->GetX(), (float)mouse->GetY()); hud->Update(realDeltatime, controlled, &charactermanager->GetCharacters(), camera, camera->lockedcursor ? nullptr : &cursor); // The greatsword rides the weapon bone while the sword test plays, its grip on the bone glm::mat4 weaponBone; const bool holding = greatsword && controlled->GetActionBoneTransform(swordTest, "R_Weapon", weaponBone); if (holding) { const glm::mat4 grip = weaponBone * glm::mat4_cast(glm::quat(glm::radians(greatswordTurn))); const glm::vec3 scale(glm::length(glm::vec3(grip[0])), glm::length(glm::vec3(grip[1])), glm::length(glm::vec3(grip[2]))); const glm::mat3 rotation(glm::vec3(grip[0]) / scale.x, glm::vec3(grip[1]) / scale.y, glm::vec3(grip[2]) / scale.z); greatsword->t.TranslateTo(glm::vec3(grip * glm::vec4(greatswordCenter, 1.0f))); greatsword->t.RotateToQuaternion(glm::normalize(glm::quat_cast(rotation))); greatsword->t.ScaleTo(greatswordSize * scale); } if (greatsword && holding != greatswordShown) { if (holding) greatsword->BindToRenderSystem(); else rendersystem->RemoveRenderable(greatsword); greatswordShown = holding; } UpdateHeldWeapons(); } // -------------------------------------------------------------------- // 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(); } }