Multiplayer — MultiplayerAPI and RPC
ENet/WebSocket/WebRTC peers, RPC, MultiplayerSpawner and Synchronizer.
Godot has built-in high-level multiplayer: MultiplayerAPI + transports + Spawner/Synchronizer nodes. It is the analog of Unity Netcode for GameObjects.
Architecture
MultiplayerAPI works on top of MultiplayerPeer. Transports:
- ENetMultiplayerPeer — UDP, the main one for native platforms.
- WebSocketMultiplayerPeer — TCP/WebSocket, the only path for the web target.
- WebRTCMultiplayerPeer — peer-to-peer, NAT traversal via STUN/TURN.
- OfflineMultiplayerPeer — a stub for single-player.
The standard model is an authoritative server: one peer with ID=1 (the server), the rest are clients with ID >= 2.
Starting a server and a client
extends Node
const PORT := 7777
const MAX_PEERS := 4
func host_game() -> Error:
var peer = ENetMultiplayerPeer.new()
var err = peer.create_server(PORT, MAX_PEERS)
if err != OK:
return err
multiplayer.multiplayer_peer = peer
print("Server up on port ", PORT)
return OK
func join_game(ip: String) -> Error:
var peer = ENetMultiplayerPeer.new()
var err = peer.create_client(ip, PORT)
if err != OK:
return err
multiplayer.multiplayer_peer = peer
print("Connecting to ", ip)
return OK
multiplayer is a built-in getter for the MultiplayerAPI on any node.
MultiplayerAPI signals:
peer_connected(id)/peer_disconnected(id)— on the server for each client.connected_to_server/connection_failed/server_disconnected— on the client.
func _ready() -> void:
multiplayer.peer_connected.connect(_on_peer_joined)
multiplayer.peer_disconnected.connect(_on_peer_left)
func _on_peer_joined(id: int) -> void:
print("Peer ", id, " joined")
if multiplayer.is_server():
spawn_player(id)
RPC — remote calls
GDScript uses the @rpc annotation on functions that can be called remotely:
extends Node3D
# from_who: any_peer | authority
# transfer: reliable | unreliable | unreliable_ordered
# call_local: whether to also call locally on a remote call
@rpc("any_peer", "reliable", "call_local")
func shoot(direction: Vector3) -> void:
spawn_bullet(direction)
# The server tells everyone (call → this method fires on every client)
@rpc("authority", "reliable")
func play_explosion_sound(at: Vector3) -> void:
var player := AudioStreamPlayer3D.new()
player.stream = preload("res://audio/explosion.ogg")
player.global_position = at
add_child(player)
player.play()
player.finished.connect(player.queue_free)
Calling:
# From client → server (if the remote method has "authority"):
rpc("shoot", direction)
# Only to a specific peer:
rpc_id(target_peer_id, "play_explosion_sound", pos)
# Specifically to the server:
rpc_id(1, "do_something")
The @rpc parameters:
- From who:
any_peer(any client can call) orauthority(only the node’s owner). - Transfer:
reliable(TCP-style),unreliable(UDP with no guarantees),unreliable_ordered. - Call local: whether to call locally in addition to remotely.
- Channel: the channel number (for sending different message types in parallel).
In the authoritative model the server is the source of truth. Do not trust RPC parameters from a
client (any client can send damage = 99999). The server must validate: check the shot distance,
cooldown, and permissions.
Multiplayer Authority
Every node has an authority — the peer ID that “owns” the node. By default it is 1 (the server). You can hand it to a client:
# On the server: give the peer control of its player
var player = player_scene.instantiate()
player.name = str(peer_id) # name for uniqueness
add_child(player, true)
player.set_multiplayer_authority(peer_id)
Inside the script:
func _physics_process(delta: float) -> void:
if not is_multiplayer_authority():
return # only the owner moves itself
# ... handle input
MultiplayerSpawner
A node that automatically replicates the spawning of child nodes from the server to the clients. You don’t need to manually send a “create_enemy_at” RPC.
World
├── Players (Node) ← children are spawned for each player
│ └── MultiplayerSpawner ← spawn_path = "../Players"
│ spawnable_scenes = [Player.tscn]
On the server you call players.add_child(player). The spawner catches this and sends it to all
clients, which create the same node locally.
MultiplayerSynchronizer
Declarative synchronization of properties. You attach it to a node and specify a list of properties
in the Inspector (for example, position, rotation, hp). MultiplayerAPI automatically sends
changes from the authority to the others.
Player (CharacterBody3D, authority = peer_id)
├── ...
└── MultiplayerSynchronizer
Replication Config:
:position [Always]
:rotation:y [Always]
hp [On Change]
state [On Change]
Replication Mode:
- Always — every tick.
- On Change — when the value changes.
- Never — do not replicate (for visibility filtering).
This removes 80% of the manual synchronization code you would otherwise write with RPCs. It’s the analog of Unity’s NetworkVariable, but declarative.
Comparison with Unity Netcode
| Concept | Unity NGO | Godot |
|---|---|---|
| Network entity | NetworkObject | A node with authority |
| Spawn | Spawn() via NetworkObject | MultiplayerSpawner |
| Sync property | NetworkVariable<T> | MultiplayerSynchronizer |
| RPC (client→server) | [ServerRpc] | @rpc("any_peer") + rpc_id(1, ...) |
| RPC (server→clients) | [ClientRpc] | @rpc("authority") |
| Transport | UnityTransport | ENetMultiplayerPeer |
| Relay | Unity Relay | Your own (or third-party, e.g. Nakama) |
| Matchmaking | Unity Lobby | Your own |
Web
Only via WebSocketMultiplayerPeer:
var peer = WebSocketMultiplayerPeer.new()
peer.create_server(8080)
# or peer.create_client("ws://example.com:8080")
multiplayer.multiplayer_peer = peer
ENet doesn’t work in the browser. WebRTC requires STUN/TURN servers for NAT traversal.
Pitfalls
- Don’t confuse local multiplayer with networked multiplayer. Split-screen is a different task,
handled through viewports and
PlayerInputsources. call_local— people forget it, and then the method doesn’t fire locally. If you want it to “run both on yourself and remotely,” enable it.- MultiplayerSynchronizer requires the node to exist on all clients. Use it together with the Spawner.
- Order matters when spawning: if the server calls
add_child(player)afterset_multiplayer_authority, the authority will get replicated in time. Otherwise it’s the reverse.
In the next chapter — resource loading and the Resource Loader.