/multiplayer-basics
Use when implementing multiplayer — MultiplayerAPI, ENet/WebSocket peers, RPCs, and authority model
$ npx -y skills add jame581/GodotPrompter --skill multiplayer-basics --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/multiplayer-basics
Context preview
The summary Claude sees to decide when to auto-load this skill.
Use when implementing multiplayer — MultiplayerAPI, ENet/WebSocket peers, RPCs, and authority model
SKILL.md
multiplayer-basics.SKILL.mdname: 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`+ | Connected clients — randomly generated unique IDs, **not** sequential |
**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
Both sides use the same three steps: create an `ENetMultiplayerPeer`, call `create_server(port, max_clients)` or `create_client(address, port)`, then assign it to `multiplayer.multiplayer_peer` and connect the four `MultiplayerAPI` signals. **Check the `create_*` return value** — it returns an `Error`, and a silent `ERR_CANT_CREATE` (port already in use) otherwise looks exactly like a hang.
The server is always peer ID `1`; clients receive randomly generated unique IDs, so never assume they are sequential.
Full server and client implementations with every signal handler, in GDScript and C#: [references/enet-setup.md](references/enet-setup.md)
---
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
# chat.gd
extends Node
# Any peer can call; server validates 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 sync; unreliable_ordered + a channel keeps this off other RPC traffic.
@rpc("authority", "unreliable_ordered", "call_local", 1)
func sync_position(pos: Vector2) -> void:
global_position = pos**Sending to specific peers:**
# 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#
// 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#:**
// Broadcast to all:
Rpc(MethodName.SendChatMessage, "Hello!");
// Send to one peer:
RpcId(targetPeerId, MethodName.SendChatMessage, "Hello!");
---
4. Authority Model
Every node has exac
Read more
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`+ | Connected clients — randomly generated unique IDs, **not** sequential |
**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
Both sides use the same three steps: create an `ENetMultiplayerPeer`, call `create_server(port, max_clients)` or `create_client(address, port)`, then assign it to `multiplayer.multiplayer_peer` and connect the four `MultiplayerAPI` signals. **Check the `create_*` return value** — it returns an `Error`, and a silent `ERR_CANT_CREATE` (port already in use) otherwise looks exactly like a hang.
The server is always peer ID `1`; clients receive randomly generated unique IDs, so never assume they are sequential.
Full server and client implementations with every signal handler, in GDScript and C#: [references/enet-setup.md](references/enet-setup.md)
---
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
# chat.gd
extends Node
# Any peer can call; server validates 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 sync; unreliable_ordered + a channel keeps this off other RPC traffic.
@rpc("authority", "unreliable_ordered", "call_local", 1)
func sync_position(pos: Vector2) -> void:
global_position = pos**Sending to specific peers:**
# 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#
// 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#:**
// Broadcast to all: Rpc(MethodName.SendChatMessage, "Hello!"); // Send to one peer: RpcId(targetPeerId, MethodName.SendChatMessage, "Hello!");
---
4. Authority Model
Every node has exac
Agentic skills framework for Godot 4.x game development. Gives AI coding agents domain-specific expertise for GDScript and C# projects.
Other skills on godot-prompter.
- /authoring-godot-prompter-skills
Use when writing or editing a SKILL.md or an agent definition in this repo — required frontmatter, section ordering, and the GDScript-then-C# example convention.
Open skill - /releasing-godot-prompter
Use when cutting a GodotPrompter release or bumping its version — the version-bump sequence, tag-triggered workflow, and the marketplace manifests that must follow.
Open skill - /2d-essentials
Use when working with 2D-specific systems — TileMaps, parallax scrolling, 2D lights and shadows, canvas layers, particles 2D, custom drawing, and 2D meshes in Godot 4.3+
Open skill - /3d-essentials
Use when working with 3D-specific systems — materials, lighting, shadows, environment, global illumination, fog, LOD, occlusion culling, and decals in Godot 4.3+
Open skill - /ability-system
Use when building character abilities — Resource-based abilities with cost/cooldown/cast, buffs/debuffs, stat modifiers, gameplay tags, and HUD binding
Open skill - /addon-development
Use when creating Godot editor plugins — EditorPlugin, @tool scripts, custom inspectors, and dock panels
Open skill

