1264 lines
58 KiB
C++
1264 lines
58 KiB
C++
#include "Engine.h"
|
|
|
|
// ============================================================================
|
|
// Standard library
|
|
// ============================================================================
|
|
#include <iostream>
|
|
#include <vector>
|
|
#include <random>
|
|
#include <map>
|
|
#include <string>
|
|
#include <memory>
|
|
#include <type_traits>
|
|
|
|
// ============================================================================
|
|
// 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 "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->SetGravity(physicsengine->GetGravity() * 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* shockwavetex = new Texture("assets/shockwave.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.7f, 2.5f, 0.7f });
|
|
testcharacter->body->t.TranslateTo({ 0.0f, 10.0f, 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.7f, 2.5f, 0.7f });
|
|
npc->body->t.TranslateTo({ 3.0f, 1.25f, 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;
|
|
p* body = nullptr; // host only: lets the host's cubes collide with this player
|
|
};
|
|
std::map<int, RemotePlayer> remotes;
|
|
auto SpawnRemote = [&]() -> Character* {
|
|
Character* c = new Character();
|
|
c->name = "remote";
|
|
c->castshadow = true;
|
|
c->body->t.ScaleBy({ 0.7f, 2.5f, 0.7f });
|
|
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<float> spread(-8.0f, 8.0f);
|
|
testcharacter->body->t.TranslateTo({ spread(rng), 10.0f, 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<Box*> 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, 110.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.6f;
|
|
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, 400.0f, 90.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();
|
|
|
|
// ------------------------------------------------------------------------
|
|
// 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<Footprint> 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)
|
|
// xStretch scales the stack along x only (left/right); height and depth are unchanged
|
|
auto AddTiers = [&](const char* name, glm::vec2 pos, float baseWidth, float tierHeight, int tiers, float xStretch = 1.0f) {
|
|
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 * xStretch, 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, 2.0f, 5, 0.5f);
|
|
// 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, 8.4f, 7.0f });
|
|
AddBlock("cube", { 24.0f, 0.0f, 0.0f }, { 7.0f, 8.4f, 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 shockedAirControl = 0.1f; // air control after being hit by another player's shockwave
|
|
const float shockedDuration = 2.0f; // seconds the reduced air control lasts
|
|
float shockedTime = 0.0f;
|
|
const float grappleRange = 30.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 = 1.5f;
|
|
const float slideDuration = 0.8f;
|
|
const float slideFalloffTime = 0.7f;
|
|
const float slideDropY = 0.65f; // standing centre height (0.9) minus lying (0.25)
|
|
const float standEyeOffset = 1.05f; // eyes 1.05 above body centre (body centre 1.25, height 2.5)
|
|
const float slideEyeOffset = 0.25f; // lying down, eyes just above the floor
|
|
float eyeOffset = standEyeOffset;
|
|
bool sliding = false;
|
|
float slideTime = 0.0f;
|
|
const float slideCooldownTime = 1.0f; // seconds after a slide ends before the next
|
|
float slideCooldown = 0.0f;
|
|
const float dashCooldownTime = 1.5f; // seconds between dashes
|
|
float dashCooldown = 0.0f;
|
|
const float shootCooldownTime = 0.5f; // seconds between shots
|
|
float shootCooldown = 0.0f;
|
|
|
|
// Cubes (E). Whoever hosts (or an offline player) owns and simulates them and decides when they
|
|
// despawn; LAN clients only ask the host for one and draw the poses it streams back.
|
|
const float cubeCooldownTime = 0.5f; // seconds between cubes per player
|
|
const double cubeLifetime = 30.0; // seconds before a cube despawns
|
|
float cubeCooldown = 0.0f;
|
|
// Cubes also rain from the sky on their own (host / offline only; clients see them via the stream)
|
|
const float cubeRainInterval = 0.5f; // seconds between rain cubes
|
|
const float cubeRainHeight = 45.0f;
|
|
const size_t cubeRainMax = 200; // stop raining while this many cubes exist
|
|
float cubeRainTimer = 0.0f;
|
|
struct HostCube { uint32_t id; Mesh* mesh; double spawnTime; };
|
|
std::vector<HostCube> hostCubes;
|
|
uint32_t nextCubeId = 0;
|
|
std::map<uint8_t, double> lastCubeSpawn; // host: when each client last spawned one (rate limit)
|
|
struct PuppetCube { Mesh* mesh; double lastSeen; };
|
|
std::map<uint32_t, PuppetCube> puppetCubes; // client: the host's cubes, keyed by host id
|
|
auto SpawnCube = [&](const glm::vec3& at) {
|
|
Mesh* nc = CreateCubeMesh();
|
|
nc->name = "cube!";
|
|
nc->t.ScaleTo(1.0f);
|
|
nc->t.TranslateTo(at);
|
|
nc->BindToRenderSystem();
|
|
mainf->AddChild(nc);
|
|
physicsengine->AddObject(new p(nc));
|
|
hostCubes.push_back({ ++nextCubeId, nc, glfwGetTime() });
|
|
};
|
|
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 = shockwavetex;
|
|
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();
|
|
|
|
// Another player's blast (not our own) takes away most of our air control for a while
|
|
if (body == characterBody && fromNet) shockedTime = shockedDuration;
|
|
|
|
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);
|
|
}
|
|
else if (lan && lan->IsActive()) {
|
|
// Another player's blast: launch the cubes (host: the real ones; client: its local copies)
|
|
for (p* obj : physicsengine->GetObjects()) {
|
|
bool isCube = false;
|
|
for (const HostCube& hc : hostCubes) if (obj->pointer == hc.mesh) isCube = true;
|
|
for (auto& kv : puppetCubes) if (obj->pointer == kv.second.mesh) isCube = true;
|
|
if (isCube) launch(obj->pointer->t.GetTranslation(), obj);
|
|
}
|
|
}
|
|
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.93f; // top 7% of the hitbox counts as the head
|
|
|
|
// Random respawn spot on the floor (NPC and player), kept clear of every map prop
|
|
std::mt19937 respawnRng(std::random_device{}());
|
|
const float respawnHeight = 15.0f; // respawn in the air and drop to the floor
|
|
auto RandomSpawn = [&]() -> glm::vec3 {
|
|
std::uniform_real_distribution<float> 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++;
|
|
// Real frame time (clamped so a hitch can't tunnel through colliders), scaled so the game
|
|
// plays at the speed it had when it ran one 1/fps step per frame on a 144 Hz display.
|
|
// refDt is the step the movement constants were tuned at.
|
|
static double prevFrameTime = glfwGetTime();
|
|
const double frameNow = glfwGetTime();
|
|
const float deltatime = glm::clamp(float(frameNow - prevFrameTime), 0.001f, 1.0f / 60.0f) * (144.0f / float(fps));
|
|
prevFrameTime = frameNow;
|
|
const float refDt = 1.0f / float(fps);
|
|
|
|
const glm::vec3 camFront = camera->t.GetFrontVector();
|
|
const glm::vec3 camRight = camera->t.GetRightVector();
|
|
const float walkScale = 0.6f; // 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) {
|
|
if (shootCooldown <= 0.0f) {
|
|
shootCooldown = shootCooldownTime;
|
|
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() && cubeCooldown <= 0.0f) {
|
|
cubeCooldown = cubeCooldownTime;
|
|
if (lan && lan->IsActive() && lan->GetMode() == Lan::Mode::Client)
|
|
lan->SendCubeSpawn(camera->t.GetTranslation()); // the host spawns it
|
|
else
|
|
SpawnCube(camera->t.GetTranslation());
|
|
}
|
|
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<TInTInfo> 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 && dashCooldown <= 0.0f) {
|
|
dashCooldown = dashCooldownTime;
|
|
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 && slideCooldown <= 0.0f && 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;
|
|
slideCooldown = slideDuration + slideCooldownTime;
|
|
|
|
// 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 = 11.0f;
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------
|
|
// Input � held keys
|
|
// --------------------------------------------------------------------
|
|
if (keyboard->IsKeyDown(GLFW_KEY_ESCAPE))
|
|
glfwSetWindowShouldClose(window->handle, 1);
|
|
|
|
// Movement + damping
|
|
grappleCooldown = std::max(grappleCooldown - deltatime, 0.0f);
|
|
slideCooldown = std::max(slideCooldown - deltatime, 0.0f);
|
|
dashCooldown = std::max(dashCooldown - deltatime, 0.0f);
|
|
shootCooldown = std::max(shootCooldown - deltatime, 0.0f);
|
|
cubeCooldown = std::max(cubeCooldown - deltatime, 0.0f);
|
|
const float accel = 40.0f;
|
|
const float damp = 0.75f;
|
|
const float groundTopSpeed = movespeed * accel * refDt * 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()) {
|
|
// Drag and acceleration rescaled so the top speed is the same at any frame rate
|
|
const float dampStep = std::pow(damp, deltatime / refDt);
|
|
const float accelScale = refDt * damp * (1.0f - dampStep) / (deltatime * dampStep * (1.0f - damp));
|
|
characterBody->linearvelocity.x *= dampStep;
|
|
characterBody->linearvelocity.z *= dampStep;
|
|
characterBody->linearvelocity.x += inputVel.x * accel * deltatime * accelScale;
|
|
characterBody->linearvelocity.z += inputVel.z * accel * deltatime * accelScale;
|
|
}
|
|
else if (glm::length(inputVel) > 0.0f) {
|
|
// 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 * (shockedTime > 0.0f ? shockedAirControl : 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);
|
|
shockedTime = std::max(shockedTime - 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) * (1.0f - std::pow(0.7f, deltatime / refDt));
|
|
camera->t.TranslateTo(testcharacter->body->t.GetTranslation() + glm::vec3(0.0f, eyeOffset, 0.0f));
|
|
|
|
Sound* music = static_cast<Sound*>(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();
|
|
// Split into equal substeps of at most 1/120 s so jump arcs, falling and collisions
|
|
// come out the same whatever the frame rate
|
|
{
|
|
const int substeps = std::max(1, (int)std::ceil(deltatime * 120.0f));
|
|
const float subDt = deltatime / float(substeps);
|
|
for (int i = 0; i < substeps; i++) physicsengine->Step(subDt);
|
|
}
|
|
|
|
// --------------------------------------------------------------------
|
|
// LAN: send our transform, apply the other player's
|
|
// --------------------------------------------------------------------
|
|
// Rain cubes from the sky at random spots over the map (host / offline)
|
|
if (!lan || !lan->IsActive() || lan->GetMode() == Lan::Mode::Host) {
|
|
cubeRainTimer -= deltatime;
|
|
if (cubeRainTimer <= 0.0f) {
|
|
cubeRainTimer += cubeRainInterval;
|
|
if (hostCubes.size() < cubeRainMax) {
|
|
glm::vec3 at = RandomSpawn();
|
|
at.y = cubeRainHeight;
|
|
SpawnCube(at);
|
|
std::uniform_real_distribution<float> spin(0.0f, 6.28318f);
|
|
hostCubes.back().mesh->t.RotateToEulerAngles({ spin(respawnRng), spin(respawnRng), spin(respawnRng) });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Despawn cubes that have lived long enough (host / offline)
|
|
{
|
|
const double cubeNow = glfwGetTime();
|
|
for (size_t i = 0; i < hostCubes.size();) {
|
|
if (cubeNow - hostCubes[i].spawnTime > cubeLifetime) {
|
|
Mesh* m = hostCubes[i].mesh;
|
|
rendersystem->RemoveRenderable(m);
|
|
physicsengine->RemoveObject(m);
|
|
m->Delete();
|
|
hostCubes.erase(hostCubes.begin() + i);
|
|
}
|
|
else ++i;
|
|
}
|
|
}
|
|
|
|
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<NetUpdate> updates;
|
|
std::vector<NetBlast> blasts;
|
|
std::vector<NetHit> hits;
|
|
std::vector<NetCubeRequest> cubeRequests;
|
|
std::vector<NetCube> cubeStates;
|
|
lan->Receive(updates, &blasts, &hits, &cubeRequests, &cubeStates);
|
|
|
|
// Host: spawn the cubes clients asked for (rate limited per client), then stream every cube's pose
|
|
for (const NetCubeRequest& rq : cubeRequests) {
|
|
double& last = lastCubeSpawn[rq.sender];
|
|
if (now - last < cubeCooldownTime * 0.9) continue;
|
|
last = now;
|
|
SpawnCube(glm::clamp(rq.position, glm::vec3(-200.0f), glm::vec3(200.0f)));
|
|
}
|
|
if (lan->GetMode() == Lan::Mode::Host) {
|
|
std::vector<NetCube> snapshot;
|
|
snapshot.reserve(hostCubes.size());
|
|
for (const HostCube& hc : hostCubes) {
|
|
NetCube nc;
|
|
nc.id = hc.id;
|
|
nc.position = hc.mesh->t.GetTranslation();
|
|
nc.rotation = hc.mesh->t.GetRotationQuaternion();
|
|
snapshot.push_back(nc);
|
|
}
|
|
lan->SendCubes(snapshot);
|
|
}
|
|
|
|
// Client: show the host's cubes, and drop the ones it stopped sending (despawned)
|
|
for (const NetCube& nc : cubeStates) {
|
|
auto it = puppetCubes.find(nc.id);
|
|
if (it == puppetCubes.end()) {
|
|
Mesh* m = CreateCubeMesh();
|
|
m->name = "cube!";
|
|
m->t.ScaleTo(1.0f);
|
|
m->BindToRenderSystem();
|
|
mainf->AddChild(m);
|
|
// Simulated locally exactly like the host's cubes (so the local player pushes them with
|
|
// no lag); the host's stream below only corrects drift.
|
|
physicsengine->AddObject(new p(m));
|
|
m->t.TranslateTo(nc.position);
|
|
m->t.RotateToQuaternion(nc.rotation);
|
|
it = puppetCubes.emplace(nc.id, PuppetCube{ m, now }).first;
|
|
}
|
|
else {
|
|
t& ct = it->second.mesh->t;
|
|
const glm::vec3 err = nc.position - ct.GetTranslation();
|
|
if (glm::length(err) > 3.0f) {
|
|
ct.TranslateTo(nc.position);
|
|
ct.RotateToQuaternion(nc.rotation);
|
|
}
|
|
else {
|
|
ct.TranslateBy(err * 0.35f);
|
|
ct.RotateToQuaternion(glm::slerp(ct.GetRotationQuaternion(), nc.rotation, 0.35f));
|
|
}
|
|
}
|
|
it->second.lastSeen = now;
|
|
}
|
|
for (auto it = puppetCubes.begin(); it != puppetCubes.end();) {
|
|
if (now - it->second.lastSeen > 1.5) {
|
|
rendersystem->RemoveRenderable(it->second.mesh);
|
|
physicsengine->RemoveObject(it->second.mesh);
|
|
it->second.mesh->Delete();
|
|
it = puppetCubes.erase(it);
|
|
}
|
|
else ++it;
|
|
}
|
|
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);
|
|
if (lan->GetMode() == Lan::Mode::Host) {
|
|
// Solid for the host's cubes (kinematic: moved by the client's stream only)
|
|
rp.body = new p(rp.character->body);
|
|
rp.body->velocity = false;
|
|
physicsengine->AddObject(rp.body);
|
|
}
|
|
it = remotes.emplace(u.id, rp).first;
|
|
}
|
|
else {
|
|
// Smooth over the 60 Hz updates
|
|
t& rt = it->second.character->body->t;
|
|
const glm::vec3 before = rt.GetTranslation();
|
|
rt.TranslateTo(glm::mix(rt.GetTranslation(), u.state.position, 0.5f));
|
|
// Kinematic body: its velocity makes it push cubes like the client's own body does there
|
|
if (it->second.body && deltatime > 0.0f)
|
|
it->second.body->linearvelocity = (rt.GetTranslation() - before) / deltatime;
|
|
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);
|
|
if (it->second.body) physicsengine->RemoveObject(it->second.character->body);
|
|
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 = 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(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<float>(xposIn), static_cast<float>(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();
|
|
}
|
|
} |