Skip to content
Development
Skill

/godot-multiplayer

Godot 4.7 high-level multiplayer: ENet host and join, @rpc with rpc_id(), MultiplayerSpawner and MultiplayerSynchronizer, with a two-instance headless test. Use for online or LAN co-op in Godot, or a rejected RPC, spawned nodes missing on clients, or state that does not sync.

From plugin
awesome-gamedev-agent-skills
8.6k79 skills

Claude Code

$ npx -y skills add gamedev-skills/awesome-gamedev-agent-skills --skill godot-multiplayer --agent claude-code

SKILL.md

godot-multiplayer.SKILL.md
name: godot-multiplayer
description: >
  Godot 4.7 high-level multiplayer: ENet host and join, @rpc with rpc_id(), MultiplayerSpawner and
  MultiplayerSynchronizer, with a two-instance headless test. Use for online or LAN co-op in
  Godot, or a rejected RPC, spawned nodes missing on clients, or state that does not sync. Not for
  Roblox (`roblox-networking`), server exports (`godot-export`) or local split-screen
  (`camera-systems`).

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** with ENet (see Version notes). Treat all client input as untrusted; keep the server authoritative.

When to use

  • Hosting/joining over ENet, writing 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 (`PacketPeer`); HTTP requests; Roblox → `roblox-networking`; persistence → `save-systems`.

Core workflow

1. **Create a peer** (`ENetMultiplayerPeer`), call `create_server(port, max_clients)` or `create_client(address, port)`, check the returned `Error`, and assign it to `multiplayer.multiplayer_peer`. The server's 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`. Print each one; the smoke test (step 8) reads those lines. 3. **Define RPCs** with `@rpc(...)` and call them on the method's `Callable` with `rpc()` (all peers) or `rpc_id(peer_id)` (one peer). Inside, `multiplayer.get_remote_sender_id()` says who sent it. 4. **Keep the RPC set identical** on every build that runs the script: Godot checksums all `@rpc` methods of a script, and a mismatch prints errors that can name an unrelated RPC. 5. **Assign authority** per node with `set_multiplayer_authority(id)`; gate input and movement on `is_multiplayer_authority()`. 6. **Replicate state** with `MultiplayerSpawner` (instances scenes on clients) and `MultiplayerSynchronizer` (syncs chosen properties). Both can be configured in code (Pattern 4), so no Replication-dock clicks are needed. 7. **Validate on the server.** Clients send intent; the server checks it and broadcasts results. 8. **Check with two headless instances before you finish.** Make the game host or join from command-line user args (Pattern 5), then:

   timeout 20 godot --headless --path . -- --host > server.log 2>&1 &
   sleep 2
   timeout 15 godot --headless --path . -- --join 127.0.0.1 > client.log 2>&1
   wait
   grep -nE "^ *(SCRIPT ERROR|ERROR):" server.log client.log   # no output = pass
   grep -n "connected" server.log client.log                   # your step-2 prints

Pass means both logs show the handshake prints and no error lines (RPC "not allowed", checksum and "Node not found" errors all print as `ERROR:`). With no Godot binary (`godot`, `godot4` or `$GODOT_BIN`), ask the user to run these lines and paste both logs. Gameplay feel still needs a human: ask the user to start two or more windows (editor menu Debug > Customize Run Instances...) and report what desyncs.

Patterns

1. Host or join (ENet)

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" % error_string(err)); return
    multiplayer.multiplayer_peer = peer
    multiplayer.peer_connected.connect(func(id: int): print("peer_connected ", id))

func join(address := "127.0.0.1") -> void:
    var peer := ENetMultiplayerPeer.new()
    var err := peer.create_client(address, PORT)
    if err != OK:
        push_error("Cannot join: %s" % error_string(err)); return
    multiplayer.multiplayer_peer = peer
    multiplayer.connected_to_server.connect(func(): print("connected_to_server"))

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.

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)

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_an
Read more
@gamedev-skills
Ships withawesome-gamedev-agent-skills

<img src="docs/assets/awesome-gamedev-agent-skills-banner.png" width="820" alt="awesome-gamedev-agent-skills — game-dev skills for AI coding agents.

Stars on GitHub

Momentum

stars in total
8.6k
Star history of gamedev-skills/awesome-gamedev-agent-skills on GitHub, a chart by star-history.com

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills13d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc2w293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react4d252k
@facebook@facebookInstall Skill

Questions about godot-multiplayer.

Which coding agents can use godot-multiplayer?
godot-multiplayer installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.