# 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. --- ## 7a. Server Orchestration (implemented, PC3 Phase 2) **One dedicated server process per world**, spawned on demand. ``` client backend (single worker) host │ POST /api/worlds/{id}/join (bearer JWT) │ │ ───────────────────────────▶ GameServerSupervisor │ │ ensure_server(world_id): │ │ if none running for world ─── spawn: │ godot --headless │ res://scenes/server.tscn -- │ --port
--world