Claude
Skills
Sign in
Back

godot-adapt-single-to-multiplayer

Included with Lifetime
$97 forever

Expert patterns for adding multiplayer to single-player games including client-server architecture, authoritative server design, MultiplayerSynchronizer, lag compensation (client prediction, server reconciliation), input buffering, and anti-cheat measures. Use when retrofitting multiplayer, porting to online play, or designing networked gameplay. Trigger keywords: MultiplayerPeer, ENetMultiplayerPeer, SceneMultiplayer, MultiplayerSynchronizer, rpc, rpc_id, multiplayer_authority, client_prediction, server_reconciliation, lag_compensation, rollback.

Designscripts

What this skill does


# Adapt: Single to Multiplayer

Expert guidance for retrofitting multiplayer into single-player games.

## NEVER Do (Expert Multiplayer Rules)

### Security & Authority
- **NEVER trust client-reported state** — Clients own their 'Input', NOT their 'Position' or 'Health'. Server must validate every coordinate and health change.
- **NEVER use `get_tree()` groups for authority checks** — Use `is_multiplayer_authority()`. Group registration is non-deterministic in high-latency joins.
- **NEVER allow unrestricted RPC rates** — A malicious client can call a 'FireWeapon' RPC 10,000 times per second. Always implement rate-limiting (`net_rpc_rate_limiter.gd`).

### Movement & Lag
- **NEVER skip Client-Side Prediction** — Movement without prediction feels 'heavy' and unresponsive. Predict movement locally, then correct only on server disagreement.
- **NEVER sync peers at 60Hz** — Sending entire state every frame will saturate client bandwidth. Use a lower tick-rate (20-30Hz) and interpolate between packets.
- **NEVER snap peer positions** — Abrupt position updates cause 'jitter'. Store a buffer of past states and lerp between them with a 100ms delay.

### Bandwidth & Sync
- **NEVER sync 'Full Floats' if possible** — Quantize Vector3 data (truncating decimals) to save 50%+ bandwidth. Use `MultiplayerSynchronizer` with delta-sync enabled.
- **NEVER ignore 'Late Joiners'** — Players who join mid-game won't see existing environmental changes. Broadcast a full world-state 'Snapshot' on peer connection.
- **NEVER test on 0ms ping** — Everything works on localhost. Use a simulator (`net_latency_simulator.gd`) with 150ms ping to identify sync bugs.

---

## Available Scripts

> **MANDATORY**: Read the appropriate script before implementing the corresponding pattern.

### [net_prediction_reconciliation.gd](scripts/net_prediction_reconciliation.gd)
Expert CharacterBody3D prediction with input-buffer replaying for server reconciliation.

### [net_snapshot_interpolation.gd](scripts/net_snapshot_interpolation.gd)
Professional snapshot interpolation logic for smoothing peer movement via jitter buffers.

### [net_auth_server_validator.gd](scripts/net_auth_server_validator.gd)
Authoritative server validator for anti-cheat (Position, Speed, and Action checks).

### [net_rpc_rate_limiter.gd](scripts/net_rpc_rate_limiter.gd)
Expert rate-limiter to prevent RPC flooding and macro-abuse by clients.

### [net_interest_management.gd](scripts/net_interest_management.gd)
Distance-based visibility management to optimize binary bandwidth per-peer.

### [net_delta_compression_sync.gd](scripts/net_delta_compression_sync.gd)
Expert quantization and significance-checking logic for delta-compression.

### [net_upnp_discovery_logic.gd](scripts/net_upnp_discovery_logic.gd)
Robust script for P2P network discovery and automatic port forwarding via UPNP.

### [net_debug_overlay_monitor.gd](scripts/net_debug_overlay_monitor.gd)
In-game diagnostic overlay reporting RTT (Ping), Packet Loss, and Jitter.

### [net_lag_compensation.gd](scripts/net_lag_compensation.gd)
Expert server-side state rewinding (Lag Compensation) for accurate hit-registration.

### [net_lobby_late_join_sync.gd](scripts/net_lobby_late_join_sync.gd)
Professional state-initialization logic to bridge 'Late Joiners' into a synced session.

### [net_latency_simulator.gd](scripts/net_latency_simulator.gd)
Editor-only tool for simulating high-ping and loss conditions for stress-testing.

---

## Architecture Patterns

### Pattern 1: Authoritative Server (Recommended)

```gdscript
# Server validates ALL gameplay logic
# Clients send inputs → Server processes → Server broadcasts state

# Pros: Secure, prevents cheating
# Cons: Requires server hosting, lag affects gameplay

# Use for: Competitive games, PvP, games with economies
```

