Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build networked games with Godot 4.x 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.
.claude/skills/gamedev-skills-godot-multiplayer/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-08 | ✗→✓ | ▲ Improved | -10% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 50% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 71% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 22% | 0% |
| case-09 | ✓→✓ | = Same ✓ | 42% | 0% |
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.
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.
ENetMultiplayerPeer), call create_server(port, max) orcreate_client(ip, port), and assign it to multiplayer.multiplayer_peer. The server's unique ID is always 1; clients get random positive IDs.
multiplayer: peer_connected(id),peer_disconnected(id), connected_to_server, connection_failed, server_disconnected.
@rpc(...). Call them on a Callable via rpc() (all peers) orrpc_id(peer_id) (one peer). Inside, multiplayer.get_remote_sender_id() tells you who sent it.
all @rpc methods in a script; mismatches break silently.
set_multiplayer_authority(id); gate input/RPCs byis_multiplayer_authority().
MultiplayerSpawner (auto-instances scenes on clients) andMultiplayerSynchronizer (auto-syncs selected properties).
gdscriptconst 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()
gdscriptfunc _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: 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 @rpc("authority", "call_local", "reliable") func spawn_projectile(owner_id: int) -> void: _do_spawn(owner_id)
gdscriptextends 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()
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). sync.set_visibility_for(target_peer_id, true)
@rpc method in a script must exist with the samedeclaration 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.
@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.
with identical names on all peers (use MultiplayerSpawner or add_child(node, true) for readable, deterministic names).
hits) directly. Send intent, validate on the server, then broadcast results.
@rpc methods must be on Node-derived classes, notplain Resource/RefCounted.
dictionaries, PackedArrays).
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new().
MultiplayerSpawner setup, transfer modes/channels, SceneMultiplayerauthentication (auth_callback/complete_auth), a lobby skeleton, and dedicated-server export notes, read references/replication-and-rpc.md.
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.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | pass→pass | 21,595 | 16,101 | -25% | 1 | 1 | 0% | 3,969 | 4,824 | +22% | 0 | 0 | — |
case-08 | fail→pass | 22,566 | 6,711 | -70% | 1 | 1 | 0% | 3,053 | 2,738 | -10% | 0 | 0 | — |
case-09 | pass→pass | 9,505 | 4,813 | -49% | 1 | 1 | 0% | 1,726 | 2,458 | +42% | 0 | 0 | — |
case-01 | fail→pass | 12,432 | 11,073 | -11% | 1 | 1 | 0% | 2,812 | 4,204 | +50% | 0 | 0 | — |
case-03 | pass→pass | 12,201 | 20,693 | +70% | 1 | 1 | 0% | 2,423 | 3,604 | +49% | 0 | 0 | — |
case-04 | pass→pass | 14,161 | 9,233 | -35% | 1 | 1 | 0% | 2,632 | 3,551 | +35% | 0 | 0 | — |
case-05 | pass→pass | 13,270 | 10,290 | -22% | 1 | 1 | 0% | 2,566 | 3,619 | +41% | 0 | 0 | — |
case-06 | pass→pass | 15,395 | 10,047 | -35% | 1 | 1 | 0% | 2,883 | 3,670 | +27% | 0 | 0 | — |
case-07 | pass→pass | 13,312 | 10,442 | -22% | 1 | 1 | 0% | 2,327 | 3,509 | +51% | 0 | 0 | — |
case-10 | pass→pass | 3,086 | 2,727 | -12% | 1 | 1 | 0% | 538 | 2,155 | +301% | 0 | 0 | — |
case-11 | pass→pass | 6,791 | 3,510 | -48% | 1 | 1 | 0% | 1,101 | 2,274 | +107% | 0 | 0 | — |
case-12 | pass→pass | 13,842 | 11,543 | -17% | 1 | 1 | 0% | 1,688 | 3,954 | +134% | 0 | 0 | — |
case-13 | pass→pass | 12,630 | 8,420 | -33% | 1 | 1 | 0% | 2,197 | 3,205 | +46% | 0 | 0 | — |
case-14 | pass→pass | 4,417 | 3,255 | -26% | 1 | 1 | 0% | 780 | 2,268 | +191% | 0 | 0 | — |
case-15 | pass→pass | 3,020 | 1,987 | -34% | 1 | 1 | 0% | 535 | 1,978 | +270% | 0 | 0 | — |
case-16 | pass→pass | 11,917 | 7,737 | -35% | 1 | 1 | 0% | 2,393 | 3,225 | +35% | 0 | 0 | — |
case-17 | pass→pass | 15,227 | 11,217 | -26% | 1 | 1 | 0% | 2,696 | 3,611 | +34% | 0 | 0 | — |
case-18 | fail→pass | 7,596 | 3,800 | -50% | 1 | 1 | 0% | 1,419 | 2,426 | +71% | 0 | 0 | — |
case-19 | pass→pass | 4,491 | 1,916 | -57% | 1 | 1 | 0% | 858 | 1,992 | +132% | 0 | 0 | — |
case-20 | pass→pass | 12,111 | 11,874 | -2% | 1 | 1 | 0% | 2,510 | 4,171 | +66% | 0 | 0 | — |
case-21 | pass→pass | 11,306 | 10,371 | -8% | 1 | 1 | 0% | 2,355 | 3,790 | +61% | 0 | 0 | — |
case-22 | pass→pass | 11,781 | 9,331 | -21% | 1 | 1 | 0% | 2,355 | 3,524 | +50% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 8/2/2026 | 0% |
Other measured skills in the registry, with their headline benchmark lift.