Skip to content

Commit d01dbf5

Browse files
committed
Add server-side hitscan, firing and respawn
Implements server-authoritative combat: adds health, firing, respawn and hit tracking to player state and logic. net_common.h: introduce IS_DEAD flag, expand NetPlayerState with health, hitByPlayerId and fireCounter and update static_assert sizes. game_server.h: add combat fields, firing/ raycast APIs and weapon constants. game_server.cpp: initialize health/fire/respawn on connect, skip actions for dead players, run per-tick respawn logic, integrate ProcessFiring (RPM/damage, fire timing, fireCounter) and RaycastPlayers (ray->capsule hits against enemies), prevent friendly fire and mark kills; skip dead players during physics; sync combat data into snapshots. Added server_raycast.h: standalone ray-capsule intersection and direction helpers used for server hitscan. These changes enable server-side hitscan, damage application, death/respawn and hit markers.
1 parent 6b04323 commit d01dbf5

4 files changed

Lines changed: 359 additions & 4 deletions

File tree

Network/game_server.cpp

Lines changed: 172 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,15 @@ void GameServer::OnPlayerConnected(uint8_t playerId)
107107
data.state.yaw = 0.0f;
108108
data.state.pitch = 0.0f;
109109
data.state.stateFlags = NetStateFlags::IS_GROUNDED;
110+
data.state.health = MAX_HEALTH;
111+
data.state.hitByPlayerId = 0xFF;
112+
data.state.fireCounter = 0;
110113
data.lastInput = {};
111114
data.reloadTimer = 0.0;
115+
data.health = MAX_HEALTH;
116+
data.respawnTimer = 0.0;
117+
data.fireTimer = 0.0;
118+
data.fireCounter = 0;
112119

113120
m_Players[playerId] = data;
114121
printf("[GameServer] Player %u (Team %s) spawned at (%.1f, %.1f, %.1f)\n",
@@ -162,13 +169,45 @@ void GameServer::Tick()
162169
// 3. Simulate physics for all players
163170
SimulatePhysics();
164171

165-
// 4. Update tick ID in all player states
172+
// 4. Process firing and combat for all players
173+
for (auto& [id, player] : m_Players)
174+
{
175+
// Clear hit marker each tick
176+
player.state.hitByPlayerId = 0xFF;
177+
178+
// Respawn timer
179+
if (player.state.stateFlags & NetStateFlags::IS_DEAD)
180+
{
181+
player.respawnTimer -= TICK_DURATION;
182+
if (player.respawnTimer <= 0.0)
183+
{
184+
// Respawn
185+
player.health = MAX_HEALTH;
186+
player.state.stateFlags &= ~NetStateFlags::IS_DEAD;
187+
player.state.stateFlags |= NetStateFlags::IS_GROUNDED;
188+
player.state.position = GetSpawnPosition(id, player.teamId);
189+
player.state.velocity = { 0.0f, 0.0f, 0.0f };
190+
player.respawnTimer = 0.0;
191+
printf("[GameServer] Player %u respawned\n", id);
192+
}
193+
}
194+
else
195+
{
196+
ProcessFiring(player, id);
197+
}
198+
199+
// Sync combat data to state
200+
player.state.health = player.health;
201+
player.state.fireCounter = player.fireCounter;
202+
}
203+
204+
// 5. Update tick ID in all player states
166205
for (auto& [id, player] : m_Players)
167206
{
168207
player.state.tickId = m_CurrentTick;
169208
}
170209

171-
// 5. Broadcast per-player snapshots
210+
// 6. Broadcast per-player snapshots
172211
BroadcastSnapshots();
173212
}
174213

@@ -198,6 +237,14 @@ void GameServer::ProcessInputCmd(const InputCmd& cmd, uint8_t playerId)
198237
PlayerData& player = it->second;
199238
player.lastInput = cmd;
200239

240+
// Dead players: only update camera, skip all actions
241+
if (player.state.stateFlags & NetStateFlags::IS_DEAD)
242+
{
243+
player.state.yaw = cmd.yaw;
244+
player.state.pitch = cmd.pitch;
245+
return;
246+
}
247+
201248
player.state.yaw = cmd.yaw;
202249
player.state.pitch = cmd.pitch;
203250

