Skills/godot-networking/SKILL.md

9.9 KiB

name description license compatibility metadata
godot-networking Networking and multiplayer for Godot 4.x: Multiplayer API with RPC system, authority models (server-authoritative vs client-predictive), replication patterns (rpc(), rset(), rset_group()), connection management, lag compensation, and Steam Frame integration. Use when building multiplayer games, networked simulations, or online features in Godot. MIT
godot-4.0
godot-4.1
godot-4.2
godot-4.3
godot-4.4
godot-4.5
godot-4.6
godot-4.7
author version tags created
vl4dt 0.1.0
godot
networking
multiplayer
rpc
replication
steam-frame
2026-06-20

Networking and Multiplayer for Godot 4.x

Comprehensive patterns for multiplayer, RPC communication, and network synchronization in Godot 4.x.

Multiplayer API (Godot 4.x RPC System)

Setting Up Multiplayer

# In _ready(), set the peer and peer ID
func _ready():
    var peer = ENetMultiplayerPeer.new()
    peer.create_client("127.0.0.1", 9000)
    multiplayer.multiplayer_peer = peer
    multiplayer.server_id = 1  # Server is always ID 1

RPC Methods

# Remote: Call only from non-owner
@rpc("reliable", "call_remote")
func rpc_move_player(position: Vector3):
    global_position = position

# Reliable RPC (ordered, guaranteed delivery)
@rpc("reliable", "authority", "call_remote")
func rpc_take_damage(amount: int, source_id: int):
    health -= amount
    if health <= 0:
        rpc("rpc_die", source_id)

# Unreliable RPC (faster, may drop packets — good for position updates)
@rpc("unreliable", "authority", "call_remote")
func rpc_update_position(position: Vector3, rotation: float):
    global_position = position
    rotation_degrees = rotation

RPC Reliability Options

Mode Use Case
"reliable" Damage, state changes, game events
"unreliable" Position updates, player movement
"unreliable_ordered" Ordered but fast (chat messages)
"call_local" Only on the node owner
"call_remote" On all non-owner nodes
"call_authority" Only on the authority node

Authority Models

Server-Authoritative Model

The server has final say on all game state. Clients send inputs, server validates and broadcasts results.

class_name ServerAuthPlayer extends CharacterBody3D

@export var player_id: int = 0
var _server := false

func _ready():
    if multiplayer.is_server():
        _server = true
        set_multiplayer_authority(1)
    else:
        # Client-owned players are predicted locally
        set_multiplayer_authority(multiplayer.get_unique_id())

# Client sends input to server
func _input(event: InputEvent) -> void:
    if not multiplayer.is_server():
        rpc("rpc_request_move", event)

# Server receives and validates input
@rpc("authority")
func rpc_request_move(event: InputEvent):
    # Validate the input (check timing, etc.)
    _process_input(event)

Client-Predictive Model

Clients predict their own actions locally and correct on server acknowledgment.

class_name ClientPredictivePlayer extends CharacterBody3D

@export var prediction_enabled: bool = true
var _input_buffer := []
var _last_confirmed_input: int = 0

func _physics_process(delta: float):
    # Apply buffered inputs up to current state
    for input_data in _input_buffer:
        if input_data.sequence > _last_confirmed_input:
            _apply_input(input_data)
    
    # Send position to server
    rpc("rpc_update_position", global_position, rotation_degrees)

When to Use Which

Model Best For
Server-authoritative Competitive games, anti-cheat priority
Client-predictive Fast-paced action, smooth feel priority
Hybrid (recommended) Most multiplayer games

Replication Patterns

rpc() — Remote Procedure Call

# Call a method on all other nodes
rpc("rpc_on_enemy_hit", damage, target_id)

# With parameters
@rpc("reliable")
func rpc_spawn_pickup(pickup_type: String, position: Vector3):
    var pickup = preload("res://scenes/pickups/" + pickup_type + ".tscn").instantiate()
    pickup.global_position = position
    get_tree().root.add_child(pickup)

rset() — Remote Set

Set a property on remote nodes.

# Set health bar on all clients
rset("health", new_health)
rset("max_health", max_health)
rset("visible", true)

# Bulk set with rset_group()
rset_group("players", "set_state", state_name)

rset_group() — Broadcast to Group

# Add nodes to a group for batch updates
func _ready():
    multiplayer.add_to_group("players")

# Update all players at once
func broadcast_position():
    rset_group("players", "sync_position", global_position, rotation_degrees)

# Server-only group
func _ready():
    if multiplayer.is_server():
        add_to_group("server_only")

Networked Autoload

Use autoloads for network-wide state.

