second commit
This commit is contained in:
+260
-50
@@ -93,6 +93,11 @@ Engine::Engine()
|
||||
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();
|
||||
@@ -243,17 +248,6 @@ void Engine::Initiate()
|
||||
crosshair->Dot = true;
|
||||
crosshair->Apply();
|
||||
|
||||
TextBox* tb = new TextBox();
|
||||
tb->font = arial;
|
||||
tb->text = "ASDASD\nASDASD\nADSADSA";
|
||||
tb->fontsize = 0.5f;
|
||||
tb->Color = { 0.0f, 0.0f, 0.0f };
|
||||
tb->t2d.position = { 0.5f, 0.5f, 0.0f, 0.0f };
|
||||
tb->t2d.center = { 0.0f, 0.0f };
|
||||
tb->textCenter = { 0.0f, 0.5f };
|
||||
tb->t2d.size = { 0.2f, 0.1f, 0.0f, 0.0f };
|
||||
tb->BindToRenderSystem();
|
||||
|
||||
TextBox* fpsCounter = new TextBox();
|
||||
fpsCounter->font = arial;
|
||||
fpsCounter->text = "TEST1231231123";
|
||||
@@ -295,7 +289,7 @@ void Engine::Initiate()
|
||||
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, 300.0f, 40.0f };
|
||||
grappleText->t2d.size = { 0.0f, 0.0f, 560.0f, 40.0f };
|
||||
grappleText->BindToRenderSystem();
|
||||
|
||||
ImageBox* testgui = new ImageBox();
|
||||
@@ -325,7 +319,7 @@ void Engine::Initiate()
|
||||
Mesh* debugcube = CreateCubeMesh();
|
||||
debugcube->name = "debugcube";
|
||||
debugcube->t.ScaleTo({ 0.8f, 0.8f, 0.8f });
|
||||
debugcube->BindToRenderSystem();
|
||||
// Punch hitbox only: intentionally not bound to the render system, so it isn't drawn
|
||||
mainf->AddChild(debugcube);
|
||||
|
||||
Mesh* floor = CreateCubeMesh();
|
||||
@@ -352,22 +346,6 @@ void Engine::Initiate()
|
||||
wallBody->friction = 0.0f;
|
||||
physicsengine->AddObject(wallBody);
|
||||
|
||||
Mesh* cube = CreateCubeMesh();
|
||||
cube->name = "cube";
|
||||
cube->meshTexture = blanktex;
|
||||
cube->t.ScaleTo({ 1.0f, 1.0f, 1.0f });
|
||||
cube->t.TranslateTo({ 5.0f, 4.0f, 6.5f });
|
||||
cube->BindToRenderSystem();
|
||||
mainf->AddChild(cube);
|
||||
|
||||
Mesh* cube2 = CreateCubeMesh();
|
||||
cube2->name = "cubee";
|
||||
cube2->meshTexture = blanktex;
|
||||
cube2->t.ScaleTo({ 1.0f, 1.0f, 1.0f });
|
||||
cube2->t.TranslateTo({ 10.0f, 4.0f, 10.5f });
|
||||
cube2->BindToRenderSystem();
|
||||
mainf->AddChild(cube2);
|
||||
|
||||
// Ground check
|
||||
auto IsGrounded = [&]() -> bool {
|
||||
Ray downRay;
|
||||
@@ -390,16 +368,16 @@ void Engine::Initiate()
|
||||
// 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 = 4.0f; // metres from the player
|
||||
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 C): locked direction, committed for slideDuration, speed eases from
|
||||
// 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 = 0.8f;
|
||||
const float slideFalloffTime = 0.7f;
|
||||
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
|
||||
@@ -408,17 +386,119 @@ void Engine::Initiate()
|
||||
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).
|
||||
auto Explode = [&](const glm::vec3& center, bool fromNet = false,
|
||||
float radius = shockRadius, float power = shockPower) {
|
||||
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 out of the wall (x -9.5..-6.5, z -11..11)
|
||||
std::mt19937 respawnRng(std::random_device{}());
|
||||
auto RandomSpawn = [&]() -> glm::vec3 {
|
||||
std::uniform_real_distribution<float> spread(-20.0f, 20.0f);
|
||||
for (;;) {
|
||||
const float x = spread(respawnRng), z = spread(respawnRng);
|
||||
if (x > -9.5f && x < -6.5f && z > -11.0f && z < 11.0f) continue;
|
||||
return { x, 0.9f, z };
|
||||
}
|
||||
};
|
||||
|
||||
auto Shoot = [&]() {
|
||||
Ray ray;
|
||||
ray.origin = camera->t.GetTranslation();
|
||||
ray.direction = glm::normalize(camera->t.GetFrontVector());
|
||||
|
||||
auto targetHit = IsRayInT(ray, npc->body->t);
|
||||
if (!targetHit.has_value()) return;
|
||||
const float targetDist = glm::distance(ray.origin, targetHit.value());
|
||||
// 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()) {
|
||||
@@ -429,16 +509,27 @@ void Engine::Initiate()
|
||||
return;
|
||||
}
|
||||
|
||||
const float heightFrac = (targetHit->y - npc->body->t.GetTranslation().y)
|
||||
/ npc->body->t.GetScale().y + 0.5f;
|
||||
const float heightFrac = (targetPoint.y - targetT->GetTranslation().y)
|
||||
/ targetT->GetScale().y + 0.5f;
|
||||
const bool headshot = heightFrac >= headFraction;
|
||||
npc->body->Health -= headshot ? maxHealth : bodyDamage;
|
||||
std::cout << (headshot ? "HEADSHOT" : "BODY SHOT") << " - NPC HP " << npc->body->Health << "\n";
|
||||
const int damage = headshot ? maxHealth : bodyDamage;
|
||||
camera->AddScreenshake(0.05f);
|
||||
|
||||
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({ 3.0f, 0.9f, 3.0f });
|
||||
npc->body->t.TranslateTo(RandomSpawn());
|
||||
npcBody->linearvelocity = glm::vec3(0.0f);
|
||||
npcBody->WakeUp();
|
||||
}
|
||||
@@ -454,7 +545,8 @@ void Engine::Initiate()
|
||||
|
||||
const glm::vec3 camFront = camera->t.GetFrontVector();
|
||||
const glm::vec3 camRight = camera->t.GetRightVector();
|
||||
const float movespeed = 3.0f * 1.3f;
|
||||
const float walkScale = 0.5f; // WASD speed relative to the original 3.9
|
||||
const float movespeed = 3.0f * 1.3f * walkScale;
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Input � mouse
|
||||
@@ -516,10 +608,30 @@ void Engine::Initiate()
|
||||
npcBody->WakeUp();
|
||||
npcBody->linearvelocity += playerFront * 5.0f + glm::vec3(0.0f, 3.0f, 0.0f);
|
||||
camera->AddScreenshake(0.15f); // landing a punch feels impactful
|
||||
|
||||
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);
|
||||
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
|
||||
@@ -529,7 +641,7 @@ void Engine::Initiate()
|
||||
characterBody->linearvelocity.z += dashDir.z * 18.0f;
|
||||
camera->AddScreenshake(0.04f);
|
||||
}
|
||||
if (buffer.GetCode() == 'C' && buffer.IsPress() && !sliding && IsGrounded()) {
|
||||
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);
|
||||
@@ -570,12 +682,21 @@ void Engine::Initiate()
|
||||
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 = 5.0f;
|
||||
characterBody->linearvelocity.y = 12.0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +715,7 @@ void Engine::Initiate()
|
||||
// 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 * (1.0f + (slideBoost - 1.0f) * (1.0f - u) * (1.0f - u));
|
||||
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;
|
||||
|
||||
@@ -634,6 +755,48 @@ void Engine::Initiate()
|
||||
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);
|
||||
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();
|
||||
|
||||
@@ -647,8 +810,7 @@ void Engine::Initiate()
|
||||
camera->t.TranslateTo(testcharacter->body->t.GetTranslation() + glm::vec3(0.0f, eyeOffset, 0.0f));
|
||||
|
||||
Sound* music = static_cast<Sound*>(mainf->GetFirstChildOfName("music"));
|
||||
Mesh* cube = static_cast<Mesh*>(mainf->GetFirstChildOfName("cube"));
|
||||
music->Update3DPosition(cube->t.GetTranslation());
|
||||
music->Update3DPosition(5.0f, 4.0f, 6.5f); // where the old floating cube was
|
||||
|
||||
soundsystem->SetListenerPosition(
|
||||
camera->t.GetTranslation(),
|
||||
@@ -675,10 +837,21 @@ void Engine::Initiate()
|
||||
}
|
||||
fpsCounter->text = std::to_string(measuredFps) + " FPS";
|
||||
}
|
||||
// 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
|
||||
@@ -702,7 +875,20 @@ void Engine::Initiate()
|
||||
|
||||
const double now = glfwGetTime();
|
||||
std::vector<NetUpdate> updates;
|
||||
lan->Receive(updates);
|
||||
std::vector<NetBlast> blasts;
|
||||
std::vector<NetHit> 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()) {
|
||||
@@ -757,10 +943,31 @@ void Engine::Initiate()
|
||||
// ============================================================================
|
||||
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);
|
||||
}
|
||||
@@ -773,8 +980,11 @@ 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);
|
||||
glfwSetCursorPos(window, ::window->width / 2, ::window->height / 2);
|
||||
mouse->OnMouseMove(::window->width / 2.0f, ::window->height / 2.0f);
|
||||
// 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)
|
||||
|
||||
@@ -225,9 +225,9 @@ class Crosshair {
|
||||
public:
|
||||
glm::vec3 Color = { 0.0f, 1.0f, 0.0f };
|
||||
float Opacity = 1.0f;
|
||||
float Length = 8.0f; // arm length
|
||||
float Thickness = 2.0f; // arm width
|
||||
float Gap = 4.0f; // distance from center to the start of each arm
|
||||
float Length = 11.2f; // arm length
|
||||
float Thickness = 2.8f; // arm width
|
||||
float Gap = 5.6f; // distance from center to the start of each arm
|
||||
bool Dot = false; // center dot
|
||||
bool Visible = true;
|
||||
|
||||
|
||||
@@ -38,6 +38,35 @@ namespace {
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
constexpr uint32_t NET_MAGIC_BLAST = 0x45494942; // "EIIB"
|
||||
constexpr int BLAST_COPIES = 4; // each blast is sent this many times
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct BlastPacket {
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
uint8_t sender;
|
||||
uint32_t id;
|
||||
float center[3];
|
||||
float radius;
|
||||
float power;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
constexpr uint32_t NET_MAGIC_HIT = 0x45494948; // "EIIH"
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct HitPacket {
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
uint8_t sender;
|
||||
uint8_t target;
|
||||
uint32_t id;
|
||||
int16_t damage;
|
||||
float knock[3];
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
constexpr int MAX_ENTRIES = Lan::MaxPlayers;
|
||||
constexpr int MAX_DATAGRAM = (int)sizeof(Header) + MAX_ENTRIES * (int)sizeof(Entry);
|
||||
|
||||
@@ -92,6 +121,33 @@ struct Lan::Impl {
|
||||
uint32_t lastSeq = 0;
|
||||
double lastRecvTime = 0.0;
|
||||
|
||||
// Blasts waiting for their remaining copies to go out
|
||||
struct PendingBlast { BlastPacket pkt; int copiesLeft; };
|
||||
std::vector<PendingBlast> pendingBlasts;
|
||||
uint32_t blastSeq = 0;
|
||||
std::vector<uint64_t> seenBlasts; // (type, sender, id) of recent blasts/hits, for de-duplication
|
||||
|
||||
struct PendingHit { HitPacket pkt; int copiesLeft; };
|
||||
std::vector<PendingHit> pendingHits;
|
||||
uint32_t hitSeq = 0;
|
||||
|
||||
void SendHitPacket(const sockaddr_in& to, const HitPacket& pkt) {
|
||||
sendto(sock, (const char*)&pkt, (int)sizeof(pkt), 0, (const sockaddr*)&to, (int)sizeof(to));
|
||||
}
|
||||
|
||||
// True the first time a (type, sender, id) triple is seen. type: 0 = blast, 1 = hit.
|
||||
bool FirstSighting(uint8_t type, uint8_t sender, uint32_t id) {
|
||||
const uint64_t key = ((uint64_t)type << 40) | ((uint64_t)sender << 32) | id;
|
||||
if (std::find(seenBlasts.begin(), seenBlasts.end(), key) != seenBlasts.end()) return false;
|
||||
seenBlasts.push_back(key);
|
||||
if (seenBlasts.size() > 64) seenBlasts.erase(seenBlasts.begin());
|
||||
return true;
|
||||
}
|
||||
|
||||
void SendBlastPacket(const sockaddr_in& to, const BlastPacket& pkt) {
|
||||
sendto(sock, (const char*)&pkt, (int)sizeof(pkt), 0, (const sockaddr*)&to, (int)sizeof(to));
|
||||
}
|
||||
|
||||
void SendDatagram(const sockaddr_in& to, const std::vector<Entry>& entries) {
|
||||
char buf[MAX_DATAGRAM];
|
||||
Header h;
|
||||
@@ -174,14 +230,68 @@ void Lan::Stop() {
|
||||
impl->wsaStarted = false;
|
||||
}
|
||||
impl->peers.clear();
|
||||
impl->pendingBlasts.clear();
|
||||
impl->pendingHits.clear();
|
||||
impl->seenBlasts.clear();
|
||||
impl->anyRecv = false;
|
||||
active = false;
|
||||
mode = Mode::Off;
|
||||
}
|
||||
|
||||
void Lan::SendBlast(const NetBlast& b) {
|
||||
if (!active) return;
|
||||
Impl::PendingBlast pb;
|
||||
pb.pkt.magic = NET_MAGIC_BLAST;
|
||||
pb.pkt.version = NET_VERSION;
|
||||
pb.pkt.sender = 0; // the host stamps the real id when relaying a client's blast
|
||||
pb.pkt.id = ++impl->blastSeq;
|
||||
pb.pkt.center[0] = b.center.x; pb.pkt.center[1] = b.center.y; pb.pkt.center[2] = b.center.z;
|
||||
pb.pkt.radius = b.radius;
|
||||
pb.pkt.power = b.power;
|
||||
pb.copiesLeft = BLAST_COPIES;
|
||||
impl->pendingBlasts.push_back(pb);
|
||||
}
|
||||
|
||||
void Lan::SendHit(uint8_t target, const NetHit& h) {
|
||||
if (!active) return;
|
||||
Impl::PendingHit ph;
|
||||
ph.pkt.magic = NET_MAGIC_HIT;
|
||||
ph.pkt.version = NET_VERSION;
|
||||
ph.pkt.sender = 0; // the host stamps the real id when relaying a client's hit
|
||||
ph.pkt.target = target;
|
||||
ph.pkt.id = ++impl->hitSeq;
|
||||
ph.pkt.damage = (int16_t)std::clamp(h.damage, 0, 1000);
|
||||
ph.pkt.knock[0] = h.knockback.x; ph.pkt.knock[1] = h.knockback.y; ph.pkt.knock[2] = h.knockback.z;
|
||||
ph.copiesLeft = BLAST_COPIES;
|
||||
impl->pendingHits.push_back(ph);
|
||||
}
|
||||
|
||||
void Lan::Send(const NetState& s) {
|
||||
if (!active) return;
|
||||
|
||||
// One more copy of every pending hit: clients send to the host, the host to the target only
|
||||
for (Impl::PendingHit& ph : impl->pendingHits) {
|
||||
if (mode == Mode::Client) impl->SendHitPacket(impl->hostAddr, ph.pkt);
|
||||
else for (const Impl::Peer& to : impl->peers)
|
||||
if (to.id == ph.pkt.target) impl->SendHitPacket(to.addr, ph.pkt);
|
||||
ph.copiesLeft--;
|
||||
}
|
||||
impl->pendingHits.erase(
|
||||
std::remove_if(impl->pendingHits.begin(), impl->pendingHits.end(),
|
||||
[](const Impl::PendingHit& ph) { return ph.copiesLeft <= 0; }),
|
||||
impl->pendingHits.end());
|
||||
|
||||
// One more copy of every pending blast (to the host, or from the host to everyone)
|
||||
for (Impl::PendingBlast& pb : impl->pendingBlasts) {
|
||||
if (mode == Mode::Client) impl->SendBlastPacket(impl->hostAddr, pb.pkt);
|
||||
else for (const Impl::Peer& to : impl->peers) impl->SendBlastPacket(to.addr, pb.pkt);
|
||||
pb.copiesLeft--;
|
||||
}
|
||||
impl->pendingBlasts.erase(
|
||||
std::remove_if(impl->pendingBlasts.begin(), impl->pendingBlasts.end(),
|
||||
[](const Impl::PendingBlast& pb) { return pb.copiesLeft <= 0; }),
|
||||
impl->pendingBlasts.end());
|
||||
|
||||
if (mode == Mode::Client) {
|
||||
impl->SendDatagram(impl->hostAddr, { MakeEntry(0, s) });
|
||||
return;
|
||||
@@ -199,7 +309,7 @@ void Lan::Send(const NetState& s) {
|
||||
}
|
||||
}
|
||||
|
||||
void Lan::Receive(std::vector<NetUpdate>& out) {
|
||||
void Lan::Receive(std::vector<NetUpdate>& out, std::vector<NetBlast>* blasts, std::vector<NetHit>* hits) {
|
||||
if (!active) return;
|
||||
|
||||
char buf[MAX_DATAGRAM + 64];
|
||||
@@ -213,6 +323,71 @@ void Lan::Receive(std::vector<NetUpdate>& out) {
|
||||
}
|
||||
if (r < (int)sizeof(Header)) continue;
|
||||
|
||||
uint32_t magic;
|
||||
std::memcpy(&magic, buf, sizeof(magic));
|
||||
if (magic == NET_MAGIC_BLAST) {
|
||||
if (r != (int)sizeof(BlastPacket)) continue;
|
||||
BlastPacket bp;
|
||||
std::memcpy(&bp, buf, sizeof(bp));
|
||||
if (bp.version != NET_VERSION) continue;
|
||||
|
||||
if (mode == Mode::Host) {
|
||||
// Only known players may blast. Stamp their id and relay to everyone else.
|
||||
const Impl::Peer* sender = nullptr;
|
||||
for (const Impl::Peer& pe : impl->peers)
|
||||
if (SameAddress(pe.addr, from)) { sender = &pe; break; }
|
||||
if (!sender) continue;
|
||||
bp.sender = sender->id;
|
||||
for (const Impl::Peer& to : impl->peers)
|
||||
if (to.id != sender->id) impl->SendBlastPacket(to.addr, bp);
|
||||
}
|
||||
else if (!SameAddress(from, impl->hostAddr)) continue; // clients only listen to the host
|
||||
|
||||
if (blasts && impl->FirstSighting(0, bp.sender, bp.id)) {
|
||||
NetBlast nb;
|
||||
nb.sender = bp.sender;
|
||||
nb.id = bp.id;
|
||||
nb.center = glm::vec3(bp.center[0], bp.center[1], bp.center[2]);
|
||||
nb.radius = bp.radius;
|
||||
nb.power = bp.power;
|
||||
blasts->push_back(nb);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (magic == NET_MAGIC_HIT) {
|
||||
if (r != (int)sizeof(HitPacket)) continue;
|
||||
HitPacket hp;
|
||||
std::memcpy(&hp, buf, sizeof(hp));
|
||||
if (hp.version != NET_VERSION) continue;
|
||||
|
||||
bool forUs = true;
|
||||
if (mode == Mode::Host) {
|
||||
// Only known players may shoot. Stamp their id; deliver to us or forward to the target.
|
||||
const Impl::Peer* sender = nullptr;
|
||||
for (const Impl::Peer& pe : impl->peers)
|
||||
if (SameAddress(pe.addr, from)) { sender = &pe; break; }
|
||||
if (!sender) continue;
|
||||
hp.sender = sender->id;
|
||||
if (hp.target != 0) {
|
||||
forUs = false;
|
||||
for (const Impl::Peer& to : impl->peers)
|
||||
if (to.id == hp.target) impl->SendHitPacket(to.addr, hp);
|
||||
}
|
||||
}
|
||||
else if (!SameAddress(from, impl->hostAddr)) continue; // clients only listen to the host
|
||||
|
||||
if (forUs && hits && impl->FirstSighting(1, hp.sender, hp.id)) {
|
||||
NetHit nh;
|
||||
nh.sender = hp.sender;
|
||||
nh.id = hp.id;
|
||||
nh.damage = hp.damage;
|
||||
nh.knockback = glm::vec3(hp.knock[0], hp.knock[1], hp.knock[2]);
|
||||
hits->push_back(nh);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Header h;
|
||||
std::memcpy(&h, buf, sizeof(h));
|
||||
if (h.magic != NET_MAGIC || h.version != NET_VERSION) continue;
|
||||
|
||||
@@ -24,6 +24,25 @@ struct NetUpdate {
|
||||
NetState state;
|
||||
};
|
||||
|
||||
// A shockwave explosion. Every machine applies the launch to its own player, since remote
|
||||
// players are only puppets here. Blasts are re-sent a few times (UDP) and de-duplicated on receive.
|
||||
struct NetBlast {
|
||||
uint8_t sender = 0; // filled in by the host; ignored on send
|
||||
uint32_t id = 0; // filled in by SendBlast
|
||||
glm::vec3 center = glm::vec3(0.0f);
|
||||
float radius = 0.0f;
|
||||
float power = 0.0f;
|
||||
};
|
||||
|
||||
// Damage dealt to another player. Each machine owns its own player's health, so the shooter
|
||||
// aims locally and sends this to the victim, who applies it. Re-sent a few times and de-duplicated.
|
||||
struct NetHit {
|
||||
uint8_t sender = 0; // filled in by the host
|
||||
uint32_t id = 0; // filled in by SendHit
|
||||
int damage = 0;
|
||||
glm::vec3 knockback = glm::vec3(0.0f); // added to the victim's velocity
|
||||
};
|
||||
|
||||
class Lan {
|
||||
public:
|
||||
enum class Mode { Off, Host, Client };
|
||||
@@ -44,7 +63,14 @@ public:
|
||||
// Call once per frame with our own state.
|
||||
void Send(const NetState& state);
|
||||
// Drains all waiting packets and appends the other players' newest states to 'out'.
|
||||
void Receive(std::vector<NetUpdate>& out);
|
||||
// If 'blasts' is given, new (de-duplicated) shockwave blasts from other players are appended to it.
|
||||
// 'hits' gets damage aimed at us (de-duplicated).
|
||||
void Receive(std::vector<NetUpdate>& out, std::vector<NetBlast>* blasts = nullptr,
|
||||
std::vector<NetHit>* hits = nullptr);
|
||||
// Damage another player. 'target' is the id from their NetUpdate (the host is 0).
|
||||
void SendHit(uint8_t target, const NetHit& hit);
|
||||
// Tell the other players about a blast we caused. Sent over the next few Send() calls.
|
||||
void SendBlast(const NetBlast& blast);
|
||||
|
||||
// Host: at least one player has joined. Client: the host has been heard from in the last 2 s.
|
||||
bool IsConnected() const;
|
||||
|
||||
Reference in New Issue
Block a user