### Pattern 2: Peer-to-Peer (Lockstep)

```gdscript
# All clients run identical simulation
# Inputs synced, deterministic physics

# Pros: No dedicated server needed
# Cons: Vulnerable to cheating, desyncs common

# Use for: Co-op, casual games, small player counts (2-4)
```

### Pattern 3: Hybrid (Authority Transfer)

```gdscript
# Host acts as server
# Authority can transfer between peers

# Use for: 4-8 player co-op, party games
```

---

## Step-by-Step Migration

### Step 1: Separate Input from Logic

```gdscript
# ❌ BAD: Input directly modifies state (single-player)
extends CharacterBody2D

func _physics_process(delta: float) -> void:
    var input := Input.get_vector("left", "right", "up", "down")
    velocity = input.normalized() * SPEED
    move_and_slide()

# ✅ GOOD: Input → Logic separation

extends CharacterBody2D

var current_input := Vector2.ZERO

func _physics_process(delta: float) -> void:
    # Only read input if this is OUR player
    if is_multiplayer_authority():
        current_input = Input.get_vector("left", "right", "up", "down")
        # Send input to server (if we're client)
        if multiplayer.get_unique_id() != 1:  # Not server
            rpc_id(1, "receive_input", current_input)
    
    # EVERYONE processes movement (server + all clients)
    _process_movement(delta, current_input)

func _process_movement(delta: float, input: Vector2) -> void:
    velocity = input.normalized() * SPEED
    move_and_slide()

@rpc("any_peer", "call_remote", "unreliable")
func receive_input(input: Vector2) -> void:
    # Server receives client input
    current_input = input
```

### Step 2: Set Up Multiplayer Authority

```gdscript
# server_setup.gd
extends Node

const PORT = 7777
const MAX_PLAYERS = 4

func host_game() -> void:
    var peer := ENetMultiplayerPeer.new()
    peer.create_server(PORT, MAX_PLAYERS)
    multiplayer.multiplayer_peer = peer
    
    multiplayer.peer_connected.connect(_on_player_connected)
    multiplayer.peer_disconnected.connect(_on_player_disconnected)
    
    print("Server started on port %d" % PORT)

func join_game(ip: String) -> void:
    var peer := ENetMultiplayerPeer.new()
    peer.create_client(ip, PORT)
    multiplayer.multiplayer_peer = peer
    
    print("Connecting to %s:%d" % [ip, PORT])

func _on_player_connected(id: int) -> void:
    print("Player %d connected" % id)
    spawn_player(id)

func _on_player_disconnected(id: int) -> void:
    print("Player %d disconnected" % id)
    despawn_player(id)

func spawn_player(id: int) -> void:
    var player := preload("res://player.tscn").instantiate()
    player.name = str(id)  # CRITICAL: Name must be unique and match peer ID
    player.set_multiplayer_authority(id)  # Client owns their own player
    get_node("/root/World").add_child(player, true)  # true = replicate to all peers
```

### Step 3: Add MultiplayerSynchronizer

```gdscript
# Scene structure:
# Player (CharacterBody2D)
#   ├─ Sprite2D
#   ├─ CollisionShape2D
#   └─ MultiplayerSynchronizer

# MultiplayerSynchronizer setup (in editor):
# - Root Path: "../"  (points to Player node)
# - Replication Interval: 0.05  (20Hz updates)
# - Public Visibility: true
# - Synchronized Properties:
#     - position
#     - rotation
#     - velocity (optional, for interpolation)

# No code needed! MultiplayerSynchronizer auto-syncs properties
```

---

## Client Prediction & Server Reconciliation

### Problem: Lag Makes Game Feel Unresponsive

```gdscript
# Without prediction:
# 1. Client presses W
# 2. Input sent to server
# 3. Server processes (50ms later)
# 4. Server sends back position
# 5. Client sees movement (100ms RTT)
# Result: 100ms delay between input and visual feedback
```

### Solution: Client-Side Prediction

```gdscript
# player_controller.gd
extends CharacterBody2D

var input_buffer: Array = []
var server_state := {"position": Vector2.ZERO, "tick": 0}

func _physics_process(delta: float) -> void:
    if is_multiplayer_authority():
        var input := Input.get_vector("left", "right", "up", "down")
        
        # Client predicts movement IMMEDIATELY
        var tick := Engine.get_physics_frames()
        input_buffer.append({"input": input

Related in Design