agentleFS
jame581jame581/GodotPrompter/skills/multiplayer-basics/SKILL.md

multiplayer-basics

Skill870 starsChanged 21 days agoMITView on GitHub

Use when implementing multiplayer — MultiplayerAPI, ENet/WebSocket peers, RPCs, and authority model

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 2 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    All examples target Godot 4.3+ with no deprecated APIs.

    line 8
  2. Distinctive:

    Every node has exactly one authoritative peer — the peer that is permitted to send state updates for that node.

    line 177

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 IDRole
1The 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 — on every peer, since the call is local and is not replicated.

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 MultiplayerAPI signals (peer_connected, peer_disconnected, connected_to_server, connection_failed, plus server_disconnected — see §7). 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


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

ModeWho may call itExecutes on
"authority" (default)Only the authority peerThe peer(s) it is sent to
"any_peer"Any connected peerThe peer(s) it is sent to

Transfer Modes

ModeDeliveryOrderUse For
"reliable"GuaranteedIn-orderChat, spawn events, important state
"unreliable"Best-effortUnorderedHigh-frequency position updates
"unreliable_ordered"Best-effortIn-order per channelSmooth movement streams

GDScript

# chat.gd
extends Node2D

# 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 : Node2D
{
    // 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 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

# player.gd
extends CharacterBody2D

func _ready() -> void:
	# multiplayer.get_unique_id() = this peer's ID; server assigns authority during spawn (see Section 6).
	pass


func _physics_process(delta: float) -> void:
	# Guard: authority-only input and movement.
	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()

	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


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#

// Player.cs
using Godot;

public partial class Player : CharacterBody2D
{
    public override void _PhysicsProcess(double delta)
    {
        // Guard: authority-only 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:

MethodReturnsNotes
multiplayer.get_unique_id()intThis peer's ID
get_multiplayer_authority()intID of the peer that owns this node
is_multiplayer_authority()boolTrue if this peer owns this node
set_multiplayer_authority(id)voidTransfer ownership; local only — call it on every peer (e.g. in the spawn function)

5. Spawning Networked Objects

Use MultiplayerSpawner to replicate scene instances across peers. Register the scenes (add_spawnable_scene() or the inspector's Auto Spawn List); when the server adds an instance of one as a direct child of the spawn_path node, the spawner instantiates it on every peer. For a spawn that needs data (position, owner), set spawn_function and call spawn(data) on the server.

See 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 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 for the timeout detection settings, server-side cleanup, and client-side reconnect flow (GDScript + C#).


8. Common Pitfalls

PitfallSymptomFix
Calling an RPC on the wrong authorityMethod never runs; the receiver logs RPC '…' is not allowed on node …Check is_multiplayer_authority() before sending; use "any_peer" only where intentional
Desync from unordered RPCsPositions jitter or snapUse "unreliable_ordered" for streams; use "reliable" for critical state changes
Reading input in _process vs _physics_processMovement desyncs on different frame ratesAlways move CharacterBody2D in _physics_process; send sync RPCs from there too
Not checking is_multiplayer_authority() before inputEvery peer controls every playerAdd an if not is_multiplayer_authority(): return guard at the top of input handling
Spawning without MultiplayerSpawnerObject appears on server, missing on clientsAdd replicated instances under a MultiplayerSpawner's spawn_path (scene in its spawnable list), or use its spawn() with a spawn_function
Forgetting call_local on authority RPCsServer state diverges from its own nodeAdd "call_local" when the sender also needs to execute the RPC locally
Calling rpc() before the client has connectedError Trying to call an RPC via a multiplayer peer which is not connected.On clients, send RPCs only after connected_to_server (before a peer is assigned, the default OfflineMultiplayerPeer runs only call_local RPCs, and only locally)
Not stripping res:// scenes from exported buildsClients can read server-only scriptsLeave server-only files out of the client preset (Resources tab: export mode or exclude filters), or use PCK encryption for sensitive server code

9. Checklist

  • ENetMultiplayerPeer.create_server() / create_client() return OK before assigning to multiplayer.multiplayer_peer
  • All five multiplayer signals connected: peer_connected, peer_disconnected, connected_to_server, connection_failed, server_disconnected
  • 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 every peer when each player node is spawned (e.g. inside the spawn_function)
  • 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 client rpc() calls made before connected_to_server fires

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.