Files

12 KiB
Raw Permalink Blame History

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