references/replication-and-rpc.md
# Replication & RPC reference (Godot 4.7)
Depth companion to `godot-multiplayer`.
## The @rpc annotation in full
```gdscript
@rpc(mode, sync, transfer_mode, transfer_channel)
```
Defaults — `@rpc` is equivalent to `@rpc("authority", "call_remote", "reliable", 0)`:
- **mode**: `"authority"` (only the node's multiplayer authority may call remotely) or
`"any_peer"` (any client may call — needed for player input).
- **sync**: `"call_remote"` (does not run on the local/sending peer) or `"call_local"`
(also runs locally — use when the host is a player).
- **transfer_mode**: `"reliable"` (acknowledged, ordered, slower), `"unreliable"`
(fire-and-forget, may drop/reorder — good for frequent position updates), or
`"unreliable_ordered"` (drops late packets to preserve order).
- **transfer_channel**: integer channel index; the first three args can be in any order,
but the channel must be the 4th argument, so all three must be given before it. Use
separate channels to keep unrelated streams from
blocking each other.
Call forms (on a `Callable` for the method):
```gdscript
my_method.rpc(args...) # call on all peers (per mode/sync rules)
my_method.rpc_id(peer_id, args) # call on one peer (1 = server)
multiplayer.get_remote_sender_id() # inside the method: who invoked it
```
## MultiplayerSpawner
Auto-instantiates scenes on remote peers when the authority spawns them.
1. Add a `MultiplayerSpawner` node; set its **Spawn Path** to the parent that will hold
spawned instances.
2. Add the scenes it may spawn to its **Auto Spawn List** (`add_spawnable_scene()` in code).
3. On the authority, `add_child()` one of those scenes under the spawn path — the spawner
replicates it to clients with a matching name. For custom setup, assign a
`spawn_function` (it receives your `data` and returns the node) and call
`spawner.spawn(data)`; `spawn()` errors without a `spawn_function`.
```gdscript
@onready var spawner: MultiplayerSpawner = $MultiplayerSpawner
func server_spawn_player(id: int) -> void:
var p := preload("res://player.tscn").instantiate()
p.name = str(id) # deterministic name = authority id
$Players.add_child(p, true) # true = readable, replicated name
```
## MultiplayerSynchronizer
Replicates chosen properties from the authority to other peers each network tick.
- Add it as a child of the node to sync. In the **Replication** dock, add property paths
(e.g. `:position`, `:velocity`, `Sprite2D:modulate`).
- Per property: **Spawn** (sent once when the node is spawned) and a **Replicate** mode:
*Always* (unreliable, every `replication_interval`), *On Change* (reliable, checked every
`delta_interval` and sent only when the value changed) or *Never*. In code use
`SceneReplicationConfig.property_set_replication_mode()`; `property_set_sync` and
`property_set_watch` are deprecated.
- `replication_interval` / `delta_interval` (on the synchronizer) tune bandwidth.
- Visibility filtering for per-peer data. A peer receives the node only if every filter
returns `true` **and** it is visible to that peer — `public_visibility` (default `true`) or
`set_visibility_for(peer, true)`. So `set_visibility_for` alone does nothing until
`public_visibility` is off:
```gdscript
sync.set_visibility_for(peer_id, true)
sync.add_visibility_filter(func(peer): return _can_see(peer))
sync.update_visibility()
```
Only the authority's values are sent; remote peers receive read-only updates. Combine with
per-node authority so each player's synchronizer pushes that player's state.
## SceneMultiplayer authentication
Protect RPCs from unauthenticated peers using the built-in handshake:
```gdscript
# Both sides set auth_callback BEFORE assigning multiplayer_peer.
@onready var sm := multiplayer as SceneMultiplayer # auth API lives on SceneMultiplayer
func setup_server_auth() -> void:
sm.auth_timeout = 3.0
sm.auth_callback = func(peer_id: int, data: PackedByteArray) -> void:
var info: Variant = JSON.parse_string(data.get_string_from_utf8())
if info is Dictionary and _valid(info):
sm.complete_auth(peer_id)
func setup_client_auth() -> void:
# The client needs an auth_callback too, or it skips the handshake entirely.
sm.auth_callback = func(_peer_id: int, _data: PackedByteArray) -> void:
pass # read the server's reply here, if any
sm.peer_authenticating.connect(func(peer_id: int) -> void:
var payload := JSON.stringify({"token": _token}).to_utf8_buffer()
sm.send_auth(peer_id, payload)
sm.complete_auth(peer_id) # the client must complete its side too
)
```
`connected_to_server` / `peer_connected` only fire once both sides call `complete_auth()`.
If the client never calls it, the handshake times out and `peer_authentication_failed`
fires; if the client has no `auth_callback`, the server waits for data that never comes.
## Transfer mode guidance
- **Player input / commands** → reliable (must not be lost).
- **Position/rotation updates** → unreliable or unreliable_ordered via a
`MultiplayerSynchronizer` (frequent, latest-wins).
- **Chat / state changes** → reliable, possibly on a dedicated channel.
## Dedicated server export
Export a **headless** build (no GPU) for servers; see `godot-export`. Modify the lobby so
the server is not counted as a player and the first joining client can start the match. Run
with the server feature tag and `--headless`.
## Minimal lobby flow
1. Host: `create_server`; add self to `players` keyed by id 1.
2. On `peer_connected(id)`: send your player info to that id via `rpc_id`.
3. Each peer registers others (`@rpc("any_peer")` register function using
`get_remote_sender_id()`).
4. On `peer_disconnected(id)`: erase from `players`, emit a UI signal.
5. Server starts the game by RPC'ing a `load_game(path)` (`@rpc("call_local","reliable")`)
so all peers `change_scene_to_file` together.
SKILL.md
---
name: godot-multiplayer
description: >
Build networked games with Godot 4.7 high-level multiplayer: set up an
ENetMultiplayerPeer server/client, define RPCs with the @rpc annotation
(call via rpc()/rpc_id()), set per-node multiplayer authority, and replicate
state with MultiplayerSpawner and MultiplayerSynchronizer. Use when adding
multiplayer/networking to a Godot project, writing @rpc functions, or syncing
player/world state across peers.
---
# Godot Multiplayer (4.x high-level)
Connect peers, call functions remotely with `@rpc`, assign authority, and replicate state
with `MultiplayerSpawner`/`MultiplayerSynchronizer`. Targets **Godot 4.7** (ENet). Treat
all client input as untrusted; keep the server authoritative.
## When to use
- Use when adding networked multiplayer: hosting/joining over ENet, calling RPCs,
assigning per-node authority, or auto-spawning/syncing nodes across peers.
**When *not* to use:** local split-screen (no networking); raw TCP/UDP/WebSocket protocol
work (low-level `PacketPeer`); HTTP requests. For save/persistence → `save-systems`.
## Core workflow
1. **Create a peer** (`ENetMultiplayerPeer`), call `create_server(port, max)` or
`create_client(ip, port)`, and assign it to `multiplayer.multiplayer_peer`. The
server's unique ID is always `1`; clients get random positive IDs.
2. **Handle connection signals** on `multiplayer`: `peer_connected(id)`,
`peer_disconnected(id)`, `connected_to_server`, `connection_failed`,
`server_disconnected`.
3. **Define RPCs** with `@rpc(...)`. Call them on a `Callable` via `rpc()` (all peers) or
`rpc_id(peer_id)` (one peer). Inside, `multiplayer.get_remote_sender_id()` tells you who
sent it.
4. **Keep RPC signatures identical** on every peer that runs the script — Godot checksums
all `@rpc` methods in a script; a mismatch prints errors that can name an unrelated RPC.
5. **Assign authority** per node with `set_multiplayer_authority(id)`; gate input/RPCs by
`is_multiplayer_authority()`.
6. **Replicate state** with `MultiplayerSpawner` (auto-instances scenes on clients) and
`MultiplayerSynchronizer` (auto-syncs selected properties).
7. **Validate on the server.** Don't trust client-reported positions/results.
## Patterns
### 1. Host or join (ENet)
```gdscript
const PORT := 7000
const MAX_PLAYERS := 8
func host() -> void:
var peer := ENetMultiplayerPeer.new()
var err := peer.create_server(PORT, MAX_PLAYERS)
if err != OK:
push_error("Cannot host: %s" % err); return
multiplayer.multiplayer_peer = peer
multiplayer.peer_connected.connect(_on_peer_connected)
func join(ip := "127.0.0.1") -> void:
var peer := ENetMultiplayerPeer.new()
peer.create_client(ip, PORT)
multiplayer.multiplayer_peer = peer
multiplayer.connected_to_server.connect(func(): print("connected"))
func leave() -> void:
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
```
### 2. RPCs: client sends input to the server (any_peer, call_local)
This script sits on a player avatar whose authority is the owning client (Pattern 3), so
the server is *not* the node's authority. A server→clients RPC on such a node must be
`"any_peer"` and check that the sender is the server.
```gdscript
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("fire") and is_multiplayer_authority():
request_fire.rpc_id(1) # send only to the server (id 1)
# Clients may call this; it runs on the server (and locally if server is a player).
@rpc("any_peer", "call_local", "reliable")
func request_fire() -> void:
if not multiplayer.is_server(): # ignore copies relayed to other clients
return
var sender := multiplayer.get_remote_sender_id()
if not _can_fire(sender): # server-side validation
return
spawn_projectile.rpc(sender) # tell everyone to spawn it
# "authority" here would make clients reject the server's call (the client is the
# authority), so accept any peer and keep only calls that come from the server.
@rpc("any_peer", "call_local", "reliable")
func spawn_projectile(owner_id: int) -> void:
if multiplayer.get_remote_sender_id() != 1:
return
_do_spawn(owner_id)
```
### 3. Per-node authority (each player controls their own avatar)
```gdscript
extends CharacterBody2D
func _ready() -> void:
# The node name is the owning peer's id; that peer is the authority.
set_multiplayer_authority(name.to_int())
func _physics_process(delta: float) -> void:
if not is_multiplayer_authority():
return # only the owner reads input & moves
velocity = Input.get_vector("left", "right", "up", "down") * 200.0
move_and_slide()
```
### 4. MultiplayerSynchronizer config (editor + replication)
```gdscript
# Add a MultiplayerSynchronizer child; in its Replication editor add the properties to
# sync (e.g. position, velocity). Set "Sync"/"Spawn" flags per property. From code you
# can scope visibility:
@onready var sync: MultiplayerSynchronizer = $MultiplayerSynchronizer
func _ready() -> void:
# Only replicate this node to a specific peer (e.g. private info). Per-peer visibility
# matters only once public_visibility is off; by default every peer receives it.
sync.public_visibility = false
sync.set_visibility_for(target_peer_id, true)
```
## Pitfalls
- **RPC signature checksum.** Every `@rpc` method in a script must exist with the same
declaration on both client and server builds — *even unused ones*. A mismatch causes
errors that may point at the wrong function. Argument names/count are not checked, but
the set of RPCs and their annotations are.
- **Default `@rpc` is `"authority"`.** Clients calling it are ignored unless you set
`"any_peer"`. Use `"call_local"` so the host (also a player) runs it too.
- **`RPC 'x' is not allowed on node ... from: 1. Mode is "authority"`** → the server called
an `"authority"` RPC on a node a client owns. "Authority" means the node's authority, not
the server. Use `"any_peer"` plus a `get_remote_sender_id() == 1` check, or move
server-driven RPCs to a server-owned node (e.g. a game manager).
- **NodePaths must match across peers.** RPC routing uses the node's path/name; spawn nodes
with identical names on all peers (use `MultiplayerSpawner` or `add_child(node, true)` for
readable, deterministic names).
- **Trusting the client.** Never let clients set authoritative state (health, position,
hits) directly. Send *intent*, validate on the server, then broadcast results.
- **RPC on non-Node classes fails.** `@rpc` methods must be on `Node`-derived classes, not
plain `Resource`/`RefCounted`.
- **RPCs don't serialize Objects/Callables.** Pass plain data (ints, strings, arrays,
dictionaries, PackedArrays).
- **Forgetting to reset the peer.** To disconnect cleanly, set
`multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()`.
- **Android needs INTERNET permission** in the export preset or all networking is blocked.
## References
- For `MultiplayerSpawner` setup, transfer modes/channels, `SceneMultiplayer`
authentication (`auth_callback`/`complete_auth`), a lobby skeleton, and dedicated-server
export notes, read `references/replication-and-rpc.md`.
## Related skills
- `godot-nodes-scenes` — instancing the scenes that get spawned/synced.
- `godot-signals-groups` — connection signals and event flow.
- `godot-export` — exporting a headless dedicated server build.