From 935fcd3aa1bc59916761307f16f9c223ec424482 Mon Sep 17 00:00:00 2001 From: pisces Date: Tue, 17 Feb 2026 15:30:35 +0900 Subject: [PATCH 01/13] Add server-side collision and dev Docker workflow Add header-only server collision system and integrate it into the game server, plus add a CI workflow to build/push a dev Docker image. - New GitHub Actions workflow (.github/workflows/docker-build-dev.yml) to build and push dev images on pushes to the dev branch. - New Network/server_collision.h: header-only capsule vs AABB resolution utilities (ClosestPointOnSegment, ClampToAABB, ResolveCapsule). Returns corrected position/velocity and grounded state; must be kept in sync with client collision logic. - Network/game_server.h: include server_collision.h, add m_Colliders vector and player collision constants (PLAYER_HEIGHT, CAPSULE_RADIUS). - Network/game_server.cpp: initialize simple world colliders (ground + test platform) in Initialize(); replace ad-hoc floor logic in SimulatePlayerPhysics with ServerCollision::ResolveCapsule and preserve fallback floor behavior when no colliders present. Collision response updates position, velocity, and grounded/jumping flags. These changes enable deterministic server-side player collision and provide a CI step to publish development Docker images. Adjust collider data and tuning constants as needed to match client behavior. --- .github/workflows/docker-build-dev.yml | 27 ++++ Network/game_server.cpp | 33 ++++- Network/game_server.h | 8 ++ Network/server_collision.h | 185 +++++++++++++++++++++++++ 4 files changed, 248 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/docker-build-dev.yml create mode 100644 Network/server_collision.h diff --git a/.github/workflows/docker-build-dev.yml b/.github/workflows/docker-build-dev.yml new file mode 100644 index 0000000..aa4e907 --- /dev/null +++ b/.github/workflows/docker-build-dev.yml @@ -0,0 +1,27 @@ +name: Build and Push Dev Docker Image + +on: + push: + branches: [dev] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/triggeron_game_server:dev + ${{ secrets.DOCKERHUB_USERNAME }}/triggeron_game_server:dev-${{ github.sha }} diff --git a/Network/game_server.cpp b/Network/game_server.cpp index 9b1d59d..ba23eb3 100644 --- a/Network/game_server.cpp +++ b/Network/game_server.cpp @@ -30,6 +30,11 @@ void GameServer::Initialize(ENetServerNetwork* pNetwork) m_ServerTime = 0.0; m_CurrentTick = 0; m_Players.clear(); + + // Register ground collider (must match client CollisionWorld) + m_Colliders.clear(); + m_Colliders.push_back({ {{ -128.0f, -1.0f, -128.0f }, { 128.0f, 0.0f, 128.0f }}, true }); + m_Colliders.push_back({ {{ 5.0f, 0.0f, 5.0f }, { 10.0f, 2.0f, 10.0f }}, true }); // Test platform } void GameServer::Finalize() @@ -314,12 +319,30 @@ void GameServer::SimulatePlayerPhysics(PlayerData& player) state.position.z += state.velocity.z * dt; state.position.y += state.velocity.y * dt; - if (state.position.y <= 0.0f) + // Collision Detection (Capsule vs World AABBs) + if (!m_Colliders.empty()) + { + auto result = ServerCollision::ResolveCapsule( + m_Colliders, state.position, + PLAYER_HEIGHT, CAPSULE_RADIUS, state.velocity); + state.position = result.position; + state.velocity = result.velocity; + if (result.isGrounded) + { + state.stateFlags |= NetStateFlags::IS_GROUNDED; + state.stateFlags &= ~NetStateFlags::IS_JUMPING; + } + } + else { - state.position.y = 0.0f; - state.velocity.y = 0.0f; - state.stateFlags |= NetStateFlags::IS_GROUNDED; - state.stateFlags &= ~NetStateFlags::IS_JUMPING; + // Fallback: simple floor at y=0 + if (state.position.y <= 0.0f) + { + state.position.y = 0.0f; + state.velocity.y = 0.0f; + state.stateFlags |= NetStateFlags::IS_GROUNDED; + state.stateFlags &= ~NetStateFlags::IS_JUMPING; + } } } diff --git a/Network/game_server.h b/Network/game_server.h index 2426ca6..3628a21 100644 --- a/Network/game_server.h +++ b/Network/game_server.h @@ -13,6 +13,7 @@ //============================================================================= #include "net_common.h" +#include "server_collision.h" #include #include @@ -80,4 +81,11 @@ class GameServer // Per-player game state std::unordered_map m_Players; + + // Collision world for gravity + std::vector m_Colliders; + + // Player collision parameters (must match client) + static constexpr float PLAYER_HEIGHT = 2.0f; + static constexpr float CAPSULE_RADIUS = 0.5f; }; diff --git a/Network/server_collision.h b/Network/server_collision.h new file mode 100644 index 0000000..d5511bf --- /dev/null +++ b/Network/server_collision.h @@ -0,0 +1,185 @@ +#pragma once +//============================================================================= +// server_collision.h +// +// Pure-math collision detection for game_server. +// No DirectXMath dependency - uses Float3 from net_common.h. +// Must be kept in sync with game_client CollisionWorld logic. +//============================================================================= + +#include "net_common.h" +#include +#include +#include + +struct ServerAABB +{ + Float3 min; + Float3 max; +}; + +struct ServerCollider +{ + ServerAABB aabb; + bool isGround; +}; + +struct ServerCollisionResult +{ + Float3 position; + Float3 velocity; + bool isGrounded; +}; + +//----------------------------------------------------------------------------- +// Inline implementation (header-only for simplicity) +//----------------------------------------------------------------------------- +namespace ServerCollision +{ + +inline Float3 ClosestPointOnSegment(const Float3& segA, const Float3& segB, const Float3& point) +{ + float abx = segB.x - segA.x; + float aby = segB.y - segA.y; + float abz = segB.z - segA.z; + + float abLenSq = abx * abx + aby * aby + abz * abz; + if (abLenSq < 1e-8f) + return segA; + + float apx = point.x - segA.x; + float apy = point.y - segA.y; + float apz = point.z - segA.z; + + float t = (apx * abx + apy * aby + apz * abz) / abLenSq; + t = std::clamp(t, 0.0f, 1.0f); + + return { segA.x + abx * t, segA.y + aby * t, segA.z + abz * t }; +} + +inline Float3 ClampToAABB(const Float3& point, const ServerAABB& aabb) +{ + return { + std::clamp(point.x, aabb.min.x, aabb.max.x), + std::clamp(point.y, aabb.min.y, aabb.max.y), + std::clamp(point.z, aabb.min.z, aabb.max.z) + }; +} + +inline ServerCollisionResult ResolveCapsule( + const std::vector& colliders, + const Float3& capsuleBottom, + float capsuleHeight, + float capsuleRadius, + const Float3& velocity) +{ + ServerCollisionResult result; + result.position = capsuleBottom; + result.velocity = velocity; + result.isGrounded = false; + + float innerBottom = capsuleRadius; + float innerTop = capsuleHeight - capsuleRadius; + if (innerTop < innerBottom) innerTop = innerBottom; + + for (int iter = 0; iter < 4; ++iter) + { + bool anyCollision = false; + + for (const auto& collider : colliders) + { + const ServerAABB& aabb = collider.aabb; + + Float3 segA = { + result.position.x, + result.position.y + innerBottom, + result.position.z + }; + Float3 segB = { + result.position.x, + result.position.y + innerTop, + result.position.z + }; + + Float3 closestOnSeg = ClosestPointOnSegment(segA, segB, + { std::clamp(segA.x, aabb.min.x, aabb.max.x), + std::clamp(segA.y, aabb.min.y, aabb.max.y), + std::clamp(segA.z, aabb.min.z, aabb.max.z) }); + + Float3 closestOnAABB = ClampToAABB(closestOnSeg, aabb); + + closestOnSeg = ClosestPointOnSegment(segA, segB, closestOnAABB); + closestOnAABB = ClampToAABB(closestOnSeg, aabb); + + float dx = closestOnSeg.x - closestOnAABB.x; + float dy = closestOnSeg.y - closestOnAABB.y; + float dz = closestOnSeg.z - closestOnAABB.z; + float distSq = dx * dx + dy * dy + dz * dz; + + if (distSq >= capsuleRadius * capsuleRadius) + continue; + + float dist = sqrtf(distSq); + float nx, ny, nz; + + if (dist > 1e-6f) + { + nx = dx / dist; + ny = dy / dist; + nz = dz / dist; + } + else + { + float cx = (aabb.min.x + aabb.max.x) * 0.5f; + float cy = (aabb.min.y + aabb.max.y) * 0.5f; + float cz = (aabb.min.z + aabb.max.z) * 0.5f; + float hx = (aabb.max.x - aabb.min.x) * 0.5f; + float hy = (aabb.max.y - aabb.min.y) * 0.5f; + float hz = (aabb.max.z - aabb.min.z) * 0.5f; + + float relX = closestOnSeg.x - cx; + float relY = closestOnSeg.y - cy; + float relZ = closestOnSeg.z - cz; + + float overlapX = hx + capsuleRadius - fabsf(relX); + float overlapY = hy + capsuleRadius - fabsf(relY); + float overlapZ = hz + capsuleRadius - fabsf(relZ); + + nx = ny = nz = 0.0f; + if (overlapX <= overlapY && overlapX <= overlapZ) + nx = (relX >= 0.0f) ? 1.0f : -1.0f; + else if (overlapY <= overlapX && overlapY <= overlapZ) + ny = (relY >= 0.0f) ? 1.0f : -1.0f; + else + nz = (relZ >= 0.0f) ? 1.0f : -1.0f; + } + + float penetration = capsuleRadius - dist; + + result.position.x += nx * penetration; + result.position.y += ny * penetration; + result.position.z += nz * penetration; + + float velDotN = result.velocity.x * nx + result.velocity.y * ny + result.velocity.z * nz; + if (velDotN < 0.0f) + { + result.velocity.x -= velDotN * nx; + result.velocity.y -= velDotN * ny; + result.velocity.z -= velDotN * nz; + } + + if (collider.isGround && ny > 0.7f) + { + result.isGrounded = true; + } + + anyCollision = true; + } + + if (!anyCollision) break; + } + + return result; +} + +} // namespace ServerCollision From 4903dfeb3049118171bd3041e9467dd01402c66a Mon Sep 17 00:00:00 2001 From: pisces Date: Tue, 17 Feb 2026 16:20:34 +0900 Subject: [PATCH 02/13] Clear IS_GROUNDED flag when not grounded In SimulatePlayerPhysics (Network/game_server.cpp), add an else branch to clear the NetStateFlags::IS_GROUNDED flag when the player is not grounded. This prevents the grounded flag from persisting incorrectly across frames and ensures accurate grounded/jumping state transitions. --- Network/game_server.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Network/game_server.cpp b/Network/game_server.cpp index ba23eb3..aacf4cb 100644 --- a/Network/game_server.cpp +++ b/Network/game_server.cpp @@ -332,6 +332,10 @@ void GameServer::SimulatePlayerPhysics(PlayerData& player) state.stateFlags |= NetStateFlags::IS_GROUNDED; state.stateFlags &= ~NetStateFlags::IS_JUMPING; } + else + { + state.stateFlags &= ~NetStateFlags::IS_GROUNDED; + } } else { From 39492a122de424f1de51c0a477779d73c65074b5 Mon Sep 17 00:00:00 2001 From: pisces Date: Tue, 17 Feb 2026 16:27:46 +0900 Subject: [PATCH 03/13] Refine grounded flag handling in physics Only set IS_GROUNDED when the collider reports grounded and the vertical velocity is non-positive (state.velocity.y <= 0.0f). Also only clear IS_GROUNDED when result.isGrounded is false, preventing spurious loss of the grounded state during upward velocity (e.g. small bounces or upward corrections). Keeps IS_JUMPING cleared when legitimately grounded. --- Network/game_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Network/game_server.cpp b/Network/game_server.cpp index aacf4cb..119ebbd 100644 --- a/Network/game_server.cpp +++ b/Network/game_server.cpp @@ -327,12 +327,12 @@ void GameServer::SimulatePlayerPhysics(PlayerData& player) PLAYER_HEIGHT, CAPSULE_RADIUS, state.velocity); state.position = result.position; state.velocity = result.velocity; - if (result.isGrounded) + if (result.isGrounded && state.velocity.y <= 0.0f) { state.stateFlags |= NetStateFlags::IS_GROUNDED; state.stateFlags &= ~NetStateFlags::IS_JUMPING; } - else + else if (!result.isGrounded) { state.stateFlags &= ~NetStateFlags::IS_GROUNDED; } From 0fb0708a0b76cd5f637e10a1e62fa981171c3ebc Mon Sep 17 00:00:00 2001 From: pisces Date: Tue, 17 Feb 2026 16:38:21 +0900 Subject: [PATCH 04/13] Fix grounding logic and capsule collision edge case Record the player's grounded state at the start of physics simulation and use it to decide gravity application so gravity is only applied when the player was airborne at frame start. Simplify grounded/jumping state updates to rely directly on collision results (always set IS_GROUNDED when collision reports grounded and clear JUMPING), avoiding incorrect velocity checks. Also adjust the capsule-vs-AABB test to use > instead of >= so exact tangential contacts (distSq == radius^2) are treated as collisions, fixing boundary miss cases. --- Network/game_server.cpp | 7 ++++--- Network/server_collision.h | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Network/game_server.cpp b/Network/game_server.cpp index 119ebbd..f8a39ac 100644 --- a/Network/game_server.cpp +++ b/Network/game_server.cpp @@ -244,6 +244,7 @@ void GameServer::SimulatePlayerPhysics(PlayerData& player) const InputCmd& input = player.lastInput; bool isGrounded = (state.stateFlags & NetStateFlags::IS_GROUNDED) != 0; + bool wasGroundedAtStart = isGrounded; float yaw = input.yaw; float frontX = sinf(yaw); @@ -310,7 +311,7 @@ void GameServer::SimulatePlayerPhysics(PlayerData& player) } } - if (!isGrounded) + if (!wasGroundedAtStart) { state.velocity.y -= GRAVITY * dt; } @@ -327,12 +328,12 @@ void GameServer::SimulatePlayerPhysics(PlayerData& player) PLAYER_HEIGHT, CAPSULE_RADIUS, state.velocity); state.position = result.position; state.velocity = result.velocity; - if (result.isGrounded && state.velocity.y <= 0.0f) + if (result.isGrounded) { state.stateFlags |= NetStateFlags::IS_GROUNDED; state.stateFlags &= ~NetStateFlags::IS_JUMPING; } - else if (!result.isGrounded) + else { state.stateFlags &= ~NetStateFlags::IS_GROUNDED; } diff --git a/Network/server_collision.h b/Network/server_collision.h index d5511bf..a7d41f1 100644 --- a/Network/server_collision.h +++ b/Network/server_collision.h @@ -116,7 +116,7 @@ inline ServerCollisionResult ResolveCapsule( float dz = closestOnSeg.z - closestOnAABB.z; float distSq = dx * dx + dy * dy + dz * dz; - if (distSq >= capsuleRadius * capsuleRadius) + if (distSq > capsuleRadius * capsuleRadius) continue; float dist = sqrtf(distSq); From 6b043239052c423756459dfb4765312ba0a3abe6 Mon Sep 17 00:00:00 2001 From: pisces Date: Tue, 17 Feb 2026 17:14:53 +0900 Subject: [PATCH 05/13] Load shared map colliders in server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a shared MapColliderDef array (Network/map_colliders.h) and update GameServer to register colliders from it instead of using hardcoded colliders. The new header defines collider categories (MAP_GROUND, MAP_CUBE, MAP_WALL), a pure-C MapColliderDef struct, MAP_COLLIDERS and MAP_COLLIDER_COUNT for client/server sync. GameServer::Initialize now iterates MAP_COLLIDERS, builds ServerCollider AABBs (auto-computes ±0.5 for MAP_CUBE, uses explicit min/max for others) and copies the isGround flag, removing the previous hardcoded ground/platform entries. --- Network/game_server.cpp | 28 ++++++++++++++++++--- Network/map_colliders.h | 54 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 Network/map_colliders.h diff --git a/Network/game_server.cpp b/Network/game_server.cpp index f8a39ac..63891ae 100644 --- a/Network/game_server.cpp +++ b/Network/game_server.cpp @@ -7,6 +7,7 @@ #include "game_server.h" #include "enet_server_network.h" +#include "map_colliders.h" #include #include @@ -31,10 +32,31 @@ void GameServer::Initialize(ENetServerNetwork* pNetwork) m_CurrentTick = 0; m_Players.clear(); - // Register ground collider (must match client CollisionWorld) + // Register colliders from shared map data (must match client) m_Colliders.clear(); - m_Colliders.push_back({ {{ -128.0f, -1.0f, -128.0f }, { 128.0f, 0.0f, 128.0f }}, true }); - m_Colliders.push_back({ {{ 5.0f, 0.0f, 5.0f }, { 10.0f, 2.0f, 10.0f }}, true }); // Test platform + for (int i = 0; i < MAP_COLLIDER_COUNT; i++) + { + const MapColliderDef& def = MAP_COLLIDERS[i]; + ServerCollider sc; + if (def.category == MAP_CUBE) + { + // Cube: AABB = position ± 0.5 + sc.aabb = { + { def.posX - 0.5f, def.posY - 0.5f, def.posZ - 0.5f }, + { def.posX + 0.5f, def.posY + 0.5f, def.posZ + 0.5f } + }; + } + else + { + // Explicit AABB + sc.aabb = { + { def.minX, def.minY, def.minZ }, + { def.maxX, def.maxY, def.maxZ } + }; + } + sc.isGround = def.isGround; + m_Colliders.push_back(sc); + } } void GameServer::Finalize() diff --git a/Network/map_colliders.h b/Network/map_colliders.h new file mode 100644 index 0000000..4387cd4 --- /dev/null +++ b/Network/map_colliders.h @@ -0,0 +1,54 @@ +#pragma once +//============================================================================= +// map_colliders.h +// +// Shared map collider definitions. +// This file is used by BOTH client and server — keep in sync! +// No DirectXMath dependency — pure C structs. +//============================================================================= + +// Object categories +enum MapCategory +{ + MAP_GROUND = 0, // Ground plane — rendered as MeshField, has AABB + MAP_CUBE = 1, // Cube block — rendered with Cube_Draw, AABB from position + MAP_WALL = 2, // Invisible wall — AABB only, no rendering +}; + +// Collider definition +// For MAP_CUBE: AABB is auto-computed from position (±0.5 unit cube) +// For MAP_GROUND / MAP_WALL: AABB is specified explicitly via min/max +struct MapColliderDef +{ + int category; + float posX, posY, posZ; // position (used for cube rendering & AABB center) + float minX, minY, minZ; // AABB min (explicit, ignored for MAP_CUBE) + float maxX, maxY, maxZ; // AABB max (explicit, ignored for MAP_CUBE) + bool isGround; // true = player can land on this surface +}; + +// ============================================================================ +// MAP DATA — Add new colliders here! +// ============================================================================ +static const MapColliderDef MAP_COLLIDERS[] = +{ + // === Ground === + // cat pos(x,y,z) AABB min AABB max ground + { MAP_GROUND, 0,0,0, -128.0f,-1.0f,-128.0f, 128.0f, 0.0f, 128.0f, true }, + + // === Cube Blocks (AABB auto = pos ± 0.5) === + // y=0.5 → AABB [0,1] (sits on ground) | y=1.5 → AABB [1,2] (second layer) + // cat pos(x,y,z) (min/max ignored for cubes) ground + { MAP_CUBE, 7.5f, 0.5f, 7.5f, 0,0,0, 0,0,0, true }, + { MAP_CUBE, 7.5f, 0.5f, 8.5f, 0,0,0, 0,0,0, true }, + { MAP_CUBE, 8.5f, 0.5f, 7.5f, 0,0,0, 0,0,0, true }, + { MAP_CUBE, 8.5f, 0.5f, 8.5f, 0,0,0, 0,0,0, true }, + { MAP_CUBE, 7.5f, 1.5f, 7.5f, 0,0,0, 0,0,0, true }, + { MAP_CUBE, 8.5f, 1.5f, 7.5f, 0,0,0, 0,0,0, true }, + + // === Invisible Walls (map boundary) === + // cat pos(unused) AABB min AABB max ground + // (Add map boundary walls here if needed) +}; + +static const int MAP_COLLIDER_COUNT = sizeof(MAP_COLLIDERS) / sizeof(MAP_COLLIDERS[0]); From d01dbf57347c7bf3c3309a822468b87e6014d6d1 Mon Sep 17 00:00:00 2001 From: pisces Date: Tue, 17 Feb 2026 17:48:27 +0900 Subject: [PATCH 06/13] 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. --- Network/game_server.cpp | 174 ++++++++++++++++++++++++++++++++++++++- Network/game_server.h | 18 ++++ Network/net_common.h | 8 +- Network/server_raycast.h | 163 ++++++++++++++++++++++++++++++++++++ 4 files changed, 359 insertions(+), 4 deletions(-) create mode 100644 Network/server_raycast.h diff --git a/Network/game_server.cpp b/Network/game_server.cpp index 63891ae..de5d4ab 100644 --- a/Network/game_server.cpp +++ b/Network/game_server.cpp @@ -107,8 +107,15 @@ void GameServer::OnPlayerConnected(uint8_t playerId) data.state.yaw = 0.0f; data.state.pitch = 0.0f; data.state.stateFlags = NetStateFlags::IS_GROUNDED; + data.state.health = MAX_HEALTH; + data.state.hitByPlayerId = 0xFF; + data.state.fireCounter = 0; data.lastInput = {}; data.reloadTimer = 0.0; + data.health = MAX_HEALTH; + data.respawnTimer = 0.0; + data.fireTimer = 0.0; + data.fireCounter = 0; m_Players[playerId] = data; printf("[GameServer] Player %u (Team %s) spawned at (%.1f, %.1f, %.1f)\n", @@ -162,13 +169,45 @@ void GameServer::Tick() // 3. Simulate physics for all players SimulatePhysics(); - // 4. Update tick ID in all player states + // 4. Process firing and combat for all players + for (auto& [id, player] : m_Players) + { + // Clear hit marker each tick + player.state.hitByPlayerId = 0xFF; + + // Respawn timer + if (player.state.stateFlags & NetStateFlags::IS_DEAD) + { + player.respawnTimer -= TICK_DURATION; + if (player.respawnTimer <= 0.0) + { + // Respawn + player.health = MAX_HEALTH; + player.state.stateFlags &= ~NetStateFlags::IS_DEAD; + player.state.stateFlags |= NetStateFlags::IS_GROUNDED; + player.state.position = GetSpawnPosition(id, player.teamId); + player.state.velocity = { 0.0f, 0.0f, 0.0f }; + player.respawnTimer = 0.0; + printf("[GameServer] Player %u respawned\n", id); + } + } + else + { + ProcessFiring(player, id); + } + + // Sync combat data to state + player.state.health = player.health; + player.state.fireCounter = player.fireCounter; + } + + // 5. Update tick ID in all player states for (auto& [id, player] : m_Players) { player.state.tickId = m_CurrentTick; } - // 5. Broadcast per-player snapshots + // 6. Broadcast per-player snapshots BroadcastSnapshots(); } @@ -198,6 +237,14 @@ void GameServer::ProcessInputCmd(const InputCmd& cmd, uint8_t playerId) PlayerData& player = it->second; player.lastInput = cmd; + // Dead players: only update camera, skip all actions + if (player.state.stateFlags & NetStateFlags::IS_DEAD) + { + player.state.yaw = cmd.yaw; + player.state.pitch = cmd.pitch; + return; + } + player.state.yaw = cmd.yaw; player.state.pitch = cmd.pitch; @@ -244,6 +291,9 @@ void GameServer::SimulatePhysics() { for (auto& [id, player] : m_Players) { + // Skip dead players + if (player.state.stateFlags & NetStateFlags::IS_DEAD) + continue; SimulatePlayerPhysics(player); } } @@ -410,3 +460,123 @@ void GameServer::BroadcastSnapshots() m_pNetwork->SendSnapshotToPlayer(myId, snapshot); } } + +//----------------------------------------------------------------------------- +// ProcessFiring - Handle fire rate and hitscan for a player +//----------------------------------------------------------------------------- +void GameServer::ProcessFiring(PlayerData& shooter, uint8_t shooterId) +{ + bool isFiring = (shooter.lastInput.buttons & InputButtons::FIRE) != 0; + + if (!isFiring) + { + shooter.fireTimer = 0.0; + return; + } + + // Determine RPM and damage based on team + double rpm = (shooter.teamId == PlayerTeam::RED) ? RED_RPM : BLUE_RPM; + uint8_t damage = (shooter.teamId == PlayerTeam::RED) ? RED_DAMAGE : BLUE_DAMAGE; + double fireInterval = 60.0 / rpm; + + // First shot fires immediately; subsequent shots at RPM interval + bool shouldFire = false; + if (shooter.fireTimer <= 0.0) + { + // First press — fire immediately + shouldFire = true; + shooter.fireTimer = fireInterval; + } + else + { + shooter.fireTimer -= TICK_DURATION; + if (shooter.fireTimer <= 0.0) + { + shouldFire = true; + shooter.fireTimer += fireInterval; + } + } + + if (!shouldFire) return; + + // Increment fire counter + shooter.fireCounter++; + + // Cast ray from eye position + Float3 eyePos = { + shooter.state.position.x, + shooter.state.position.y + PLAYER_HEIGHT - 0.1f, + shooter.state.position.z + }; + Float3 rayDir = ServerRaycast::DirectionFromYawPitch( + shooter.state.yaw, shooter.state.pitch); + + // Test against all other alive players (no friendly fire) + uint8_t hitId = 0xFF; + float hitDist = 0.0f; + if (RaycastPlayers(eyePos, rayDir, shooterId, shooter.teamId, hitId, hitDist)) + { + // Apply damage + auto hitIt = m_Players.find(hitId); + if (hitIt != m_Players.end()) + { + PlayerData& target = hitIt->second; + if (target.health > damage) + { + target.health -= damage; + } + else + { + target.health = 0; + target.state.stateFlags |= NetStateFlags::IS_DEAD; + target.respawnTimer = RESPAWN_TIME; + target.state.velocity = { 0.0f, 0.0f, 0.0f }; + printf("[GameServer] Player %u killed Player %u\n", shooterId, hitId); + } + + // Record hit for attacker's hit marker + shooter.state.hitByPlayerId = hitId; + } + } +} + +//----------------------------------------------------------------------------- +// RaycastPlayers - Test ray against all alive enemy players +// +// Returns true if any enemy player is hit. outHitId/outDist are set to the +// closest hit. Players on excludeTeam are skipped (no friendly fire). +//----------------------------------------------------------------------------- +bool GameServer::RaycastPlayers(const Float3& origin, const Float3& dir, + uint8_t excludeId, uint8_t excludeTeam, + uint8_t& outHitId, float& outDist) +{ + bool anyHit = false; + float closestT = 9999.0f; + + for (const auto& [id, player] : m_Players) + { + // Skip self + if (id == excludeId) continue; + // Skip same team (no friendly fire) + if (player.teamId == excludeTeam) continue; + // Skip dead + if (player.state.stateFlags & NetStateFlags::IS_DEAD) continue; + + float t = 0.0f; + if (ServerRaycast::RayCapsule(origin, dir, + player.state.position, PLAYER_HEIGHT, CAPSULE_RADIUS, t)) + { + if (t < closestT) + { + closestT = t; + outHitId = id; + anyHit = true; + } + } + } + + if (anyHit) + outDist = closestT; + + return anyHit; +} diff --git a/Network/game_server.h b/Network/game_server.h index 3628a21..f8f0048 100644 --- a/Network/game_server.h +++ b/Network/game_server.h @@ -14,6 +14,7 @@ #include "net_common.h" #include "server_collision.h" +#include "server_raycast.h" #include #include @@ -56,11 +57,20 @@ class GameServer InputCmd lastInput{}; double reloadTimer = 0.0; uint8_t teamId = PlayerTeam::RED; + // Combat + uint8_t health = 200; + double respawnTimer = 0.0; + double fireTimer = 0.0; + uint16_t fireCounter = 0; }; void Tick(); void ProcessPlayerEvents(); void ProcessInputCmd(const InputCmd& cmd, uint8_t playerId); + void ProcessFiring(PlayerData& shooter, uint8_t shooterId); + bool RaycastPlayers(const Float3& origin, const Float3& dir, + uint8_t excludeId, uint8_t excludeTeam, + uint8_t& outHitId, float& outDist); void SimulatePlayerPhysics(PlayerData& player); void SimulatePhysics(); void BroadcastSnapshots(); @@ -88,4 +98,12 @@ class GameServer // Player collision parameters (must match client) static constexpr float PLAYER_HEIGHT = 2.0f; static constexpr float CAPSULE_RADIUS = 0.5f; + + // Weapon parameters per team + static constexpr double RED_RPM = 600.0; + static constexpr uint8_t RED_DAMAGE = 34; + static constexpr double BLUE_RPM = 800.0; + static constexpr uint8_t BLUE_DAMAGE = 25; + static constexpr uint8_t MAX_HEALTH = 200; + static constexpr double RESPAWN_TIME = 2.0; }; diff --git a/Network/net_common.h b/Network/net_common.h index 0004b1b..016aed8 100644 --- a/Network/net_common.h +++ b/Network/net_common.h @@ -64,6 +64,7 @@ constexpr uint32_t IS_FIRING = 1 << 2; constexpr uint32_t IS_ADS = 1 << 3; constexpr uint32_t IS_RELOADING = 1 << 4; constexpr uint32_t IS_RELOAD_EMPTY = 1 << 5; +constexpr uint32_t IS_DEAD = 1 << 6; } // namespace NetStateFlags //----------------------------------------------------------------------------- @@ -76,6 +77,9 @@ struct NetPlayerState { float yaw; // Camera yaw float pitch; // Camera pitch uint32_t stateFlags; // Bitfield of StateFlags + uint8_t health; // 0-200, server authoritative + uint8_t hitByPlayerId; // 0xFF = no hit, else attacker ID + uint16_t fireCounter; // Server-tracked fire count }; //----------------------------------------------------------------------------- @@ -125,7 +129,7 @@ struct Snapshot { //----------------------------------------------------------------------------- static_assert(sizeof(InputCmd) == 24, "InputCmd size changed - update network serialization"); -static_assert(sizeof(NetPlayerState) == 40, +static_assert(sizeof(NetPlayerState) == 44, "NetPlayerState size changed - update network serialization"); -static_assert(sizeof(RemotePlayerEntry) == 44, +static_assert(sizeof(RemotePlayerEntry) == 48, "RemotePlayerEntry size changed - update network serialization"); diff --git a/Network/server_raycast.h b/Network/server_raycast.h new file mode 100644 index 0000000..5851417 --- /dev/null +++ b/Network/server_raycast.h @@ -0,0 +1,163 @@ +#pragma once +//============================================================================= +// server_raycast.h +// +// Ray-Capsule intersection test for server-side hitscan. +// Pure math, no DirectXMath dependency — uses Float3 from net_common.h. +//============================================================================= + +#include "net_common.h" +#include + +namespace ServerRaycast { + +//----------------------------------------------------------------------------- +// Dot product +//----------------------------------------------------------------------------- +inline float Dot(const Float3& a, const Float3& b) +{ + return a.x * b.x + a.y * b.y + a.z * b.z; +} + +//----------------------------------------------------------------------------- +// Subtract +//----------------------------------------------------------------------------- +inline Float3 Sub(const Float3& a, const Float3& b) +{ + return { a.x - b.x, a.y - b.y, a.z - b.z }; +} + +//----------------------------------------------------------------------------- +// Add +//----------------------------------------------------------------------------- +inline Float3 Add(const Float3& a, const Float3& b) +{ + return { a.x + b.x, a.y + b.y, a.z + b.z }; +} + +//----------------------------------------------------------------------------- +// Scale +//----------------------------------------------------------------------------- +inline Float3 Scale(const Float3& v, float s) +{ + return { v.x * s, v.y * s, v.z * s }; +} + +//----------------------------------------------------------------------------- +// ClosestPointOnSegment — returns t in [0,1] +//----------------------------------------------------------------------------- +inline float ClosestTOnSegment(const Float3& segA, const Float3& segB, + const Float3& point) +{ + Float3 ab = Sub(segB, segA); + Float3 ap = Sub(point, segA); + float denom = Dot(ab, ab); + if (denom < 1e-8f) return 0.0f; + float t = Dot(ap, ab) / denom; + if (t < 0.0f) t = 0.0f; + if (t > 1.0f) t = 1.0f; + return t; +} + +//----------------------------------------------------------------------------- +// RayCapsule — test ray against a capsule (two hemispheres + cylinder) +// +// Returns true if the ray hits the capsule, and outT is the hit distance. +// Uses the "closest approach between two lines" method: +// Ray line: P = origin + t * dir +// Capsule segment: Q = segA + s * (segB - segA) +// Find (t, s) that minimize |P - Q|, check if distance <= radius. +// +// Parameters: +// origin — ray start position (eye position) +// dir — ray direction (normalized) +// capBottom — capsule foot position (bottom of capsule) +// capHeight — total height of capsule +// capRadius — capsule radius +// outT — [out] hit distance along ray (if hit) +// maxRange — maximum ray range (default 200m) +// +// Returns: true if hit within maxRange +//----------------------------------------------------------------------------- +inline bool RayCapsule(const Float3& origin, const Float3& dir, + const Float3& capBottom, float capHeight, float capRadius, + float& outT, float maxRange = 200.0f) +{ + // Capsule segment: A = bottom + (0, radius, 0), B = bottom + (0, height - radius, 0) + Float3 segA = { capBottom.x, capBottom.y + capRadius, capBottom.z }; + Float3 segB = { capBottom.x, capBottom.y + capHeight - capRadius, capBottom.z }; + + // Line-segment closest approach + // Ray: P(t) = origin + t * dir + // Seg: Q(s) = segA + s * segDir, where segDir = segB - segA + Float3 segDir = Sub(segB, segA); + Float3 w0 = Sub(origin, segA); + + float a = Dot(dir, dir); // always 1 if dir is normalized + float b = Dot(dir, segDir); + float c = Dot(segDir, segDir); + float d = Dot(dir, w0); + float e = Dot(segDir, w0); + + float denom = a * c - b * b; + + float t, s; + + if (denom < 1e-6f) + { + // Lines are nearly parallel + s = 0.0f; + t = -d / a; + } + else + { + s = (b * d - a * e) / denom; + t = (c * d - b * e) / denom; + } + + // Clamp s to [0, 1] (capsule segment) and recompute t + if (s < 0.0f) + { + s = 0.0f; + t = -d / a; // dot(dir, origin - segA) / dot(dir, dir) + } + else if (s > 1.0f) + { + s = 1.0f; + Float3 w1 = Sub(origin, segB); + t = -Dot(dir, w1) / a; + } + + // t must be positive (ray goes forward) and within range + if (t < 0.0f) t = 0.0f; + if (t > maxRange) return false; + + // Compute closest points + Float3 closestOnRay = Add(origin, Scale(dir, t)); + Float3 closestOnSeg = Add(segA, Scale(segDir, s)); + Float3 diff = Sub(closestOnRay, closestOnSeg); + float distSq = Dot(diff, diff); + + if (distSq <= capRadius * capRadius) + { + outT = t; + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// DirectionFromYawPitch — compute normalized direction vector +//----------------------------------------------------------------------------- +inline Float3 DirectionFromYawPitch(float yaw, float pitch) +{ + float cosPitch = cosf(pitch); + return { + sinf(yaw) * cosPitch, + -sinf(pitch), + cosf(yaw) * cosPitch + }; +} + +} // namespace ServerRaycast From 8a308c38a6e4dd6b4c0dffd2f93ef1ffa797fb55 Mon Sep 17 00:00:00 2001 From: pisces Date: Tue, 17 Feb 2026 18:03:14 +0900 Subject: [PATCH 07/13] Fix raycast pitch sign and eye height Lower the player eye offset to PLAYER_HEIGHT - 0.3f (approx. 1.7m) and correct the vertical component sign in DirectionFromYawPitch (use sinf(pitch) instead of -sinf(pitch)). These changes align the ray direction with the pitch convention and improve vertical aiming/hit detection accuracy. --- Network/game_server.cpp | 2 +- Network/server_raycast.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Network/game_server.cpp b/Network/game_server.cpp index de5d4ab..e3bea92 100644 --- a/Network/game_server.cpp +++ b/Network/game_server.cpp @@ -505,7 +505,7 @@ void GameServer::ProcessFiring(PlayerData& shooter, uint8_t shooterId) // Cast ray from eye position Float3 eyePos = { shooter.state.position.x, - shooter.state.position.y + PLAYER_HEIGHT - 0.1f, + shooter.state.position.y + PLAYER_HEIGHT - 0.3f, // 1.7m eye height shooter.state.position.z }; Float3 rayDir = ServerRaycast::DirectionFromYawPitch( diff --git a/Network/server_raycast.h b/Network/server_raycast.h index 5851417..dfd1751 100644 --- a/Network/server_raycast.h +++ b/Network/server_raycast.h @@ -155,7 +155,7 @@ inline Float3 DirectionFromYawPitch(float yaw, float pitch) float cosPitch = cosf(pitch); return { sinf(yaw) * cosPitch, - -sinf(pitch), + sinf(pitch), cosf(yaw) * cosPitch }; } From 95841d36b0df1fda309e01aff071b655da412b80 Mon Sep 17 00:00:00 2001 From: pisces Date: Wed, 18 Feb 2026 00:24:23 +0900 Subject: [PATCH 08/13] Update server_raycast.h --- Network/server_raycast.h | 145 ++++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 64 deletions(-) diff --git a/Network/server_raycast.h b/Network/server_raycast.h index dfd1751..d97b7aa 100644 --- a/Network/server_raycast.h +++ b/Network/server_raycast.h @@ -44,33 +44,39 @@ inline Float3 Scale(const Float3& v, float s) } //----------------------------------------------------------------------------- -// ClosestPointOnSegment — returns t in [0,1] +// RaySphere — test ray against a sphere +// +// Returns true if hit, outT = entry distance along ray. //----------------------------------------------------------------------------- -inline float ClosestTOnSegment(const Float3& segA, const Float3& segB, - const Float3& point) +inline bool RaySphere(const Float3& origin, const Float3& dir, + const Float3& center, float radius, float& outT) { - Float3 ab = Sub(segB, segA); - Float3 ap = Sub(point, segA); - float denom = Dot(ab, ab); - if (denom < 1e-8f) return 0.0f; - float t = Dot(ap, ab) / denom; - if (t < 0.0f) t = 0.0f; - if (t > 1.0f) t = 1.0f; - return t; + Float3 oc = Sub(origin, center); + float a = Dot(dir, dir); + float h = Dot(oc, dir); + float c = Dot(oc, oc) - radius * radius; + float disc = h * h - a * c; + if (disc < 0.0f) return false; + float sqrtDisc = sqrtf(disc); + float t = (-h - sqrtDisc) / a; + if (t < 0.0f) t = (-h + sqrtDisc) / a; + if (t < 0.0f) return false; + outT = t; + return true; } //----------------------------------------------------------------------------- -// RayCapsule — test ray against a capsule (two hemispheres + cylinder) +// RayCapsule — test ray against a capsule (cylinder + two hemispheres) // -// Returns true if the ray hits the capsule, and outT is the hit distance. -// Uses the "closest approach between two lines" method: -// Ray line: P = origin + t * dir -// Capsule segment: Q = segA + s * (segB - segA) -// Find (t, s) that minimize |P - Q|, check if distance <= radius. +// Decomposes the capsule into: +// 1. Infinite cylinder (clamped to segment extent) +// 2. Bottom hemisphere (sphere at segA) +// 3. Top hemisphere (sphere at segB) +// Returns the closest hit among all three. // // Parameters: -// origin — ray start position (eye position) -// dir — ray direction (normalized) +// origin — ray start position (eye position) +// dir — ray direction (normalized) // capBottom — capsule foot position (bottom of capsule) // capHeight — total height of capsule // capRadius — capsule radius @@ -83,67 +89,78 @@ inline bool RayCapsule(const Float3& origin, const Float3& dir, const Float3& capBottom, float capHeight, float capRadius, float& outT, float maxRange = 200.0f) { - // Capsule segment: A = bottom + (0, radius, 0), B = bottom + (0, height - radius, 0) + // Capsule segment endpoints (sphere centers) Float3 segA = { capBottom.x, capBottom.y + capRadius, capBottom.z }; Float3 segB = { capBottom.x, capBottom.y + capHeight - capRadius, capBottom.z }; - - // Line-segment closest approach - // Ray: P(t) = origin + t * dir - // Seg: Q(s) = segA + s * segDir, where segDir = segB - segA Float3 segDir = Sub(segB, segA); - Float3 w0 = Sub(origin, segA); - - float a = Dot(dir, dir); // always 1 if dir is normalized - float b = Dot(dir, segDir); - float c = Dot(segDir, segDir); - float d = Dot(dir, w0); - float e = Dot(segDir, w0); + float segLenSq = Dot(segDir, segDir); - float denom = a * c - b * b; + float bestT = maxRange + 1.0f; + bool hasHit = false; - float t, s; - - if (denom < 1e-6f) - { - // Lines are nearly parallel - s = 0.0f; - t = -d / a; - } - else + // 1. Test ray against infinite cylinder, clamp to segment extent + if (segLenSq > 1e-8f) { - s = (b * d - a * e) / denom; - t = (c * d - b * e) / denom; + float segLen = sqrtf(segLenSq); + Float3 axis = Scale(segDir, 1.0f / segLen); + Float3 oc = Sub(origin, segA); + + float dDotAxis = Dot(dir, axis); + float ocDotAxis = Dot(oc, axis); + + // Project out the capsule axis component + Float3 dPerp = Sub(dir, Scale(axis, dDotAxis)); + Float3 ocPerp = Sub(oc, Scale(axis, ocDotAxis)); + + float a = Dot(dPerp, dPerp); + float b = Dot(dPerp, ocPerp); + float c = Dot(ocPerp, ocPerp) - capRadius * capRadius; + + float disc = b * b - a * c; + if (disc >= 0.0f && a > 1e-8f) + { + float sqrtDisc = sqrtf(disc); + float t = (-b - sqrtDisc) / a; + if (t < 0.0f) t = (-b + sqrtDisc) / a; + + if (t >= 0.0f && t <= maxRange) + { + float hitOnAxis = ocDotAxis + t * dDotAxis; + if (hitOnAxis >= 0.0f && hitOnAxis <= segLen) + { + bestT = t; + hasHit = true; + } + } + } } - // Clamp s to [0, 1] (capsule segment) and recompute t - if (s < 0.0f) + // 2. Test against bottom hemisphere (sphere at segA) + float tSphere; + if (RaySphere(origin, dir, segA, capRadius, tSphere)) { - s = 0.0f; - t = -d / a; // dot(dir, origin - segA) / dot(dir, dir) + if (tSphere <= maxRange && tSphere < bestT) + { + bestT = tSphere; + hasHit = true; + } } - else if (s > 1.0f) + + // 3. Test against top hemisphere (sphere at segB) + if (RaySphere(origin, dir, segB, capRadius, tSphere)) { - s = 1.0f; - Float3 w1 = Sub(origin, segB); - t = -Dot(dir, w1) / a; + if (tSphere <= maxRange && tSphere < bestT) + { + bestT = tSphere; + hasHit = true; + } } - // t must be positive (ray goes forward) and within range - if (t < 0.0f) t = 0.0f; - if (t > maxRange) return false; - - // Compute closest points - Float3 closestOnRay = Add(origin, Scale(dir, t)); - Float3 closestOnSeg = Add(segA, Scale(segDir, s)); - Float3 diff = Sub(closestOnRay, closestOnSeg); - float distSq = Dot(diff, diff); - - if (distSq <= capRadius * capRadius) + if (hasHit) { - outT = t; + outT = bestT; return true; } - return false; } From 69d9492340b5275a0305451a4234c8ce137fab4a Mon Sep 17 00:00:00 2001 From: pisces Date: Wed, 18 Feb 2026 00:58:06 +0900 Subject: [PATCH 09/13] Update game_server.cpp --- Network/game_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Network/game_server.cpp b/Network/game_server.cpp index e3bea92..58874af 100644 --- a/Network/game_server.cpp +++ b/Network/game_server.cpp @@ -505,7 +505,7 @@ void GameServer::ProcessFiring(PlayerData& shooter, uint8_t shooterId) // Cast ray from eye position Float3 eyePos = { shooter.state.position.x, - shooter.state.position.y + PLAYER_HEIGHT - 0.3f, // 1.7m eye height + shooter.state.position.y + PLAYER_HEIGHT * 0.75f, // 1.5m eye height (match client camera) shooter.state.position.z }; Float3 rayDir = ServerRaycast::DirectionFromYawPitch( From c0d626461e8498540a02727c910f356ef8283d6d Mon Sep 17 00:00:00 2001 From: pisces Date: Wed, 18 Feb 2026 01:11:05 +0900 Subject: [PATCH 10/13] Adjust player collision size & eye height Update player collision constants to match the client model: PLAYER_HEIGHT reduced from 2.0 to 1.6 and CAPSULE_RADIUS from 0.5 to 0.3. Also change the eye position calc in ProcessFiring to use an explicit 1.5f offset (matching the client camera) instead of PLAYER_HEIGHT * 0.75f to keep server hit detection consistent with the client. --- Network/game_server.cpp | 2 +- Network/game_server.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Network/game_server.cpp b/Network/game_server.cpp index 58874af..00396e2 100644 --- a/Network/game_server.cpp +++ b/Network/game_server.cpp @@ -505,7 +505,7 @@ void GameServer::ProcessFiring(PlayerData& shooter, uint8_t shooterId) // Cast ray from eye position Float3 eyePos = { shooter.state.position.x, - shooter.state.position.y + PLAYER_HEIGHT * 0.75f, // 1.5m eye height (match client camera) + shooter.state.position.y + 1.5f, // eye height (match client camera) shooter.state.position.z }; Float3 rayDir = ServerRaycast::DirectionFromYawPitch( diff --git a/Network/game_server.h b/Network/game_server.h index f8f0048..c53b363 100644 --- a/Network/game_server.h +++ b/Network/game_server.h @@ -96,8 +96,8 @@ class GameServer std::vector m_Colliders; // Player collision parameters (must match client) - static constexpr float PLAYER_HEIGHT = 2.0f; - static constexpr float CAPSULE_RADIUS = 0.5f; + static constexpr float PLAYER_HEIGHT = 1.6f; + static constexpr float CAPSULE_RADIUS = 0.3f; // Weapon parameters per team static constexpr double RED_RPM = 600.0; From 1555113507c7a16b5c65d18934b06846b6f67658 Mon Sep 17 00:00:00 2001 From: pisces Date: Wed, 18 Feb 2026 09:51:08 +0900 Subject: [PATCH 11/13] Add world raycast and AABB ray test Prevent hits through world geometry by testing player raycasts against map colliders. ProcessFiring now computes worldDist via GameServer::RaycastWorld and only applies player damage if the player hit is closer than the world hit. Added GameServer::RaycastWorld (iterates m_Colliders and returns closest hit distance or a large value if none) and declared it in the header. Implemented ServerRaycast::RayAABB in server_raycast.h using the slab method (with a default maxRange = 200) to test ray vs. axis-aligned bounding boxes. --- Network/game_server.cpp | 28 +++++++++++++++++- Network/game_server.h | 1 + Network/server_raycast.h | 64 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/Network/game_server.cpp b/Network/game_server.cpp index 00396e2..0cbeb47 100644 --- a/Network/game_server.cpp +++ b/Network/game_server.cpp @@ -512,9 +512,12 @@ void GameServer::ProcessFiring(PlayerData& shooter, uint8_t shooterId) shooter.state.yaw, shooter.state.pitch); // Test against all other alive players (no friendly fire) + // Also test against world geometry — player hit only counts if closer than world uint8_t hitId = 0xFF; float hitDist = 0.0f; - if (RaycastPlayers(eyePos, rayDir, shooterId, shooter.teamId, hitId, hitDist)) + float worldDist = RaycastWorld(eyePos, rayDir); + if (RaycastPlayers(eyePos, rayDir, shooterId, shooter.teamId, hitId, hitDist) + && hitDist < worldDist) { // Apply damage auto hitIt = m_Players.find(hitId); @@ -580,3 +583,26 @@ bool GameServer::RaycastPlayers(const Float3& origin, const Float3& dir, return anyHit; } + +//----------------------------------------------------------------------------- +// RaycastWorld - Test ray against all map AABB colliders +// +// Returns the closest hit distance, or a very large value if no hit. +//----------------------------------------------------------------------------- +float GameServer::RaycastWorld(const Float3& origin, const Float3& dir) +{ + float closest = 99999.0f; + + for (const auto& col : m_Colliders) + { + float t = 0.0f; + if (ServerRaycast::RayAABB(origin, dir, + col.aabb.min, col.aabb.max, t)) + { + if (t < closest) + closest = t; + } + } + + return closest; +} diff --git a/Network/game_server.h b/Network/game_server.h index c53b363..43aa7ff 100644 --- a/Network/game_server.h +++ b/Network/game_server.h @@ -71,6 +71,7 @@ class GameServer bool RaycastPlayers(const Float3& origin, const Float3& dir, uint8_t excludeId, uint8_t excludeTeam, uint8_t& outHitId, float& outDist); + float RaycastWorld(const Float3& origin, const Float3& dir); void SimulatePlayerPhysics(PlayerData& player); void SimulatePhysics(); void BroadcastSnapshots(); diff --git a/Network/server_raycast.h b/Network/server_raycast.h index d97b7aa..c523d50 100644 --- a/Network/server_raycast.h +++ b/Network/server_raycast.h @@ -177,4 +177,68 @@ inline Float3 DirectionFromYawPitch(float yaw, float pitch) }; } +//----------------------------------------------------------------------------- +// RayAABB — test ray against an axis-aligned bounding box (slab method) +// +// Returns true if hit within maxRange, outT = entry distance along ray. +//----------------------------------------------------------------------------- +inline bool RayAABB(const Float3& origin, const Float3& dir, + const Float3& aabbMin, const Float3& aabbMax, + float& outT, float maxRange = 200.0f) +{ + float tMin = 0.0f; + float tMax = maxRange; + + // X axis + if (fabsf(dir.x) < 1e-8f) + { + if (origin.x < aabbMin.x || origin.x > aabbMax.x) return false; + } + else + { + float invD = 1.0f / dir.x; + float t1 = (aabbMin.x - origin.x) * invD; + float t2 = (aabbMax.x - origin.x) * invD; + if (t1 > t2) { float tmp = t1; t1 = t2; t2 = tmp; } + if (t1 > tMin) tMin = t1; + if (t2 < tMax) tMax = t2; + if (tMin > tMax) return false; + } + + // Y axis + if (fabsf(dir.y) < 1e-8f) + { + if (origin.y < aabbMin.y || origin.y > aabbMax.y) return false; + } + else + { + float invD = 1.0f / dir.y; + float t1 = (aabbMin.y - origin.y) * invD; + float t2 = (aabbMax.y - origin.y) * invD; + if (t1 > t2) { float tmp = t1; t1 = t2; t2 = tmp; } + if (t1 > tMin) tMin = t1; + if (t2 < tMax) tMax = t2; + if (tMin > tMax) return false; + } + + // Z axis + if (fabsf(dir.z) < 1e-8f) + { + if (origin.z < aabbMin.z || origin.z > aabbMax.z) return false; + } + else + { + float invD = 1.0f / dir.z; + float t1 = (aabbMin.z - origin.z) * invD; + float t2 = (aabbMax.z - origin.z) * invD; + if (t1 > t2) { float tmp = t1; t1 = t2; t2 = tmp; } + if (t1 > tMin) tMin = t1; + if (t2 < tMax) tMax = t2; + if (tMin > tMax) return false; + } + + outT = tMin; + return true; +} + } // namespace ServerRaycast From 7f3ea8e673ae5eafe584f39ad0a24ffca30201fe Mon Sep 17 00:00:00 2001 From: pisces Date: Wed, 18 Feb 2026 11:37:51 +0900 Subject: [PATCH 12/13] Refactor map colliders and randomize spawns Convert map data to a grid-based layout and simplify collider defs: replace the old MapCategory/cube-based entries with a MAP_GRID, map constants, and merged AABB entries (MAP_COLLIDERS now contains explicit min/max boxes only). Update MapColliderDef to only store explicit AABB + isGround and remove cube auto-AABB handling. Adjust GameServer to read the new colliders (always use the provided AABB) and add for rand(). Change GetSpawnPosition to pick a random position within one of four corner spawn areas (randomized X/Z per spawn); playerId/teamId are no longer used for deterministic offsets. This reduces server-side complexity and centralizes map geometry, but note spawn behavior is now nondeterministic unless srand is set elsewhere. --- Network/game_server.cpp | 47 +++++++++--------- Network/map_colliders.h | 104 +++++++++++++++++++++++++++------------- 2 files changed, 96 insertions(+), 55 deletions(-) diff --git a/Network/game_server.cpp b/Network/game_server.cpp index 0cbeb47..79916f5 100644 --- a/Network/game_server.cpp +++ b/Network/game_server.cpp @@ -10,6 +10,7 @@ #include "map_colliders.h" #include #include +#include GameServer::GameServer() : m_pNetwork(nullptr) @@ -38,22 +39,10 @@ void GameServer::Initialize(ENetServerNetwork* pNetwork) { const MapColliderDef& def = MAP_COLLIDERS[i]; ServerCollider sc; - if (def.category == MAP_CUBE) - { - // Cube: AABB = position ± 0.5 - sc.aabb = { - { def.posX - 0.5f, def.posY - 0.5f, def.posZ - 0.5f }, - { def.posX + 0.5f, def.posY + 0.5f, def.posZ + 0.5f } - }; - } - else - { - // Explicit AABB - sc.aabb = { - { def.minX, def.minY, def.minZ }, - { def.maxX, def.maxY, def.maxZ } - }; - } + sc.aabb = { + { def.minX, def.minY, def.minZ }, + { def.maxX, def.maxY, def.maxZ } + }; sc.isGround = def.isGround; m_Colliders.push_back(sc); } @@ -80,15 +69,27 @@ uint8_t GameServer::AssignTeam() const } //----------------------------------------------------------------------------- -// Spawn position per team + player ID +// Spawn position — random within one of 4 corner areas (4x4 each) +// +// Corner A (top-left): X: -9 to -5, Z: -9 to -5 +// Corner B (top-right): X: 5 to 9, Z: -9 to -5 +// Corner C (bottom-left): X: -9 to -5, Z: 5 to 9 +// Corner D (bottom-right): X: 5 to 9, Z: 5 to 9 //----------------------------------------------------------------------------- -Float3 GameServer::GetSpawnPosition(uint8_t playerId, uint8_t teamId) +Float3 GameServer::GetSpawnPosition(uint8_t /*playerId*/, uint8_t /*teamId*/) { - float offsets[] = { 0.0f, 5.0f, -5.0f, 10.0f }; - float x = offsets[playerId % 4]; - if (teamId == PlayerTeam::BLUE) - x = -x; // Mirror for blue team - float z = (teamId == PlayerTeam::RED) ? -20.0f : 20.0f; + struct SpawnArea { float minX, maxX, minZ, maxZ; }; + static const SpawnArea areas[4] = { + { -9.0f, -5.0f, -9.0f, -5.0f }, // top-left + { 5.0f, 9.0f, -9.0f, -5.0f }, // top-right + { -9.0f, -5.0f, 5.0f, 9.0f }, // bottom-left + { 5.0f, 9.0f, 5.0f, 9.0f }, // bottom-right + }; + + int corner = rand() % 4; + const SpawnArea& a = areas[corner]; + float x = a.minX + static_cast(rand()) / RAND_MAX * (a.maxX - a.minX); + float z = a.minZ + static_cast(rand()) / RAND_MAX * (a.maxZ - a.minZ); return { x, 0.0f, z }; } diff --git a/Network/map_colliders.h b/Network/map_colliders.h index 4387cd4..5831f88 100644 --- a/Network/map_colliders.h +++ b/Network/map_colliders.h @@ -2,53 +2,93 @@ //============================================================================= // map_colliders.h // -// Shared map collider definitions. +// Shared map data definitions. // This file is used by BOTH client and server — keep in sync! // No DirectXMath dependency — pure C structs. +// +// MAP_GRID[][] defines the obstacle layout (1=block, 0=empty). +// MAP_COLLIDERS[] defines merged AABBs for physics/hitscan. +// The client generates individual cube draw calls from the grid; +// the server only reads MAP_COLLIDERS[]. //============================================================================= -// Object categories -enum MapCategory +//----------------------------------------------------------------------------- +// Grid-based map layout +//----------------------------------------------------------------------------- +static const int MAP_GRID_ROWS = 20; +static const int MAP_GRID_COLS = 20; +static const int MAP_BLOCK_HEIGHT = 4; // cubes stacked per obstacle cell +static const float MAP_OFFSET_X = -10.0f; // world X offset (center the grid) +static const float MAP_OFFSET_Z = -10.0f; // world Z offset (center the grid) + +static const int MAP_GRID[MAP_GRID_ROWS][MAP_GRID_COLS] = { - MAP_GROUND = 0, // Ground plane — rendered as MeshField, has AABB - MAP_CUBE = 1, // Cube block — rendered with Cube_Draw, AABB from position - MAP_WALL = 2, // Invisible wall — AABB only, no rendering + {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1}, + {1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1}, + {1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1}, + {1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1}, + {1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, }; -// Collider definition -// For MAP_CUBE: AABB is auto-computed from position (±0.5 unit cube) -// For MAP_GROUND / MAP_WALL: AABB is specified explicitly via min/max +//----------------------------------------------------------------------------- +// Collider definition (physics / hitscan) +//----------------------------------------------------------------------------- struct MapColliderDef { - int category; - float posX, posY, posZ; // position (used for cube rendering & AABB center) - float minX, minY, minZ; // AABB min (explicit, ignored for MAP_CUBE) - float maxX, maxY, maxZ; // AABB max (explicit, ignored for MAP_CUBE) - bool isGround; // true = player can land on this surface + float minX, minY, minZ; + float maxX, maxY, maxZ; + bool isGround; }; // ============================================================================ -// MAP DATA — Add new colliders here! +// COLLIDERS — Manually merged AABBs for physics/hitscan. +// +// Grid cell (row, col) maps to world: +// X = col + MAP_OFFSET_X, Z = row + MAP_OFFSET_Z +// A group from (r1,c1)-(r2,c2) → AABB: +// min = (c1-10, 0, r1-10) max = (c2+1-10, 4, r2+1-10) // ============================================================================ static const MapColliderDef MAP_COLLIDERS[] = { - // === Ground === - // cat pos(x,y,z) AABB min AABB max ground - { MAP_GROUND, 0,0,0, -128.0f,-1.0f,-128.0f, 128.0f, 0.0f, 128.0f, true }, - - // === Cube Blocks (AABB auto = pos ± 0.5) === - // y=0.5 → AABB [0,1] (sits on ground) | y=1.5 → AABB [1,2] (second layer) - // cat pos(x,y,z) (min/max ignored for cubes) ground - { MAP_CUBE, 7.5f, 0.5f, 7.5f, 0,0,0, 0,0,0, true }, - { MAP_CUBE, 7.5f, 0.5f, 8.5f, 0,0,0, 0,0,0, true }, - { MAP_CUBE, 8.5f, 0.5f, 7.5f, 0,0,0, 0,0,0, true }, - { MAP_CUBE, 8.5f, 0.5f, 8.5f, 0,0,0, 0,0,0, true }, - { MAP_CUBE, 7.5f, 1.5f, 7.5f, 0,0,0, 0,0,0, true }, - { MAP_CUBE, 8.5f, 1.5f, 7.5f, 0,0,0, 0,0,0, true }, - - // === Invisible Walls (map boundary) === - // cat pos(unused) AABB min AABB max ground - // (Add map boundary walls here if needed) + // AABB min AABB max ground + + // --- Ground plane --- + { -128.0f,-1.0f,-128.0f, 128.0f, 0.0f, 128.0f, true }, + + // --- Border walls --- + // Top wall: row 0, cols 0-19 + { -10.0f, 0.0f,-10.0f, 10.0f, 4.0f, -9.0f, true }, + // Bottom wall: row 19, cols 0-19 + { -10.0f, 0.0f, 9.0f, 10.0f, 4.0f, 10.0f, true }, + // Left wall: rows 1-18, col 0 + { -10.0f, 0.0f, -9.0f, -9.0f, 4.0f, 9.0f, true }, + // Right wall: rows 1-18, col 19 + { 9.0f, 0.0f, -9.0f, 10.0f, 4.0f, 9.0f, true }, + + // --- Interior blocks (3x3 each, height 4) --- + // Block A: rows 5-7, cols 5-7 + { -5.0f, 0.0f, -5.0f, -2.0f, 4.0f, -2.0f, true }, + // Block B: rows 5-7, cols 12-14 + { 2.0f, 0.0f, -5.0f, 5.0f, 4.0f, -2.0f, true }, + // Block C: rows 12-14, cols 5-7 + { -5.0f, 0.0f, 2.0f, -2.0f, 4.0f, 5.0f, true }, + // Block D: rows 12-14, cols 12-14 + { 2.0f, 0.0f, 2.0f, 5.0f, 4.0f, 5.0f, true }, }; static const int MAP_COLLIDER_COUNT = sizeof(MAP_COLLIDERS) / sizeof(MAP_COLLIDERS[0]); From b9273969af7e45fbd6733b19578eecf1f5eaa6a8 Mon Sep 17 00:00:00 2001 From: pisces Date: Wed, 18 Feb 2026 12:25:15 +0900 Subject: [PATCH 13/13] Update map_colliders.h --- Network/map_colliders.h | 50 +++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/Network/map_colliders.h b/Network/map_colliders.h index 5831f88..b04b88d 100644 --- a/Network/map_colliders.h +++ b/Network/map_colliders.h @@ -23,26 +23,26 @@ static const float MAP_OFFSET_Z = -10.0f; // world Z offset (center the grid) static const int MAP_GRID[MAP_GRID_ROWS][MAP_GRID_COLS] = { - {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, - {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, - {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, - {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, - {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, - {1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1}, - {1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1}, - {1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1}, - {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, - {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, - {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, - {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, - {1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1}, - {1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1}, - {1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1}, - {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, - {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, - {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, - {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, - {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0}, + {0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0}, + {0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0}, + {0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0}, + {0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, }; //----------------------------------------------------------------------------- @@ -70,16 +70,6 @@ static const MapColliderDef MAP_COLLIDERS[] = // --- Ground plane --- { -128.0f,-1.0f,-128.0f, 128.0f, 0.0f, 128.0f, true }, - // --- Border walls --- - // Top wall: row 0, cols 0-19 - { -10.0f, 0.0f,-10.0f, 10.0f, 4.0f, -9.0f, true }, - // Bottom wall: row 19, cols 0-19 - { -10.0f, 0.0f, 9.0f, 10.0f, 4.0f, 10.0f, true }, - // Left wall: rows 1-18, col 0 - { -10.0f, 0.0f, -9.0f, -9.0f, 4.0f, 9.0f, true }, - // Right wall: rows 1-18, col 19 - { 9.0f, 0.0f, -9.0f, 10.0f, 4.0f, 9.0f, true }, - // --- Interior blocks (3x3 each, height 4) --- // Block A: rows 5-7, cols 5-7 { -5.0f, 0.0f, -5.0f, -2.0f, 4.0f, -2.0f, true },