# Multiplayer Networking Architecture > **Status:** Proposed > **Owner:** Platform team > **Related:** Server authority + client prediction ## 1. Overview This document specifies the multiplayer networking architecture for **agp-game**, a Godot-based game built by autonomous agents. The architecture follows a **server-authoritative model with client-side prediction and reconciliation**, which is the industry standard for fast-paced, competitive multiplayer games. The goals of this architecture are: - **Fairness & anti-cheat:** The server is the single source of truth for all gameplay state. Clients never directly mutate world state. - **Responsiveness:** Client-side prediction hides network latency so local input feels immediate. - **Determinism:** The server runs a fixed-timestep simulation so that state is reproducible and testable. - **Scalability:** A clear separation between transport, simulation, and replication layers allows the server to be tuned independently. --- ## 2. Transport Choice: ENet over WebSocket ### 2.1 Decision We choose **ENet** as the primary transport for gameplay traffic. ### 2.2 Justification | Criterion | ENet | WebSocket (TCP) | |----------------------|----------------------------------------|-------------------------------------| | **Reliability model**| Reliable + unreliable channels | Reliable only (TCP) | | **Latency** | Low; unreliable packets skip retransmit| Head-of-line blocking on loss | | **Packet ordering** | Per-channel ordering | Global strict ordering | | **Congestion control**| Lightweight, game-oriented | TCP-style (slow start, backoff) | | **Godot support** | First-class (`ENetConnection`, `MultiplayerPeerENet`) | Supported but TCP-based | | **Use case fit** | Real-time action game | Chat, lobby, HTTP-style messaging | **Rationale:** - **Movement & state replication** are latency-sensitive and tolerate occasional packet loss. ENet's *unreliable* channel lets us send position snapshots at high frequency without head-of-line blocking. - **RPCs that must be delivered** (e.g., interactions, spawns) use ENet's *reliable* channel, giving us exactly the semantics we need without TCP's retransmission stalls. - Godot's `MultiplayerPeerENet` is mature and integrates directly with the high-level multiplayer API (`MultiplayerAPI`, `rpc()`). ### 2.3 WebSocket as a Secondary Transport WebSocket remains available as a **secondary transport** for: - **Lobby / matchmaking** messaging (low-frequency, must-be-reliable). - **Browser builds** where raw UDP/ENet is unavailable. - **Admin / telemetry** channels. The transport layer is abstracted behind an interface so gameplay code does not depend on the underlying protocol. --- ## 3. Server-Side Game Loop ### 3.1 Tick Rate - **Simulation tick rate:** **30 Hz** (33.33 ms per tick). - **Replication / snapshot rate:** **30 Hz**, aligned with the simulation tick. **Justification:** - 30 Hz is sufficient for smooth movement and interaction in a top-down / 2D action game while keeping CPU and bandwidth costs predictable. - A fixed timestep (rather than delta-time) makes the simulation deterministic and testable. - The tick rate is a **configuration constant** so it can be raised (e.g., to 60 Hz) for competitive modes without architectural changes. ### 3.2 Authority Model - **Server is authoritative** for: - Player position, velocity, and physics state. - Health, damage, and death. - Item pickups, inventory, and world-object state. - Spawning and despawning of entities. - Game rules, scoring, and win/lose conditions. - **Client is authoritative** for: - Its own *input* (which keys/buttons are pressed). - Local camera and UI. - Cosmetic / non-gameplay presentation. ### 3.3 Server Loop Pseudocode ``` while server_running: process_network_inbound() # read client inputs & RPCs for _ in range(ticks_to_advance): simulate_tick() # fixed 30 Hz step broadcast_snapshots() # replicate authoritative state process_network_outbound() # flush reliable/unreliable channels ``` The server accumulates real elapsed time and advances the simulation in fixed `TICK_DURATION` increments, decoupling simulation from frame rate. --- ## 4. RPC Call Patterns RPCs are grouped by direction and reliability. All gameplay RPCs are routed through the high-level `MultiplayerAPI` with explicit `rpc_id` targeting. ### 4.1 Client → Server (Input & Requests) | RPC | Channel | Payload | Notes | |------------------------------|-----------|-------------------------------------------|-------| | `request_move_input` | unreliable| `{ seq, input_state, timestamp }` | Batched per tick; drives prediction | | `request_interact` | reliable | `{ target_id }` | Open door, pick up item, use object | | `request_attack` | reliable | `{ target_id, aim_dir }` | Combat action | | `request_join` | reliable | `{ player_name, profile }` | Session setup | | `request_respawn` | reliable | `{}` | After death | **Design notes:** - **Input is batched** per client tick and sent with a monotonically increasing `seq` so the server can detect gaps and the client can reconcile. - **Interactions are reliable** because they are discrete, must-happen events (opening a door, picking up an item). Losing them would corrupt world state. ### 4.2 Server → Client (State & Events) | RPC | Channel | Payload | Notes | |------------------------------|-----------|-------------------------------------------|-------| | `snapshot_state` | unreliable| `{ tick, entities[], player_states[] }` | Periodic authoritative state | | `spawn_entity` | reliable | `{ entity_id, type, transform, props }` | New entity enters world | | `despawn_entity` | reliable | `{ entity_id, reason }` | Entity removed | | `interact_result` | reliable | `{ request_seq, success, result }` | Acknowledges interaction | | `damage_event` | reliable | `{ target_id, amount, source_id }` | Combat feedback | | `game_event` | reliable | `{ type, payload }` | Score, win, round start/end | ### 4.3 Broadcast vs. Targeted - **Broadcast (all clients):** `snapshot_state`, `spawn_entity`, `despawn_entity`, `game_event`. - **Targeted (single client):** `interact_result`, `damage_event` (to affected players), and any player-private data. --- ## 5. Client-Side Prediction & Reconciliation ### 5.1 Prediction - The client runs a **local copy of the movement simulation** using the same physics constants as the server. - When the player provides input, the client: 1. Applies the input to its local player immediately. 2. Renders the predicted position. 3. Sends the input to the server (batched, with `seq`). ### 5.2 Reconciliation - The server sends authoritative `snapshot_state` at 30 Hz containing the server's tick number and authoritative player positions. - The client keeps a **ring buffer of predicted states** keyed by input `seq`. - On receiving a snapshot: 1. Find the last predicted state that matches the server's tick. 2. Compute the **error** between predicted and authoritative position. 3. If error exceeds a threshold, **snap or smoothly interpolate** the local player to the authoritative position (error correction). 4. Re-simulate any inputs that occurred after the server's tick (rollback). ### 5.3 Interpolation of Remote Entities - **Local player:** predicted + reconciled (see above). - **Remote players / entities:** rendered with **interpolation delay** of `2 × TICK_DURATION` (≈ 66 ms) to smooth out snapshot jitter. - The client buffers the last N snapshots and interpolates between them. ### 5.4 Prediction Buffer Sizing - Buffer size is adaptive based on measured RTT. - Default: hold up to `ceil(RTT / TICK_DURATION) + 2` predicted states. --- ## 6. Entity & World State Replication ### 6.1 Entity Model Every networked entity has: - A globally unique `entity_id` (server-assigned). - An `EntityType` (player, enemy, item, projectile, door, ...). - A `Transform` (position, rotation). - A typed `props` dictionary of gameplay state (health, state machine, etc.). ### 6.2 Snapshot Format ``` snapshot_state: tick: uint32 entities: - entity_id: uint32 type: uint8 transform: { x, y, rot } props: { ... } # delta-encoded where possible player_states: - peer_id: int entity_id: uint32 health: int state: uint8 ``` ### 6.3 Delta Compression - Only **changed** entity fields are sent per snapshot (dirty-flag per entity). - Static or unchanged entities are omitted from the snapshot. - This keeps bandwidth proportional to *activity*, not world size. --- ## 7. Connection & Session Lifecycle 1. **Connect:** Client opens an ENet connection to the server. 2. **Authenticate / Join:** Client sends `request_join`; server validates and assigns a `peer_id` and spawns the player entity. 3. **Spawn:** Server broadcasts `spawn_entity` for the new player to all clients. 4. **Play:** Input → prediction → server simulation → snapshot → reconciliation. 5. **Disconnect:** Server removes the player entity, broadcasts `despawn_entity`, and cleans up session state. --- ## 8. Security & Anti-Cheat - **No client-authoritative gameplay values.** Clients only send *inputs* and *requests*; the server computes all outcomes. - **Input validation:** Server clamps movement speed, rejects out-of-bounds positions, and validates interaction targets. - **Rate limiting:** Per-peer input and RPC rate limits prevent flooding. - **Sequence validation:** Gaps or replays in input `seq` are detected and rejected. --- ## 9. Testing & Determinism - The simulation is a **pure function of (state, inputs, tick)** so it can be unit-tested headlessly. - A **replay system** records inputs and can re-simulate to verify server and client produce identical results. - Network code is tested with a **mock transport** that can inject latency, jitter, and packet loss. --- ## 10. Open Questions / Future Work - **Interest management / spatial partitioning:** For large worlds, restrict snapshots to entities near each client (AOI culling). - **Server-side physics:** Decide whether to use Godot's physics server on the host or a custom deterministic physics step. - **Dedicated server vs. listen server:** The architecture supports both; a dedicated headless server is the target for production. - **WebSocket fallback:** Implement the transport abstraction for browser builds. --- ## 11. Summary | Concern | Decision | |------------------------|-------------------------------------------------| | Transport | ENet (primary), WebSocket (secondary) | | Tick rate | 30 Hz fixed timestep | | Authority | Server-authoritative; client sends input only | | Prediction | Client-side prediction + reconciliation | | Remote interpolation | 2-tick interpolation delay | | Replication | Delta-compressed snapshots at 30 Hz | | Interaction RPCs | Reliable channel | | Movement input | Unreliable channel, batched with sequence |