@@ -244,6 +291,9 @@ void GameServer::SimulatePhysics()
244291
{
245292
for (auto& [id, player] : m_Players)
246293
{
294+
// Skip dead players
295+
if (player.state.stateFlags & NetStateFlags::IS_DEAD)
296+
continue;
247297
SimulatePlayerPhysics(player);
248298
}
249299
}
@@ -410,3 +460,123 @@ void GameServer::BroadcastSnapshots()
410460
m_pNetwork->SendSnapshotToPlayer(myId, snapshot);
411461
}
412462
}
463+
464+
//-----------------------------------------------------------------------------
465+
// ProcessFiring - Handle fire rate and hitscan for a player
466+
//-----------------------------------------------------------------------------
467+
void GameServer::ProcessFiring(PlayerData& shooter, uint8_t shooterId)
468+
{
469+
bool isFiring = (shooter.lastInput.buttons & InputButtons::FIRE) != 0;
470+
471+
if (!isFiring)
472+
{
473+
shooter.fireTimer = 0.0;
474+
return;
475+
}
476+
477+
// Determine RPM and damage based on team
478+
double rpm = (shooter.teamId == PlayerTeam::RED) ? RED_RPM : BLUE_RPM;
479+
uint8_t damage = (shooter.teamId == PlayerTeam::RED) ? RED_DAMAGE : BLUE_DAMAGE;
480+
double fireInterval = 60.0 / rpm;
481+
482+
// First shot fires immediately; subsequent shots at RPM interval
483+
bool shouldFire = false;
484+
if (shooter.fireTimer <= 0.0)
485+
{
486+
// First press — fire immediately
487+
shouldFire = true;
488+
shooter.fireTimer = fireInterval;
489+
}
490+
else
491+
{
492+
shooter.fireTimer -= TICK_DURATION;
493+
if (shooter.fireTimer <= 0.0)
494+
{
495+
shouldFire = true;
496+
shooter.fireTimer += fireInterval;
497+
}
498+
}
499+
500+
if (!shouldFire) return;
501+
502+
// Increment fire counter
503+
shooter.fireCounter++;
504+
505+
// Cast ray from eye position
506+
Float3 eyePos = {
507+
shooter.state.position.x,
508+
shooter.state.position.y + PLAYER_HEIGHT - 0.1f,
509+
shooter.state.position.z
510+
};
511+
Float3 rayDir = ServerRaycast::DirectionFromYawPitch(
512+
shooter.state.yaw, shooter.state.pitch);
513+
514+
// Test against all other alive players (no friendly fire)
515+
uint8_t hitId = 0xFF;
516+
float hitDist = 0.0f;
517+
if (RaycastPlayers(eyePos, rayDir, shooterId, shooter.teamId, hitId, hitDist))
518+
{
519+
// Apply damage
520+
auto hitIt = m_Players.find(hitId);
521+
if (hitIt != m_Players.end())
522+
{
523+
PlayerData& target = hitIt->second;
524+
if (target.health > damage)
525+
{
526+
target.health -= damage;
527+
}
528+
else
529+
{
530+
target.health = 0;
531+
target.state.stateFlags |= NetStateFlags::IS_DEAD;
532+
target.respawnTimer = RESPAWN_TIME;
533+
target.state.velocity = { 0.0f, 0.0f, 0.0f };
534+
printf("[GameServer] Player %u killed Player %u\n", shooterId, hitId);
535+
}
536+
537+
// Record hit for attacker's hit marker
538+
shooter.state.hitByPlayerId = hitId;
539+
}
540+
}
541+
}
542+
543+
//-----------------------------------------------------------------------------
544+
// RaycastPlayers - Test ray against all alive enemy players
545+
//
546+
// Returns true if any enemy player is hit. outHitId/outDist are set to the
547+
// closest hit. Players on excludeTeam are skipped (no friendly fire).
548+
//-----------------------------------------------------------------------------
549+
bool GameServer::RaycastPlayers(const Float3& origin, const Float3& dir,
550+
uint8_t excludeId, uint8_t excludeTeam,
551+
uint8_t& outHitId, float& outDist)
552+
{
553+
bool anyHit = false;
554+
float closestT = 9999.0f;
555+
556+
for (const auto& [id, player] : m_Players)
557+
{
558+
// Skip self
559+
if (id == excludeId) continue;
560+
// Skip same team (no friendly fire)
561+
if (player.teamId == excludeTeam) continue;
562+
// Skip dead
563+
if (player.state.stateFlags & NetStateFlags::IS_DEAD) continue;
564+
565+
float t = 0.0f;
566+
if (ServerRaycast::RayCapsule(origin, dir,
567+
player.state.position, PLAYER_HEIGHT, CAPSULE_RADIUS, t))
568+
{
569+
if (t < closestT)
570+
{
571+
closestT = t;
572+
outHitId = id;
573+
anyHit = true;
574+
}
575+
}
576+
}
577+
578+
if (anyHit)
579+
outDist = closestT;
580+
581+
return anyHit;
582+
}

