Files
combattest/Engine.cpp
T
2026-11-08 07:45:01 -05:00

2139 lines
102 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>
#include <functional>
#include <cctype>
#include <map>
#include <filesystem>
// ============================================================================
// 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"
#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 <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->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 <amount> [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 <amount> [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<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, -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<CharacterTemplate*> fighters;
for (CharacterTemplate* candidate : charactermanager->GetTemplates())
if (candidate->selectable)
fighters.push_back(candidate);
std::vector<MenuButton> 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<MenuButton*> 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<std::pair<Character*, const CharacterTemplate*>, 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<Tracer> 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<std::pair<Character*, std::string>, 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<StuckProp> 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<AbilityHit> 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 = &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<Character*, float> gravityLinesOwed; // lines built up that haven't come out yet, by who they're on
std::uniform_real_distribution<float> 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<Character*, FMOD::Channel*> 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<float> 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<HitboxDebugLine> 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<Ability::KillSound> sounds; // still to play
};
std::vector<Execution> 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<Sound*>(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<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();
}
}