Use when implementing multiplayer — MultiplayerAPI, ENet/WebSocket peers, RPCs, and authority model
Scanned 6/3/2026
Install to Claude Code
npx -y skills add jame581/GodotPrompter --skill multiplayer-basics --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Multiplayer Basics?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jame581-multiplayer-basics)More formats (shields.io, HTML) on the badges page.
---
name: multiplayer-basics
description: Use when implementing multiplayer — MultiplayerAPI, ENet/WebSocket peers, RPCs, and authority model
---
# Multiplayer Basics in Godot 4.3+
All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, C# follows.
**Related skills:** See **multiplayer-sync** for state synchronization and interpolation. See **dedicated-server** for headless export and server deployment.
---
## 1. Multiplayer Architecture
Godot uses a **client-server model** built on top of `MultiplayerAPI`. One peer acts as the server; all others are clients. Every peer has a unique integer ID assigned by the network layer:
| Peer ID | Role |
|---------|------|
| `1` | The server (always) |
| `2`, `3`, … | Connected clients |
**Multiplayer authority** is the concept of ownership over a node. Only the authoritative peer should read input and drive that node's state. By default the server (peer `1`) is the authority for every node. Call `set_multiplayer_authority(peer_id)` to transfer ownership to a client.
```
Server (peer 1)
├── Owns game state by default
├── Spawns and validates objects
└── Routes RPCs
Client (peer 2, 3, …)
├── Sends input to server via RPC
└── Receives state updates from server
```
---
## 2. Setting Up ENetMultiplayerPeer
### GDScript
```gdscript
# network_manager.gd — add as autoload named NetworkManager
extends Node
const DEFAULT_PORT := 7777
const MAX_CLIENTS := 16
var peer: ENetMultiplayerPeer
func host_game(port: int = DEFAULT_PORT) -> void:
peer = ENetMultiplayerPeer.new()
var err := peer.create_server(port, MAX_CLIENTS)
if err != OK:
push_error("NetworkManager: create_server failed — error %d" % err)
return
multiplayer.multiplayer_peer = peer
_connect_signals()
print("NetworkManager: hosting on port %d" % port)
func join_game(address: String, port: int = DEFAULT_PORT) -> void:
peer = ENetMultiplayerPeer.new()
var err := peer.create_client(address, port)
if err != OK:
push_error("NetworkManager: create_client failed — error %d" % err)
return
multiplayer.multiplayer_peer = peer
_connect_signals()
print("NetworkManager: connecting to %s:%d" % [address, port])
func disconnect_from_game() -> void:
if peer:
peer.close()
multiplayer.multiplayer_peer = null
func _connect_signals() -> void:
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
multiplayer.connected_to_server.connect(_on_connected_to_server)
multiplayer.connection_failed.connect(_on_connection_failed)
func _on_peer_connected(id: int) -> void:
print("NetworkManager: peer connected — id %d" % id)
func _on_peer_disconnected(id: int) -> void:
print("NetworkManager: peer disconnected — id %d" % id)
func _on_connected_to_server() -> void:
print("NetworkManager: connected to server — my id is %d" % multiplayer.get_unique_id())
func _on_connection_failed() -> void:
push_error("NetworkManager: connection failed")
```
**Key signal summary:**
| Signal | Fires on | When |
|--------|----------|------|
| `peer_connected` | Server + clients | A new peer finishes connecting |
| `peer_disconnected` | Server + clients | A peer disconnects or times out |
| `connected_to_server` | Client only | This client successfully connected |
| `connection_failed` | Client only | This client could not connect |
### C#
```csharp
// NetworkManager.cs — add as autoload named NetworkManager
using Godot;
public partial class NetworkManager : Node
{
private const int DefaultPort = 7777;
private const int MaxClients = 16;
private ENetMultiplayerPeer _peer;
public void HostGame(int port = DefaultPort)
{
_peer = new ENetMultiplayerPeer();
var err = _peer.CreateServer(port, MaxClients);
if (err != Error.Ok)
{
GD.PushError($"NetworkManager: CreateServer failed — error {err}");
return;
}
Multiplayer.MultiplayerPeer = _peer;
ConnectSignals();
GD.Print($"NetworkManager: hosting on port {port}");
}
public void JoinGame(string address, int port = DefaultPort)
{
_peer = new ENetMultiplayerPeer();
var err = _peer.CreateClient(address, port);
if (err != Error.Ok)
{
GD.PushError($"NetworkManager: CreateClient failed — error {err}");
return;
}
Multiplayer.MultiplayerPeer = _peer;
ConnectSignals();
GD.Print($"NetworkManager: connecting to {address}:{port}");
}
public void DisconnectFromGame()
{
_peer?.Close();
Multiplayer.MultiplayerPeer = null;
}
private void ConnectSignals()
{
Multiplayer.PeerConnected += OnPeerConnected;
Multiplayer.PeerDisconnected += OnPeerDisconnected;
Multiplayer.ConnectedToServer += OnConnectedToServer;
Multiplayer.ConnectionFailed += OnConnectionFailed;
}
private void OnPeerConnected(long id)
=> GD.Print($"NetworkManager: peer connected — id {id}");
private void OnPeerDisconnected(long id)
=> GD.Print($"NetworkManager: peer disconnected — id {id}");
private void OnConnectedToServer()
=> GD.Print($"NetworkManager: connected — my id is {Multiplayer.GetUniqueId()}");
private void OnConnectionFailed()
=> GD.PushError("NetworkManager: connection failed");
}
```
---
## 3. RPCs
`@rpc` (GDScript) / `[Rpc]` (C#) marks a method as callable across the network. Choose the mode and transfer settings carefully — they affect both security and performance.
### RPC Modes
| Mode | Who may call it | Executes on |
|------|-----------------|-------------|
| `"authority"` (default) | Only the authority peer | The peer(s) it is sent to |
| `"any_peer"` | Any connected peer | The peer(s) it is sent to |
### Transfer Modes
| Mode | Delivery | Order | Use For |
|------|----------|-------|---------|
| `"reliable"` | Guaranteed | In-order | Chat, spawn events, important state |
| `"unreliable"` | Best-effort | Unordered | High-frequency position updates |
| `"unreliable_ordered"` | Best-effort | In-order per channel | Smooth movement streams |
### GDScript
```gdscript
# chat.gd
extends Node
# Any peer can call this; executed on the server only.
# The server then broadcasts to all peers.
@rpc("any_peer", "reliable")
func send_chat_message(text: String) -> void:
if not multiplayer.is_server():
return
var sender_id := multiplayer.get_remote_sender_id()
_broadcast_chat.rpc(sender_id, text)
# Only the authority (server) can call this; runs on every peer.
@rpc("authority", "reliable", "call_local")
func _broadcast_chat(sender_id: int, text: String) -> void:
print("[%d]: %s" % [sender_id, text])
# Client → server: request to spawn an object.
@rpc("any_peer", "reliable")
func request_spawn(scene_path: String, spawn_position: Vector2) -> void:
if not multiplayer.is_server():
return
# Server validates and performs the actual spawn.
var scene: PackedScene = load(scene_path)
if scene == null:
return
var instance := scene.instantiate()
instance.global_position = spawn_position
get_tree().root.add_child(instance)
# High-frequency position sync — unreliable_ordered is acceptable here.
# transfer_channel separates this stream from other RPC traffic.
@rpc("authority", "unreliable_ordered", "call_local", 1)
func sync_position(pos: Vector2) -> void:
global_position = pos
```
**Sending to specific peers:**
```gdscript
# Send to everyone (including self if call_local is set):
send_chat_message.rpc("Hello!")
# Send to one specific peer:
send_chat_message.rpc_id(target_peer_id, "Hello!")
```
### C#
```csharp
// Chat.cs
using Godot;
public partial class Chat : Node
{
// Any peer can call; executes on the server only.
[Rpc(MultiplayerApi.RpcMode.AnyPeer, TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
public void SendChatMessage(string text)
{
if (!Multiplayer.IsServer()) return;
int senderId = Multiplayer.GetRemoteSenderId();
Rpc(MethodName.BroadcastChat, senderId, text);
}
// Authority only; runs on every peer including the caller.
[Rpc(MultiplayerApi.RpcMode.Authority,
CallLocal = true,
TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
private void BroadcastChat(int senderId, string text)
=> GD.Print($"[{senderId}]: {text}");
// Client → server: request a spawn.
[Rpc(MultiplayerApi.RpcMode.AnyPeer, TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
public void RequestSpawn(string scenePath, Vector2 spawnPosition)
{
if (!Multiplayer.IsServer()) return;
var scene = GD.Load<PackedScene>(scenePath);
if (scene == null) return;
var instance = scene.Instantiate<Node2D>();
instance.GlobalPosition = spawnPosition;
GetTree().Root.AddChild(instance);
}
// High-frequency position sync.
[Rpc(MultiplayerApi.RpcMode.Authority,
CallLocal = true,
TransferMode = MultiplayerPeer.TransferModeEnum.UnreliableOrdered,
TransferChannel = 1)]
public void SyncPosition(Vector2 pos)
=> GlobalPosition = pos;
}
```
**Sending to specific peers in C#:**
```csharp
// Broadcast to all:
Rpc(MethodName.SendChatMessage, "Hello!");
// Send to one peer:
RpcId(targetPeerId, MethodName.SendChatMessage, "Hello!");
```
---
## 4. Authority Model
Every node has exactly one authoritative peer — the peer that is permitted to send state updates for that node. Other peers should treat incoming state as read-only.
### GDScript
```gdscript
# player.gd
extends CharacterBody2D
func _ready() -> void:
# multiplayer.get_unique_id() returns this peer's ID.
# The server assigns authority during spawn (see Section 6).
pass
func _physics_process(delta: float) -> void:
# Guard: only the authority peer reads input and moves.
if not is_multiplayer_authority():
return
var direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
velocity = direction * 200.0
move_and_slide()
# Broadcast position to all other peers.
sync_position.rpc(global_position)
@rpc("authority", "unreliable_ordered", "call_local", 1)
func sync_position(pos: Vector2) -> void:
if not is_multiplayer_authority():
global_position = pos
# Check who owns this node at runtime:
func print_authority_info() -> void:
print("My peer ID : %d" % multiplayer.get_unique_id())
print("Authority : %d" % get_multiplayer_authority())
print("Am I auth? : %s" % str(is_multiplayer_authority()))
```
### C#
```csharp
// Player.cs
using Godot;
public partial class Player : CharacterBody2D
{
public override void _PhysicsProcess(double delta)
{
// Guard: only the authority peer processes input.
if (!IsMultiplayerAuthority()) return;
var direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
Velocity = direction * 200f;
MoveAndSlide();
Rpc(MethodName.SyncPosition, GlobalPosition);
}
[Rpc(MultiplayerApi.RpcMode.Authority,
CallLocal = true,
TransferMode = MultiplayerPeer.TransferModeEnum.UnreliableOrdered,
TransferChannel = 1)]
private void SyncPosition(Vector2 pos)
{
if (!IsMultiplayerAuthority())
GlobalPosition = pos;
}
}
```
**API summary:**
| Method | Returns | Notes |
|--------|---------|-------|
| `multiplayer.get_unique_id()` | `int` | This peer's ID |
| `get_multiplayer_authority()` | `int` | ID of the peer that owns this node |
| `is_multiplayer_authority()` | `bool` | True if this peer owns this node |
| `set_multiplayer_authority(id)` | `void` | Transfer ownership; call on the server |
---
## 5. Spawning Networked Objects
Use `MultiplayerSpawner` to replicate scene instances across peers. The server adds a child to the spawned node's parent, the spawner mirrors it on every peer with synchronized state. For dynamic spawn paths, configure `_spawnable_scenes` and call `add_child(scene.instantiate())` only on the server.
> See [references/spawning-networked-objects.md](references/spawning-networked-objects.md) for `MultiplayerSpawner` scene setup and the spawn-on-server flow (GDScript + C#).
---
## 6. Player Join Flow
The full lobby-join lifecycle: peer connects → server allocates a slot → load lobby scene → spawn player node → broadcast peer-list to all clients → on "start match" RPC, transition all peers to gameplay scene.
> See [references/player-join-flow.md](references/player-join-flow.md) for the full GDScript and C# implementation (peer-connected handler, slot allocation, lobby state, gameplay transition).
---
## 7. Disconnect Handling
Listen for `peer_disconnected(id)` on the `multiplayer` API. On the server: free the disconnected peer's player node and broadcast the updated peer-list. On clients: detect a server-disconnect and route to a reconnect / main-menu screen.
> See [references/disconnect-handling.md](references/disconnect-handling.md) for the timeout detection settings, server-side cleanup, and client-side reconnect flow (GDScript + C#).
---
## 8. Common Pitfalls
| Pitfall | Symptom | Fix |
|---------|---------|-----|
| Calling an RPC on the wrong authority | `rpc_id` silently ignored; method never runs | Check `is_multiplayer_authority()` before sending; use `"any_peer"` only where intentional |
| Desync from unordered RPCs | Positions jitter or snap | Use `"unreliable_ordered"` for streams; use `"reliable"` for critical state changes |
| Reading input in `_process` vs `_physics_process` | Movement desyncs on different frame rates | Always move `CharacterBody2D` in `_physics_process`; send sync RPCs from there too |
| Not checking `is_multiplayer_authority()` before input | Every peer controls every player | Add an `if not is_multiplayer_authority(): return` guard at the top of input handling |
| Spawning without `MultiplayerSpawner` | Object appears on server, missing on clients | Every runtime `add_child` on the server that should be replicated must go through `MultiplayerSpawner.spawn()` |
| Forgetting `call_local` on authority RPCs | Server state diverges from its own node | Add `"call_local"` when the sender also needs to execute the RPC locally |
| Using `rpc()` before the peer is assigned | Crash or silent failure | Assign `multiplayer.multiplayer_peer` before calling any RPC |
| Not stripping `res://` scenes from exported builds | Clients can read server-only scripts | Use `export_exclude` or PCK encryption for sensitive server code |
---
## 9. Checklist
- [ ] `ENetMultiplayerPeer.create_server()` / `create_client()` return `OK` before assigning to `multiplayer.multiplayer_peer`
- [ ] All four multiplayer signals connected: `peer_connected`, `peer_disconnected`, `connected_to_server`, `connection_failed`
- [ ] Every node that reads player input guards with `if not is_multiplayer_authority(): return`
- [ ] Input processing and `sync_position` RPC are both in `_physics_process`, not `_process`
- [ ] RPC modes chosen deliberately: `"any_peer"` only for client → server calls; `"authority"` for server → client
- [ ] Unreliable RPCs used only for high-frequency updates (position, rotation); reliable for events (spawn, damage, chat)
- [ ] `MultiplayerSpawner` configured with all spawnable scenes before the first player joins
- [ ] `set_multiplayer_authority(peer_id)` called on the server after each player node is spawned
- [ ] `peer_disconnected` handler frees the player node and removes it from tracking collections
- [ ] `server_disconnected` handler on clients returns to main menu and nulls `multiplayer.multiplayer_peer`
- [ ] `is_instance_valid()` checked before dereferencing any stored node reference in disconnect callbacks
- [ ] No `rpc()` calls made before `multiplayer.multiplayer_peer` is assigned
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!