# NetworkManager.gd (Autoload)
class_name NetworkManager extends Node

signal player_connected(player_id: int)
signal player_disconnected(player_id: int)
signal game_started()

var players := {}

func _ready():
    multiplayer.peer_connected.connect(_on_peer_connected)
    multiplayer.peer_disconnected.connect(_on_peer_disconnected)

func _on_peer_connected(id: int):
    players[id] = PlayerInfo.new(id)
    player_connected.emit(id)

func _on_peer_disconnected(id: int):
    players.erase(id)
    player_disconnected.emit(id)

Connection Management

Server Setup

class_name MultiplayerServer extends Node

const PORT = 9000
const MAX_PLAYERS = 16
var server: HTTPServer

func start_server():
    var peer = ENetMultiplayerPeer.new()
    peer.create_server(PORT, MAX_PLAYERS)
    multiplayer.multiplayer_peer = peer
    print("Server started on port ", PORT)

func stop_server():
    if multiplayer.multiplayer_peer:
        multiplayer.multiplayer_peer.disconnect_from_server()

Client Connection

class_name MultiplayerClient extends Node

signal connected
signal connection_failed

func connect_to_server(host: String, port: int):
    var peer = ENetMultiplayerPeer.new()
    var error = peer.create_client(host, port)
    if error != OK:
        connection_failed.emit()
        return
    
    multiplayer.multiplayer_peer = peer
    # Wait for server to assign unique ID
    await get_tree().create_timer(0.5).timeout
    if multiplayer.is_connected_to_server():
        connected.emit()

Lag Compensation

# Store player positions for rewind capability
class_name PositionHistory extends Node

@export var history_length: int = 3.0
var _history := []

func _physics_process(delta: float):
    _history.push_back({
        "position": global_position,
        "rotation": rotation_degrees,
        "time": Time.get_ticks_msec()
    })
    # Prune old entries
    var cutoff = Time.get_ticks_msec() - int(history_length * 1000)
    while _history.size() > 0 and _history[0].time < cutoff:
        _history.pop_front()

func get_position_at(time_ms: int) -> Variant:
    for entry in _history:
        if entry.time >= time_ms:
            return entry
    return _history.back()

Steam Frame Integration (Godot 4.7+)

Using Steam Networking Sockets

# Requires Steamworks SDK and godot-steam plugin
func connect_steam_server(app_id: int, steam_id: String):
    var peer = SteamMultiplayerPeer.new()
    peer.create_client(steam_id)
    multiplayer.multiplayer_peer = peer

Frame-Synced Updates

For competitive games requiring frame-accurate sync:

@export var tick_rate: int = 60
var _tick_counter: int = 0

func _physics_process(delta: float):
    _tick_counter += 1
    if _tick_counter % (int(60 / tick_rate)) == 0:
        rpc("rpc_tick_update", _tick_counter, get_state_snapshot())

Steam Lobby Integration

func create_lobby(max_players: int = 4):
    # Create lobby via Steamworks API
    var lobby_id = Steam.matchmaking.create_lobby(
        max_players,
        Steam.LobbyType.LobbyPrivate
    )
    return lobby_id

func join_lobby(lobby_id: String):
    Steam.matchmaking.join_lobby(lobby_id)

Best Practices

Do

  • Use @rpc("unreliable") for frequent position updates — saves bandwidth
  • Validate all client input on the server before applying
  • Keep RPC payloads small — serialize only what changes
  • Use multiplayer.is_server() guards to prevent client-side authority bugs
  • Implement a heartbeat/ping system for connection quality monitoring

Don't

  • Don't call rpc() in _process without throttling — floods the network
  • Don't trust client-reported health/damage — always recalculate server-side
  • Don't send full state snapshots every frame — use delta compression
  • Don't forget set_multiplayer_authority() when spawning remote nodes
  • Don't mix reliable and unreliable RPCs for the same logical update

Debugging Tips

# Enable network debugging in Project Settings:
  network/enable_packet_logging = true
  network/max_packet_size = 1400

# Print all RPC traffic:
multiplayer.peer_connected.connect(func(id): print("Peer connected: ", id))
multiplayer.peer_disconnected.connect(func(id): print("Peer disconnected: ", id))

MCP Bridge Tools (Optional — Live Editor Integration)

When the MCP bridge is running (Phase 2), these tools enhance networking debugging:

  • network_peer_list — List connected peers and their unique IDs
  • rpc_trace — Log all RPC calls with sender/receiver info for debugging
  • network_stats — View bandwidth usage, packet loss, and latency metrics
  • lobby_inspect — Query Steam lobby state and player list

Note: MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.

References