Godot's networking stack is layered. At the bottom sit transport-level peers (ENetMultiplayerPeer, WebSocketMultiplayerPeer, WebRTCMultiplayerPeer). Above that is the MultiplayerAPI, which handles peer tracking, RPC dispatch, and replication. The SceneTree exposes multiplayer as the default API instance.
| Layer | Class | Responsibility |
|---|---|---|
| Transport | ENetMultiplayerPeer | Reliable/unreliable UDP channels, peer management |
| Transport | WebSocketMultiplayerPeer | TCP-based, browser-compatible |
| Transport | WebRTCMultiplayerPeer | P2P via STUN/TURN, browser-native |
| API | MultiplayerAPI | RPC routing, replication, peer signals |
| API | SceneMultiplayer | Default MultiplayerAPI; adds scene-level replication |
Each SubViewport can have its own MultiplayerAPI instance — useful for isolating game worlds on a single server process.
The server always holds peer ID 1. Clients receive a unique positive integer assigned by the server on connection.
var peer = ENetMultiplayerPeer.new()
peer.create_server(7777, max_clients) # port, max_clients
multiplayer.multiplayer_peer = peer
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
var peer = ENetMultiplayerPeer.new()
peer.create_client("127.0.0.1", 7777)
multiplayer.multiplayer_peer = peer
multiplayer.connected_to_server.connect(_on_connected)
multiplayer.connection_failed.connect(_on_failed)
| Signal | Fires on | Available to |
|---|---|---|
peer_connected(id) | new peer joins | server & clients |
peer_disconnected(id) | peer drops | server & clients |
connected_to_server | client handshake OK | client only |
connection_failed | server unreachable | client only |
server_disconnected | server closed | client only |
multiplayer.get_unique_id() returns your local peer ID. multiplayer.is_server() is shorthand for get_unique_id() == 1.
Functions annotated with @rpc can be called across the network. The annotation accepts up to four arguments in this order: mode, sync, transfer mode, channel.
# Syntax
@rpc(mode, sync, transfer_mode, channel)
func my_function():
pass
| Parameter | Options | Default |
|---|---|---|
| mode | any_peer, authority | authority |
| sync | call_local, call_remote | call_remote |
| transfer_mode | reliable, unreliable, unreliable_ordered | reliable |
| channel | integer (ENet: 0–255) | 0 |
# Call on all peers
rpc("apply_damage", amount)
# Call on a specific peer by ID
rpc_id(peer_id, "apply_damage", amount)
# Only the server can call this on all clients
@rpc("authority", "call_local", "reliable")
func sync_health(new_hp: int):
health = new_hp
_update_hud()
# Any client can send input to the server
@rpc("any_peer", "call_remote", "unreliable_ordered")
func send_input(dir: Vector2):
if is_multiplayer_authority():
_process_input(dir)
any_peer, any connected client can invoke the function. Always validate inputs server-side before applying state changes.
Godot 4 ships two nodes that automate synchronization: MultiplayerSpawner and MultiplayerSynchronizer. Both require SceneMultiplayer as the active API (the default).
Tracks instantiation and deletion of nodes under a spawn path and replicates those events to all peers. Configure spawn_limit, auto_spawn_path, and add scenes to the spawnable list in the inspector or via code.
# Server spawns; clients receive automatically
func spawn_bullet(pos: Vector2):
var b = BULLET_SCENE.instantiate()
b.position = pos
$Bullets.add_child(b) # spawner is watching $Bullets
Continuously replicates named properties from the authority to all other peers on a configurable interval. Add properties in the Replication editor panel or via add_property().
# Code-driven property registration (Godot 4.1+)
func _ready():
var sync: MultiplayerSynchronizer = $Synchronizer
sync.add_property(NodePath(".:position"))
sync.add_property(NodePath(".:health"))
| Property | Purpose |
|---|---|
replication_interval | Seconds between sync packets. 0 = every frame. |
delta_interval | Seconds between delta (change-only) packets. |
visibility_filters | Callbacks to restrict sync to a subset of peers. |
public_visibility | If false, only peers passing visibility filters receive updates. |
MultiplayerSynchronizer respects authority: only the authority peer sends updates; all others receive them. Combining a per-player Synchronizer with set_multiplayer_authority(peer_id) achieves client-authoritative movement with minimal code.
A common pattern: keep lobby logic in an autoload singleton and use it as the coordination point before loading the game scene.
# Network.gd (AutoLoad singleton)
signal player_list_changed
var players: Dictionary = {} # { id: data }
@rpc("any_peer", "call_local", "reliable")
func register_player(data: Dictionary):
var id = multiplayer.get_remote_sender_id()
players[id] = data
player_list_changed.emit()
@rpc("authority", "call_local", "reliable")
func start_game():
get_tree().change_scene_to_file("res://game.tscn")
get_remote_sender_id() returns the peer ID of whoever triggered the RPC on this machine — critical for validating who is sending what.
Use rpc("start_game") called by the server after all players have registered. Because the singleton persists across scene changes, the peer list survives the transition intact. Instantiate player nodes in the game scene's _ready() using the data already in Network.players.
UDP-based. Supports reliable, unreliable, and ordered channels. Lowest latency option for LAN and internet games. Channels (0–255) let you separate traffic streams; e.g., channel 0 for state, channel 1 for chat.
var peer = ENetMultiplayerPeer.new()
peer.create_server(7777, 32, 4) # port, max_peers, max_channels
TCP fallback; mandatory for HTML5 exports. Use WebSocketMultiplayerPeer. API is identical to ENet from the multiplayer layer's perspective.
var peer = WebSocketMultiplayerPeer.new()
peer.create_server(7777) # ws://
# For TLS: peer.create_server(7777, "*", tls_options)
Peer-to-peer; no dedicated server required for data relay, but a signaling server is needed for peer discovery. Use the WebRTC GDExtension plugin. Suited for browser-to-browser games or scenarios where hosting cost is a concern.
| Transport | Protocol | HTML5 | P2P | Latency |
|---|---|---|---|---|
| ENet | UDP | ✗ | ✗ | Low |
| WebSocket | TCP | ✓ | ✗ | Medium |
| WebRTC | UDP/DTLS | ✓ | ✓ | Low–Medium |
Godot does not include built-in lag compensation. You implement it at the game layer.
Run authoritative movement logic locally on the client (authority check disabled or duplicated), then reconcile when the server state arrives. If the server position diverges beyond a threshold, snap or interpolate back.
func _physics_process(delta):
var input = _get_input_vector()
_apply_movement(input, delta) # run locally always
if is_multiplayer_authority():
rpc("server_move", input) # also send to server
@rpc("authority", "call_local", "unreliable")
func correct_position(server_pos: Vector2):
if position.distance_to(server_pos) > CORRECTION_THRESHOLD:
position = position.lerp(server_pos, 0.3)
For remote (non-authority) entities, buffer received state snapshots and render between the two most recent frames. Aim for a buffer of ~100ms. This trades a small fixed delay for smooth visuals instead of jittery teleporting.
unreliable_ordered is the right transfer mode for position updates — drop old packets, never reorder. reliable is for events (damage, death, pickups) that must arrive exactly once.
get_remote_sender_id(). In every any_peer RPC, confirm the sender is allowed to perform that action before applying it.TLSOptions object to create_server()/create_client().MultiplayerSpawner whitelist defines what clients can request spawned. Keep it minimal.any_peer RPC mode is open to every connected peer — including other clients in a client-server topology. A compromised or malicious client can call those functions freely. Design accordingly.