Network/game_server.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#include "net_common.h"
1616
#include "server_collision.h"
17+
#include "server_raycast.h"
1718
#include <unordered_map>
1819
#include <cstddef>
1920

@@ -56,11 +57,20 @@ class GameServer
5657
InputCmd lastInput{};
5758
double reloadTimer = 0.0;
5859
uint8_t teamId = PlayerTeam::RED;
60+
// Combat
61+
uint8_t health = 200;
62+
double respawnTimer = 0.0;
63+
double fireTimer = 0.0;
64+
uint16_t fireCounter = 0;
5965
};
6066

6167
void Tick();
6268
void ProcessPlayerEvents();
6369
void ProcessInputCmd(const InputCmd& cmd, uint8_t playerId);
70+
void ProcessFiring(PlayerData& shooter, uint8_t shooterId);
71+
bool RaycastPlayers(const Float3& origin, const Float3& dir,
72+
uint8_t excludeId, uint8_t excludeTeam,
73+
uint8_t& outHitId, float& outDist);
6474
void SimulatePlayerPhysics(PlayerData& player);
6575
void SimulatePhysics();
6676
void BroadcastSnapshots();
@@ -88,4 +98,12 @@ class GameServer
8898
// Player collision parameters (must match client)
8999
static constexpr float PLAYER_HEIGHT = 2.0f;
90100
static constexpr float CAPSULE_RADIUS = 0.5f;
101+
102+
// Weapon parameters per team
103+
static constexpr double RED_RPM = 600.0;
104+
static constexpr uint8_t RED_DAMAGE = 34;
105+
static constexpr double BLUE_RPM = 800.0;
106+
static constexpr uint8_t BLUE_DAMAGE = 25;
107+
static constexpr uint8_t MAX_HEALTH = 200;
108+
static constexpr double RESPAWN_TIME = 2.0;
91109
};

Network/net_common.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ constexpr uint32_t IS_FIRING = 1 << 2;
6464
constexpr uint32_t IS_ADS = 1 << 3;
6565
constexpr uint32_t IS_RELOADING = 1 << 4;
6666
constexpr uint32_t IS_RELOAD_EMPTY = 1 << 5;
67+
constexpr uint32_t IS_DEAD = 1 << 6;
6768
} // namespace NetStateFlags
6869

6970
//-----------------------------------------------------------------------------
@@ -76,6 +77,9 @@ struct NetPlayerState {
7677
float yaw; // Camera yaw
7778
float pitch; // Camera pitch
7879
uint32_t stateFlags; // Bitfield of StateFlags
80+
uint8_t health; // 0-200, server authoritative
81+
uint8_t hitByPlayerId; // 0xFF = no hit, else attacker ID
82+
uint16_t fireCounter; // Server-tracked fire count
7983
};
8084

8185
//-----------------------------------------------------------------------------
@@ -125,7 +129,7 @@ struct Snapshot {
125129
//-----------------------------------------------------------------------------
126130
static_assert(sizeof(InputCmd) == 24,
127131
"InputCmd size changed - update network serialization");
128-
static_assert(sizeof(NetPlayerState) == 40,
132+
static_assert(sizeof(NetPlayerState) == 44,
129133
"NetPlayerState size changed - update network serialization");
130-
static_assert(sizeof(RemotePlayerEntry) == 44,
134+
static_assert(sizeof(RemotePlayerEntry) == 48,
131135
"RemotePlayerEntry size changed - update network serialization");

0 commit comments

Comments
 (0)