#include "Engine.h" // ============================================================================ // Standard library // ============================================================================ #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 "Net.h" #include "font.h" // Character / animation #include "Rig.h" #include "Animator.h" #include "Character.h" #include "DefaultBodyComponents.h" #include "BodyComponentActions.h" #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" // ============================================================================ // Constructor � window, callbacks, systems, shaders // ============================================================================ Engine::Engine() { engine = this; glfwSetFramebufferSizeCallback(window->handle, framebuffer_size_callback); glfwSetCursorPosCallback(window->handle, mouse_callback); glfwSetScrollCallback(window->handle, scroll_callback); glfwSetKeyCallback(window->handle, key_callback); glfwSetMouseButtonCallback(window->handle, mouse_button_callback); glfwSwapInterval(1); // Start from the real framebuffer size (can differ from the requested size under DPI scaling) int fbw, fbh; glfwGetFramebufferSize(window->handle, &fbw, &fbh); framebuffer_size_callback(window->handle, fbw, fbh); rendersystem = new RenderSystem(); rendersystem->Initialize(); physicsengine = new Physics(); physicsengine->gravity *= 1.25f; // slightly stronger gravity to suit the larger map 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->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"); } // ============================================================================ // Initiate � scene setup + game loop // ============================================================================ void Engine::Initiate() { // ------------------------------------------------------------------------ // Assets // ------------------------------------------------------------------------ Font* arial = new Font("assets/ft/arial.ttf"); Texture* blanktex = new Texture("assets/blanktexture.png"); Texture* magic = new Texture("assets/itsmagicbitch.jpg"); Texture* snowflake = new Texture("assets/snowflake.png"); Texture* leontex = new Texture("assets/revolvertex.png"); // ------------------------------------------------------------------------ // Shared rig + animations // ------------------------------------------------------------------------ Rig* testrig = new Rig("assets/rig.glb"); Animation* testanimation = new Animation("assets/walkanim.glb"); Animation* punchanim = new Animation("assets/punchanim.glb"); // ------------------------------------------------------------------------ // Player character // ------------------------------------------------------------------------ Character* testcharacter = new Character(); testcharacter->name = "character"; testcharacter->castshadow = true; testcharacter->body->t.ScaleBy({ 0.5f, 1.8f, 0.5f }); testcharacter->body->t.TranslateTo({ 0.0f, 0.9f, 0.0f }); testcharacter->body->SetRig(testrig); // Base animator (weight 0) � walk, looped testcharacter->body->SetAnimation(testanimation); testcharacter->body->animator->loops = true; // Punch animator (weight 1) � one-shot, starts stopped Animator* punchAnimator = testcharacter->body->AddAnimator(1); punchAnimator->loops = false; punchAnimator->SetAnimation(punchanim); punchAnimator->StopAnimation(); // First person: don't draw our own model (other players still see it via the network) p* characterBody = new p(testcharacter->body); characterBody->mass = 80.0f; characterBody->lockRotation = true; characterBody->friction = 0.0f; physicsengine->AddObject(characterBody); // ------------------------------------------------------------------------ // NPC // ------------------------------------------------------------------------ Character* npc = new Character(); npc->name = "npc"; npc->castshadow = true; npc->body->t.ScaleBy({ 0.5f, 1.8f, 0.5f }); npc->body->t.TranslateTo({ 3.0f, 0.9f, 3.0f }); npc->body->SetRig(testrig); npc->body->SetAnimation(testanimation); npc->BindToRenderSystem(); p* npcBody = new p(npc->body); npcBody->mass = 80.0f; npcBody->lockRotation = true; npcBody->friction = 0.0f; physicsengine->AddObject(npcBody); // ------------------------------------------------------------------------ // Remote players (other PCs), keyed by network id. Not in physics: their transform // comes from the network. Created on first packet, removed when packets stop. // ------------------------------------------------------------------------ struct RemotePlayer { Character* character = nullptr; double lastSeen = 0.0; }; std::map remotes; auto SpawnRemote = [&]() -> Character* { Character* c = new Character(); c->name = "remote"; c->castshadow = true; c->body->t.ScaleBy({ 0.5f, 1.8f, 0.5f }); c->body->SetRig(testrig); c->body->SetAnimation(testanimation); c->BindToRenderSystem(); return c; }; // In a LAN game, start at a random spot so players don't all spawn inside each other if (lan && lan->IsActive()) { std::mt19937 rng(std::random_device{}()); std::uniform_real_distribution spread(-8.0f, 8.0f); testcharacter->body->t.TranslateTo({ spread(rng), 0.9f, spread(rng) }); } // ------------------------------------------------------------------------ // Rain particles // ------------------------------------------------------------------------ ParticleEmitter* rain = new ParticleEmitter(); rain->name = "rain"; rain->tex = snowflake; rain->opaque = false; rain->speed = 35.0f; rain->lifespan = 0.6f; rain->angularvelocity = { 0.0f, 0.0f, glm::radians(10.0f) }; rain->size = { 0.04f, 0.4f, 0.0f }; rain->emitangle = { glm::radians(60.0f), glm::radians(360.0f), glm::radians(60.0f) }; rain->facecamera = false; rain->color = { 1.0f, 1.0f, 1.0f, 1.0f }; rain->emitdirection = ParticleEmitter::EmitDirection::Perpendicular; rain->t.TranslateTo({ 0.0f, 10.0f, 0.0f }); rain->t.RotateToQuaternion(glm::quat(glm::vec3(glm::radians(85.0f), 0.0f, 0.0f))); rain->BindToRenderSystem(); mainf->AddChild(rain); // ------------------------------------------------------------------------ // 3D audio // ------------------------------------------------------------------------ Sound* music = new Sound(); music->name = "music"; music->LoadSound("assets/fs.wav", true); music->Update3DPosition(0.0f, 0.0f, 0.0f); music->PlayTrack(true); mainf->AddChild(music); // ------------------------------------------------------------------------ // GUI // ------------------------------------------------------------------------ Crosshair* crosshair = new Crosshair(); crosshair->Color = { 0.0f, 0.0f, 0.0f }; crosshair->Dot = true; crosshair->Apply(); // Hit marker: an X around the crosshair built from small squares (Box has no rotation), // white for a normal hit and red for a headshot / kill. Fades out after each hit. std::vector hitMarker; for (int corner = 0; corner < 4; ++corner) { const float sx = (corner & 1) ? 1.0f : -1.0f, sy = (corner & 2) ? 1.0f : -1.0f; for (int step = 0; step < 3; ++step) { const float d = 7.0f + step * 3.5f; Box* b = new Box(); b->Opacity = 0.0f; b->t2d.center = { 0.5f, 0.5f }; b->t2d.position = { 0.5f, 0.5f, sx * d, sy * d }; b->t2d.size = { 0.0f, 0.0f, 3.5f, 3.5f }; b->BindToRenderSystem(); hitMarker.push_back(b); } } float hitMarkerTime = 0.0f; const float hitMarkerDuration = 0.25f; auto ShowHitMarker = [&](bool strong) { hitMarkerTime = hitMarkerDuration; for (Box* b : hitMarker) b->Color = strong ? glm::vec3(1.0f, 0.15f, 0.15f) : glm::vec3(1.0f); }; TextBox* fpsCounter = new TextBox(); fpsCounter->font = arial; fpsCounter->text = "TEST1231231123"; 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(); TextBox* netText = new TextBox(); netText->font = arial; netText->text = ""; netText->fontsize = 0.5f; netText->Color = { 1.0f, 1.0f, 1.0f }; netText->t2d.position = { 1.0f, 0.0f, 0.0f, 40.0f }; netText->t2d.center = { 1.0f, 0.0f }; netText->textCenter = { 1.0f, 0.5f }; netText->t2d.size = { 0.0f, 0.0f, 360.0f, 40.0f }; netText->BindToRenderSystem(); // Full-screen green flash when the player takes damage (drawn before the HUD text) Box* damageFlash = new Box(); damageFlash->Color = { 0.0f, 1.0f, 0.2f }; damageFlash->Opacity = 0.0f; damageFlash->t2d.position = { 0.0f, 0.0f, 0.0f, 0.0f }; damageFlash->t2d.center = { 0.0f, 0.0f }; damageFlash->t2d.size = { 1.0f, 1.0f, 0.0f, 0.0f }; damageFlash->BindToRenderSystem(); int lastPlayerHealth = 100; float damageFlashTime = 0.0f; const float damageFlashDuration = 0.35f; TextBox* hpText = new TextBox(); hpText->font = arial; hpText->text = "100/100 HP"; hpText->fontsize = 0.7f; hpText->Color = { 1.0f, 1.0f, 1.0f }; hpText->t2d.position = { 0.0f, 1.0f, 20.0f, -20.0f }; hpText->t2d.center = { 0.0f, 1.0f }; hpText->textCenter = { 0.0f, 0.5f }; hpText->t2d.size = { 0.0f, 0.0f, 220.0f, 50.0f }; hpText->BindToRenderSystem(); TextBox* grappleText = new TextBox(); grappleText->font = arial; grappleText->text = "GRAPPLE READY"; grappleText->fontsize = 0.5f; grappleText->Color = { 1.0f, 1.0f, 1.0f }; grappleText->t2d.position = { 0.5f, 1.0f, 0.0f, -20.0f }; grappleText->t2d.center = { 0.5f, 1.0f }; grappleText->textCenter = { 0.5f, 0.5f }; grappleText->t2d.size = { 0.0f, 0.0f, 560.0f, 40.0f }; grappleText->BindToRenderSystem(); ImageBox* testgui = new ImageBox(); testgui->tex = magic; testgui->Color = { 1.0f, 1.0f, 1.0f }; testgui->Opacity = 0.8f; testgui->rounding = 0.1f; testgui->t2d.center = { 0.0f, 1.0f }; testgui->t2d.position = { 0.0f, 1.0f, 10.0f, -10.0f }; testgui->t2d.size = { 0.0f, 0.0f, 744.0f / 2.0f, 914.0f / 2.0f }; testgui->BindToRenderSystem(); // ------------------------------------------------------------------------ // World meshes // ------------------------------------------------------------------------ Model leonrevolver("assets/leonrevolver.obj"); for (Mesh* mesh : leonrevolver.meshes) { auto newmesh = mesh->Clone(); newmesh->name = "revolver"; newmesh->castshadow = true; newmesh->meshTexture = leontex; newmesh->t.TranslateBy({ 4.0f, 4.0f, 4.0f }); newmesh->BindToRenderSystem(); mainf->AddChild(newmesh); } Mesh* debugcube = CreateCubeMesh(); debugcube->name = "debugcube"; debugcube->t.ScaleTo({ 0.8f, 0.8f, 0.8f }); // Punch hitbox only: intentionally not bound to the render system, so it isn't drawn mainf->AddChild(debugcube); const float mapScale = 1.5f; // uniform scale applied to the floor and every map prop Mesh* floor = CreateCubeMesh(); floor->name = "floor"; floor->meshTexture = blanktex; floor->t.ScaleTo({ 80.0f * mapScale, 2.0f, 40.0f * mapScale }); floor->t.TranslateTo({ 0.0f, -1.0f, 0.0f }); floor->BindToRenderSystem(); mainf->AddChild(floor); p* floorBody = new p(floor); floorBody->velocity = false; physicsengine->AddObject(floorBody); // Static map props: axis-aligned blocks (collision is AABB-based, so dome/cones/ramp are // built from stacked tiers and stairs). Footprints are recorded so respawns avoid them. struct Footprint { float minX, maxX, minZ, maxZ; }; std::vector propFootprints; // pos = centre of the block's footprint on the floor; it sits on y = 0 and is `size.y` tall. // Walls are frictionless (player friction is 0 too) so you slide along them. auto AddBlock = [&](const char* name, glm::vec3 pos, glm::vec3 size, bool slippery = false) { pos *= mapScale; size *= mapScale; Mesh* m = CreateCubeMesh(); m->name = name; m->meshTexture = blanktex; m->t.ScaleTo(size); m->t.TranslateTo({ pos.x, pos.y + size.y * 0.5f, pos.z }); m->BindToRenderSystem(); mainf->AddChild(m); p* body = new p(m); body->velocity = false; if (slippery) body->friction = 0.0f; physicsengine->AddObject(body); propFootprints.push_back({ pos.x - size.x * 0.5f, pos.x + size.x * 0.5f, pos.z - size.z * 0.5f, pos.z + size.z * 0.5f }); }; // Concentric stack of square tiers, widest at the bottom (stands in for dome / cone) auto AddTiers = [&](const char* name, glm::vec2 pos, float baseWidth, float tierHeight, int tiers) { for (int i = 0; i < tiers; ++i) { const float w = baseWidth * (1.0f - float(i) / float(tiers)); AddBlock(name, { pos.x, tierHeight * i, pos.y }, { w, tierHeight, w }); } }; // Map is a rectangle: x in [-60, 60], z in [-30, 30] (authored at 1x, scaled by mapScale) // Centre dome AddTiers("dome", { 0.0f, 0.0f }, 18.0f, 1.0f, 5); // Cones, opposite corners of the dome AddTiers("cone", { -12.0f, -13.0f }, 6.0f, 1.0f, 5); AddTiers("cone", { 12.0f, 13.0f }, 6.0f, 1.0f, 5); // Big cubes either side of the dome AddBlock("cube", { -24.0f, 0.0f, 0.0f }, { 7.0f, 7.0f, 7.0f }); AddBlock("cube", { 24.0f, 0.0f, 0.0f }, { 7.0f, 7.0f, 7.0f }); // Stairs climbing to a tall back wall (the ramp), back right AddBlock("stair", { 29.0f, 0.0f, -12.0f }, { 2.5f, 1.0f, 10.0f }); AddBlock("stair", { 31.5f, 0.0f, -12.0f }, { 2.5f, 2.0f, 10.0f }); AddBlock("stair", { 34.0f, 0.0f, -12.0f }, { 2.5f, 3.0f, 10.0f }); AddBlock("wall", { 36.5f, 0.0f, -12.0f }, { 1.0f, 9.0f, 10.0f }, true); // Stacked boxes, front right AddBlock("box", { 30.0f, 0.0f, 12.0f }, { 6.0f, 2.5f, 6.0f }); AddBlock("box", { 34.5f, 0.0f, 15.0f }, { 5.0f, 5.0f, 5.0f }); // Long wall with a small block at its end, left front AddBlock("wall", { -33.0f, 0.0f, 9.0f }, { 1.0f, 7.0f, 18.0f }, true); AddBlock("box", { -30.0f, 0.0f, 17.5f }, { 3.0f, 2.0f, 3.0f }); // Small box cluster, back left AddBlock("box", { -26.0f, 0.0f, -12.0f }, { 6.0f, 2.0f, 6.0f }); AddBlock("box", { -22.0f, 0.0f, -15.5f }, { 6.0f, 1.0f, 4.0f }); // Ground check auto IsGrounded = [&]() -> bool { Ray downRay; downRay.origin = testcharacter->body->t.GetTranslation(); downRay.direction = glm::vec3(0.0f, -1.0f, 0.0f); const float halfHeight = testcharacter->body->t.GetScale().y * 0.5f; for (p* obj : physicsengine->GetObjects()) { if (obj->pointer == testcharacter->body) continue; if (!obj->collision) continue; auto hit = IsRayInT(downRay, obj->pointer->t); if (hit.has_value()) { if (glm::distance(downRay.origin, hit.value()) < halfHeight + 0.15f) return true; } } return false; }; // Hitscan shot from the camera. Head = top 20% of the target's box = instant kill, // body = 50 damage (two shots). Respawns the target when it dies. // Movement tuning const float airControl = 0.5f; // fraction of ground acceleration available in the air const float grappleRange = 8.0f; // metres from the player const float grappleSpeed = 28.0f; // launch speed toward the hit point const float grappleCooldownTime = 2.5f; // seconds float grappleCooldown = 0.0f; // Slide (key Left Ctrl): locked direction, committed for slideDuration, speed eases from // slideBoost x ground top speed down to 1x by slideFalloffTime. const float slideBoost = 2.0f; const float slideDuration = 1.2f; const float slideFalloffTime = 1.05f; const float slideDropY = 0.65f; // standing centre height (0.9) minus lying (0.25) const float standEyeOffset = 0.7f; // eyes 0.7 above body centre (body centre 0.9, height 1.8) const float slideEyeOffset = 0.25f; // lying down, eyes just above the floor float eyeOffset = standEyeOffset; bool sliding = false; float slideTime = 0.0f; glm::vec3 slideDir(0.0f); // Shockwave (key Q): projectile that explodes on the first surface it touches. The blast // launches the player, the NPC and loose props away from the impact point. No damage. const float shockSpeed = 30.0f; const float shockGravity = 6.0f; const float shockLifetime = 3.0f; const float shockRadius = 7.0f; const float shockPower = 26.0f; // launch speed at the centre of the blast const float shockCooldownTime = 6.0f; const float blastShowTime = 0.25f; const glm::vec3 hiddenPos(0.0f, -1000.0f, 0.0f); float shockCooldown = 0.0f; bool shockActive = false; float shockAge = 0.0f; float blastTime = 0.0f; glm::vec3 shockVel(0.0f); Mesh* shockMesh = CreateCubeMesh(); shockMesh->name = "shockwave"; shockMesh->meshTexture = blanktex; shockMesh->t.ScaleTo({ 0.3f, 0.3f, 0.3f }); shockMesh->t.TranslateTo(hiddenPos); shockMesh->BindToRenderSystem(); mainf->AddChild(shockMesh); Mesh* blastMesh = CreateCubeMesh(); blastMesh->name = "shockblast"; blastMesh->meshTexture = blanktex; blastMesh->t.TranslateTo(hiddenPos); blastMesh->BindToRenderSystem(); mainf->AddChild(blastMesh); // fromNet = another player's blast: we only launch ourselves (the other machines launch // themselves the same way, since remote players are just puppets on this machine). // (Lambda default arguments can't reference locals, so callers pass radius/power explicitly.) auto Explode = [&](const glm::vec3& center, bool fromNet, float radius, float power) { auto launch = [&](const glm::vec3& pos, p* body) { const glm::vec3 offset = pos - center; const float dist = glm::length(offset); if (dist >= radius) return; // Away from the blast, biased upward so grounded targets actually lift off glm::vec3 dir = dist > 0.001f ? offset / dist : glm::vec3(0.0f, 1.0f, 0.0f); dir.y = std::max(dir.y, 0.0f) + 0.6f; dir = glm::normalize(dir); const float falloff = 1.0f - 0.7f * (dist / radius); // 100% at centre, 30% at the edge body->linearvelocity += dir * power * falloff; body->WakeUp(); if (body == characterBody && sliding) { // The slide overrides horizontal velocity every frame, so end it and stand back up sliding = false; glm::vec3 p0 = testcharacter->body->t.GetTranslation(); p0.y += slideDropY + 0.02f; testcharacter->body->t.TranslateTo(p0); } }; launch(testcharacter->body->t.GetTranslation(), characterBody); if (!fromNet) { launch(npc->body->t.GetTranslation(), npcBody); for (p* obj : physicsengine->GetObjects()) { if (!obj->velocity || obj == characterBody || obj == npcBody) continue; launch(obj->pointer->t.GetTranslation(), obj); } NetBlast nb; nb.center = center; nb.radius = radius; nb.power = power; if (lan && lan->IsActive()) lan->SendBlast(nb); shockActive = false; shockMesh->t.TranslateTo(hiddenPos); } blastMesh->t.TranslateTo(center); blastMesh->t.ScaleTo(glm::vec3(0.5f)); blastTime = blastShowTime; camera->AddScreenshake(0.25f); }; const int maxHealth = 100; const int bodyDamage = 50; const int punchDamage = 25; const float headFraction = 0.8f; // Random respawn spot on the floor (NPC and player), kept clear of every map prop std::mt19937 respawnRng(std::random_device{}()); const float respawnHeight = 10.0f; // respawn in the air and drop to the floor auto RandomSpawn = [&]() -> glm::vec3 { std::uniform_real_distribution spreadX(-37.0f * mapScale, 37.0f * mapScale), spreadZ(-17.0f * mapScale, 17.0f * mapScale); const float margin = 1.5f; for (;;) { const float x = spreadX(respawnRng), z = spreadZ(respawnRng); bool blocked = false; for (const Footprint& f : propFootprints) { if (x > f.minX - margin && x < f.maxX + margin && z > f.minZ - margin && z < f.maxZ + margin) { blocked = true; break; } } if (blocked) continue; return { x, respawnHeight, z }; } }; auto Shoot = [&]() { Ray ray; ray.origin = camera->t.GetTranslation(); ray.direction = glm::normalize(camera->t.GetFrontVector()); // Nearest shootable target along the ray: the NPC or any remote (LAN) player float targetDist = 1e9f; glm::vec3 targetPoint(0.0f); t* targetT = nullptr; int targetRemoteId = -1; // -1 = the NPC auto consider = [&](t& tr, int remoteId) { auto h = IsRayInT(ray, tr); if (!h.has_value()) return; const float d = glm::distance(ray.origin, h.value()); if (d < targetDist) { targetDist = d; targetPoint = h.value(); targetT = &tr; targetRemoteId = remoteId; } }; consider(npc->body->t, -1); for (auto& kv : remotes) consider(kv.second.character->body->t, kv.first); if (!targetT) return; // Walls/props in front of the target block the shot for (p* obj : physicsengine->GetObjects()) { if (obj->pointer == testcharacter->body || obj->pointer == npc->body) continue; if (!obj->collision) continue; auto blocked = IsRayInT(ray, obj->pointer->t); if (blocked.has_value() && glm::distance(ray.origin, blocked.value()) < targetDist) return; } const float heightFrac = (targetPoint.y - targetT->GetTranslation().y) / targetT->GetScale().y + 0.5f; const bool headshot = heightFrac >= headFraction; const int damage = headshot ? maxHealth : bodyDamage; camera->AddScreenshake(0.05f); ShowHitMarker(headshot); if (targetRemoteId >= 0) { // Another player: their machine owns their health, so tell it NetHit hit; hit.damage = damage; if (lan && lan->IsActive()) lan->SendHit((uint8_t)targetRemoteId, hit); std::cout << (headshot ? "HEADSHOT" : "BODY SHOT") << " - player " << targetRemoteId << "\n"; return; } npc->body->Health -= damage; std::cout << (headshot ? "HEADSHOT" : "BODY SHOT") << " - NPC HP " << npc->body->Health << "\n"; if (npc->body->Health <= 0) { npc->body->Health = maxHealth; npc->body->t.TranslateTo(RandomSpawn()); npcBody->linearvelocity = glm::vec3(0.0f); npcBody->WakeUp(); } }; // ======================================================================== // Game loop // ======================================================================== while (!glfwWindowShouldClose(window->handle)) { frame++; const float deltatime = 1.0f / float(fps); const glm::vec3 camFront = camera->t.GetFrontVector(); const glm::vec3 camRight = camera->t.GetRightVector(); const float walkScale = 0.5f; // WASD speed relative to the original 3.9 const float movespeed = 3.0f * 1.3f * walkScale; // -------------------------------------------------------------------- // Input � mouse // -------------------------------------------------------------------- while (Mouse::Event buffer = mouse->Read()) { if (buffer.GetType() == Mouse::Event::Type::Move) { float xoffset = mouse->GetX() - mouse->GetLastX(); float yoffset = mouse->GetLastY() - mouse->GetY(); camera->ProcessMouseMovement(xoffset, yoffset); } else if (buffer.GetType() == Mouse::Event::Type::LPress) { Shoot(); } else if (buffer.GetType() == Mouse::Event::Type::RPress) { camera->lockedcursor = !camera->lockedcursor; } } // -------------------------------------------------------------------- // Input � held movement // -------------------------------------------------------------------- glm::vec3 inputVel = glm::vec3(0.0f); if (keyboard->IsKeyDown('W')) inputVel += glm::normalize(glm::vec3(camFront.x, 0.0f, camFront.z)); if (keyboard->IsKeyDown('S')) inputVel -= glm::normalize(glm::vec3(camFront.x, 0.0f, camFront.z)); if (keyboard->IsKeyDown('A')) inputVel += glm::normalize(glm::vec3(camRight.x, 0.0f, camRight.z)); if (keyboard->IsKeyDown('D')) inputVel -= glm::normalize(glm::vec3(camRight.x, 0.0f, camRight.z)); if (glm::length(inputVel) > 0.0f) inputVel = glm::normalize(inputVel) * movespeed; // -------------------------------------------------------------------- // Input � keyboard events // -------------------------------------------------------------------- while (Keyboard::Event buffer = keyboard->ReadKey()) { if (buffer.GetCode() == 'E' && buffer.IsPress()) { Mesh* nc = CreateCubeMesh(); nc->name = "cube!"; nc->t.ScaleTo(1.0f); nc->t.TranslateTo(camera->t.GetTranslation()); nc->t.RotateByEulerAngles({ 0.0f, 0.0f, 0.0f }); nc->BindToRenderSystem(); mainf->AddChild(nc); physicsengine->AddObject(new p(nc)); } if (buffer.GetCode() == 'F' && buffer.IsPress()) { const glm::vec3 playerPos = testcharacter->body->t.GetTranslation(); const glm::vec3 playerFront = glm::normalize(glm::vec3(camFront.x, 0.0f, camFront.z)); // Restart punch on high-priority animator punchAnimator->m_CurrentTime = 0.0f; punchAnimator->PlayAnimation(); debugcube->t.TranslateTo(playerPos + playerFront * 0.8f); debugcube->t.ScaleTo({ 0.8f, 0.8f, 0.8f }); debugcube->t.RotateToQuaternion(LookAt(-playerFront)); std::optional hit = TInT(debugcube->t, npc->body->t); if (hit.has_value()) { std::cout << "HIT NPC\n"; npcBody->WakeUp(); npcBody->linearvelocity += playerFront * 5.0f + glm::vec3(0.0f, 3.0f, 0.0f); camera->AddScreenshake(0.15f); // landing a punch feels impactful ShowHitMarker(false); npc->body->Health -= punchDamage; std::cout << "PUNCH - NPC HP " << npc->body->Health << "\n"; if (npc->body->Health <= 0) { npc->body->Health = maxHealth; npc->body->t.TranslateTo(RandomSpawn()); npcBody->linearvelocity = glm::vec3(0.0f); npcBody->WakeUp(); } } else { std::cout << "MISSED\n"; } // Punch LAN players too: their machine applies the damage and knockback for (auto& kv : remotes) { if (!TInT(debugcube->t, kv.second.character->body->t).has_value()) continue; NetHit ph; ph.damage = punchDamage; ph.knockback = playerFront * 5.0f + glm::vec3(0.0f, 3.0f, 0.0f); if (lan && lan->IsActive()) lan->SendHit((uint8_t)kv.first, ph); camera->AddScreenshake(0.15f); ShowHitMarker(false); std::cout << "PUNCH - player " << kv.first << "\n"; } } if (buffer.GetCode() == GLFW_KEY_LEFT_SHIFT && buffer.IsPress() && !sliding) { glm::vec3 dashDir = glm::length(inputVel) > 0.0f ? glm::normalize(inputVel) : glm::normalize(glm::vec3(camFront.x, 0.0f, camFront.z)); characterBody->linearvelocity.x += dashDir.x * 18.0f; characterBody->linearvelocity.z += dashDir.z * 18.0f; camera->AddScreenshake(0.04f); } if (buffer.GetCode() == GLFW_KEY_LEFT_CONTROL && buffer.IsPress() && !sliding && IsGrounded()) { const glm::vec3 horizVel(characterBody->linearvelocity.x, 0.0f, characterBody->linearvelocity.z); if (glm::length(inputVel) > 0.0f || glm::length(horizVel) > 1.0f) { slideDir = glm::length(inputVel) > 0.0f ? glm::normalize(inputVel) : glm::normalize(horizVel); sliding = true; slideTime = 0.0f; // Lie down feet-first: tilt the model's up axis to point back along the slide. // Hitbox is the same box, rotated with it; drop it so the feet stay on the floor. const glm::vec3 tiltAxis = glm::normalize(glm::cross(slideDir, glm::vec3(0.0f, 1.0f, 0.0f))); testcharacter->body->t.RotateToQuaternion( glm::angleAxis(glm::radians(90.0f), tiltAxis) * LookAt(-slideDir)); glm::vec3 pos = testcharacter->body->t.GetTranslation(); pos.y -= slideDropY; testcharacter->body->t.TranslateTo(pos); camera->AddScreenshake(0.06f); } } if (buffer.GetCode() == 'R' && buffer.IsPress() && !sliding && grappleCooldown <= 0.0f) { // Short-range grapple: aim with the camera, latch onto a surface within range Ray ray; ray.origin = camera->t.GetTranslation(); ray.direction = glm::normalize(camFront); float bestDist = grappleRange; glm::vec3 bestPoint(0.0f); bool found = false; for (p* obj : physicsengine->GetObjects()) { if (obj->pointer == testcharacter->body || obj->pointer == npc->body) continue; if (!obj->collision) continue; auto hit = IsRayInT(ray, obj->pointer->t); if (!hit.has_value()) continue; const float d = glm::distance(ray.origin, hit.value()); if (d < bestDist) { bestDist = d; bestPoint = hit.value(); found = true; } } if (found) { characterBody->linearvelocity = glm::normalize(bestPoint - ray.origin) * grappleSpeed; characterBody->WakeUp(); grappleCooldown = grappleCooldownTime; camera->AddScreenshake(0.1f); } } if (buffer.GetCode() == 'Q' && buffer.IsPress() && !shockActive && shockCooldown <= 0.0f) { const glm::vec3 front = glm::normalize(camFront); shockMesh->t.TranslateTo(camera->t.GetTranslation() + front * 0.6f); shockVel = front * shockSpeed; shockActive = true; shockAge = 0.0f; shockCooldown = shockCooldownTime; camera->AddScreenshake(0.05f); } if (buffer.GetCode() == 'G' && buffer.IsPress()) { camera->AddScreenshake(0.2f); } if (buffer.GetCode() == GLFW_KEY_SPACE && buffer.IsPress()) { if (!sliding && IsGrounded()) characterBody->linearvelocity.y = 12.0f; } } // -------------------------------------------------------------------- // Input � held keys // -------------------------------------------------------------------- if (keyboard->IsKeyDown(GLFW_KEY_ESCAPE)) glfwSetWindowShouldClose(window->handle, 1); // Movement + damping grappleCooldown = std::max(grappleCooldown - deltatime, 0.0f); const float accel = 40.0f; const float damp = 0.75f; const float groundTopSpeed = movespeed * accel * deltatime * damp / (1.0f - damp); if (sliding) { // Direction is locked; speed eases from boost x down to 1x (quadratic: drops fastest early) slideTime += deltatime; const float u = glm::clamp(slideTime / slideFalloffTime, 0.0f, 1.0f); const float speed = (groundTopSpeed / walkScale) * (1.0f + (slideBoost - 1.0f) * (1.0f - u) * (1.0f - u)); characterBody->linearvelocity.x = slideDir.x * speed; characterBody->linearvelocity.z = slideDir.z * speed; if (slideTime >= slideDuration) { sliding = false; glm::vec3 pos = testcharacter->body->t.GetTranslation(); pos.y += slideDropY + 0.02f; // stand back up (rotation is reset below) testcharacter->body->t.TranslateTo(pos); } } else if (IsGrounded()) { characterBody->linearvelocity.x *= damp; characterBody->linearvelocity.z *= damp; characterBody->linearvelocity.x += inputVel.x * accel * deltatime; characterBody->linearvelocity.z += inputVel.z * accel * deltatime; } else if (glm::length(inputVel) > 0.0f) { // Air: no ground drag so momentum (grapple, dash) carries. Input only adds // speed along the wish direction up to movespeed, at airControl of ground accel. const glm::vec3 wishDir = glm::normalize(inputVel); const float current = glm::dot(glm::vec3(characterBody->linearvelocity.x, 0.0f, characterBody->linearvelocity.z), wishDir); const float add = glm::clamp(groundTopSpeed - current, 0.0f, accel * airControl * movespeed * deltatime); characterBody->linearvelocity.x += wishDir.x * add; characterBody->linearvelocity.z += wishDir.z * add; } // -------------------------------------------------------------------- // Scene updates // -------------------------------------------------------------------- { const glm::vec3 playerPos = testcharacter->body->t.GetTranslation(); const glm::vec3 playerFront = glm::normalize(glm::vec3(camFront.x, 0.0f, camFront.z)); debugcube->t.TranslateTo(playerPos + playerFront * 0.8f); debugcube->t.ScaleTo({ 0.8f, 0.8f, 0.8f }); debugcube->t.RotateToQuaternion(LookAt(-playerFront)); } // Shockwave projectile: arc forward, explode on the first collider along this frame's path shockCooldown = std::max(shockCooldown - deltatime, 0.0f); if (shockActive) { shockVel.y -= shockGravity * deltatime; shockAge += deltatime; const glm::vec3 from = shockMesh->t.GetTranslation(); const glm::vec3 step = shockVel * deltatime; const float stepLen = glm::length(step); Ray ray; ray.origin = from; ray.direction = step / stepLen; float bestDist = stepLen; glm::vec3 bestPoint(0.0f); bool found = false; for (p* obj : physicsengine->GetObjects()) { if (obj == characterBody || !obj->collision) continue; auto hit = IsRayInT(ray, obj->pointer->t); if (!hit.has_value()) continue; const float d = glm::distance(from, hit.value()); if (d <= bestDist) { bestDist = d; bestPoint = hit.value(); found = true; } } if (found) Explode(bestPoint, false, shockRadius, shockPower); else if (shockAge >= shockLifetime) { shockActive = false; shockMesh->t.TranslateTo(hiddenPos); } else shockMesh->t.TranslateTo(from + step); } if (blastTime > 0.0f) { blastTime -= deltatime; if (blastTime <= 0.0f) { blastMesh->t.TranslateTo(hiddenPos); } else { // Expand to the full blast radius over its lifetime const float u = 1.0f - blastTime / blastShowTime; blastMesh->t.ScaleTo(glm::vec3(0.5f + (shockRadius * 2.0f - 0.5f) * u)); } } rain->t.TranslateTo(camera->t.GetTranslation() + glm::vec3(0.0f, 20.0f, 0.0f)); for (int i = 0; i < 30; i++) rain->Emit(); if (!sliding) { testcharacter->body->t.RotateToQuaternion( LookAt(-glm::normalize(glm::vec3(camFront.x, 0.0f, camFront.z))) ); } // First person: camera at the eyes (eased down while sliding) eyeOffset += ((sliding ? slideEyeOffset : standEyeOffset) - eyeOffset) * 0.3f; camera->t.TranslateTo(testcharacter->body->t.GetTranslation() + glm::vec3(0.0f, eyeOffset, 0.0f)); Sound* music = static_cast(mainf->GetFirstChildOfName("music")); music->Update3DPosition(5.0f, 4.0f, 6.5f); // where the old floating cube was soundsystem->SetListenerPosition( camera->t.GetTranslation(), camera->t.GetFrontVector(), camera->t.GetUpVector() ); // -------------------------------------------------------------------- // 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"; } hitMarkerTime = std::max(hitMarkerTime - deltatime, 0.0f); for (Box* b : hitMarker) b->Opacity = hitMarkerTime / hitMarkerDuration; // Damage flash: any HP loss restarts a short green fade-out if (testcharacter->body->Health < lastPlayerHealth) damageFlashTime = damageFlashDuration; lastPlayerHealth = testcharacter->body->Health; damageFlashTime = std::max(damageFlashTime - deltatime, 0.0f); damageFlash->Opacity = 0.45f * (damageFlashTime / damageFlashDuration); // Player respawn: at 0 HP or after falling off the map, teleport to a random spot if (testcharacter->body->Health <= 0 || testcharacter->body->t.GetTranslation().y < -20.0f) { testcharacter->body->Health = maxHealth; testcharacter->body->t.TranslateTo(RandomSpawn()); characterBody->linearvelocity = glm::vec3(0.0f); characterBody->WakeUp(); sliding = false; } hpText->text = std::to_string(std::max(testcharacter->body->Health, 0)) + "/100 HP"; grappleText->text = grappleCooldown > 0.0f ? "GRAPPLE " + std::to_string(grappleCooldown).substr(0, 3) + "s" : "GRAPPLE READY"; grappleText->text += shockCooldown > 0.0f ? " | SHOCKWAVE " + std::to_string(shockCooldown).substr(0, 3) + "s" : " | SHOCKWAVE READY (Q)"; // -------------------------------------------------------------------- // Step systems // -------------------------------------------------------------------- camera->Step(deltatime); rain->Step(deltatime); testcharacter->Step(deltatime); npc->Step(deltatime); soundsystem->Update(); physicsengine->Step(deltatime); // -------------------------------------------------------------------- // LAN: send our transform, apply the other player's // -------------------------------------------------------------------- if (lan && lan->IsActive()) { NetState mine; mine.position = testcharacter->body->t.GetTranslation(); mine.rotation = testcharacter->body->t.GetRotationQuaternion(); mine.sliding = sliding; lan->Send(mine); const double now = glfwGetTime(); std::vector updates; std::vector blasts; std::vector hits; lan->Receive(updates, &blasts, &hits); for (const NetBlast& b : blasts) Explode(b.center, true, b.radius, b.power); // Damage from other players (respawn handles dying, see the player respawn check) for (const NetHit& h : hits) { testcharacter->body->Health -= h.damage; characterBody->linearvelocity += h.knockback; characterBody->WakeUp(); camera->AddScreenshake(0.15f); std::cout << "HIT by player " << (int)h.sender << " for " << h.damage << " - HP " << testcharacter->body->Health << "\n"; } for (const NetUpdate& u : updates) { auto it = remotes.find(u.id); if (it == remotes.end()) { // First packet from this player: spawn them exactly at their pose RemotePlayer rp; rp.character = SpawnRemote(); rp.character->body->t.TranslateTo(u.state.position); rp.character->body->t.RotateToQuaternion(u.state.rotation); it = remotes.emplace(u.id, rp).first; } else { // Smooth over the 60 Hz updates t& rt = it->second.character->body->t; rt.TranslateTo(glm::mix(rt.GetTranslation(), u.state.position, 0.5f)); rt.RotateToQuaternion(glm::slerp(rt.GetRotationQuaternion(), u.state.rotation, 0.5f)); } it->second.lastSeen = now; } // Drop players we haven't heard from in 2 s, animate the rest for (auto it = remotes.begin(); it != remotes.end();) { if (now - it->second.lastSeen > 2.0) { rendersystem->RemoveRenderable(it->second.character); delete it->second.character; it = remotes.erase(it); } else { it->second.character->Step(deltatime); ++it; } } if (lan->GetMode() == Lan::Mode::Host) netText->text = "LAN: hosting - " + std::to_string(lan->PeerCount() + 1) + "/" + std::to_string(Lan::MaxPlayers) + " players"; else netText->text = lan->IsConnected() ? "LAN: connected - " + std::to_string(remotes.size() + 1) + " players" : "LAN: joining..."; } // -------------------------------------------------------------------- // Render // -------------------------------------------------------------------- rendersystem->Render(*camera); glfwPollEvents(); } } // ============================================================================ // GLFW callbacks // ============================================================================ void framebuffer_size_callback(GLFWwindow* window, int w, int h) { if (w <= 0 || h <= 0) return; // minimised // Everything pixel-based (viewport, projection aspect, GUI layout, scissor) reads these ::window->width = w; ::window->height = h; glViewport(0, 0, w, h); } void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { // F11: toggle fullscreen on the primary monitor, restoring the old window rect on exit if (key == GLFW_KEY_F11 && action == GLFW_PRESS) { static int wx = 100, wy = 100, ww = 1280, wh = 720; 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(1); } if (action == GLFW_PRESS) keyboard->KeyDown(key); else if (action == GLFW_RELEASE) keyboard->KeyUp(key); } void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) { } void mouse_callback(GLFWwindow* window, double xposIn, double yposIn) { mouse->OnMouseMove(static_cast(xposIn), static_cast(yposIn)); glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); // Cursor coordinates are in window units, which can differ from framebuffer pixels (DPI scaling) int ww, wh; glfwGetWindowSize(window, &ww, &wh); glfwSetCursorPos(window, ww / 2.0, wh / 2.0); mouse->OnMouseMove(ww / 2.0f, wh / 2.0f); } 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(); } }