12 KiB
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
MultiplayerPeerENetis 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
seqso 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:
- Applies the input to its local player immediately.
- Renders the predicted position.
- Sends the input to the server (batched, with
seq).
5.2 Reconciliation
- The server sends authoritative
snapshot_stateat 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:
- Find the last predicted state that matches the server's tick.
- Compute the error between predicted and authoritative position.
- If error exceeds a threshold, snap or smoothly interpolate the local player to the authoritative position (error correction).
- 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) + 2predicted 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
propsdictionary 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
- Connect: Client opens an ENet connection to the server.
- Authenticate / Join: Client sends
request_join; server validates and assigns apeer_idand spawns the player entity. - Spawn: Server broadcasts
spawn_entityfor the new player to all clients. - Play: Input → prediction → server simulation → snapshot → reconciliation.
- 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
seqare 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 |