second commit

This commit is contained in:
2026-10-18 11:39:58 -04:00
parent aff6a4f902
commit 78e77a0cdc
4 changed files with 466 additions and 55 deletions
+260 -50
View File
@@ -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)
+3 -3
View File
@@ -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;
+176 -1
View File
@@ -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;
+27 -1
View File
@@ -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;