From summer
Adds multiplayer to existing single-player Godot games using MultiplayerAPI, MultiplayerSpawner, and MultiplayerSynchronizer. Guides authority model decisions (host, dedicated server, P2P) before writing netcode.
How this skill is triggered — by the user, by Claude, or both
Slash command
/summer:setup-multiplayer**/*.gd**/*.tscn**/*.tresproject.godotThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Multiplayer is a one-way door. Pick the wrong authority model and refactoring later is brutal. This skill enforces an opinionated path: **start with Godot's high-level MultiplayerAPI + MultiplayerSpawner + MultiplayerSynchronizer + ENet**. Don't roll custom networking unless you have a measured reason. Make the irreversible decisions up front, in writing.
Multiplayer is a one-way door. Pick the wrong authority model and refactoring later is brutal. This skill enforces an opinionated path: start with Godot's high-level MultiplayerAPI + MultiplayerSpawner + MultiplayerSynchronizer + ENet. Don't roll custom networking unless you have a measured reason. Make the irreversible decisions up front, in writing.
Core principle: the architecture decision (peer authority vs server authority) is harder than the code. Lock it before touching a single RPC.
Before any code:
- What kind of multiplayer? co-op LAN / co-op online / competitive PvP / lobbies + matchmaking
- How many players? 2 / 2–4 / 8+
- Who runs the simulation? host (one of the players) / dedicated server / authoritative peer-to-peer
- What's the network you're targeting? same Wi-Fi / public internet with ~80ms RTT / mobile
Wait. Each answer flips an irreversible knob.
Lock it now. Refactoring authority is a project-killer.
| Model | Best for | Pros | Cons |
|---|---|---|---|
| Host authority (one player runs the sim) | Co-op (2–4 players), casual PvP, friends-only | Cheapest. No server bill. Host migration possible. | Host advantage (no lag for them). Cheating possible. |
| Dedicated server authority | Competitive PvP, matchmaking, > 4 players | Fair. Anti-cheat possible. Stable. | Server bill. Need a deploy story. Higher complexity. |
| Peer-to-peer with shared authority | Lock-step strategy / RTS, fighting games (rollback) | Symmetric latency. No "host advantage". | Determinism is a project on its own. Hard to debug. |
Default recommendation: host authority for co-op (2–4 players), dedicated server for everything else. If user says "competitive PvP" + "lobbies": dedicated.
State the choice explicitly:
Going with host authority for 2–4 player co-op. The first player to start a game is the host. Their scene tree is the source of truth; clients receive replicated state and send input RPCs. Host migration is hard with this model — if the host quits, the game ends. Confirm?
Almost always: ENetMultiplayerPeer. It's UDP with reliability layers, ships with Godot, and just works for < 32 peers.
When to switch:
Replicate the minimum. Every replicated field is bandwidth + bug surface.
For a typical co-op shooter:
| Object | Replicated state | Replication frequency | Authority |
|---|---|---|---|
| Player position / rotation | x, y, z, yaw | 20 Hz | Owner peer |
| Player health | int | On change | Server/host |
| Player input intent | move_dir, jump, fire | 30 Hz | Owner peer (sends to host) |
| Enemy state | position, hp, FSM state | 10 Hz | Host only |
| World items (pickups) | spawn / despawn events | On change | Host only |
| UI / menus | nothing | nothing | Local only |
| Particles, decorative VFX | nothing | nothing | Local only (each peer plays its own) |
Don't replicate:
summer_get_scene_tree
summer_inspect_node "./World/Player"
Note: the existing single-player Player needs to become a per-peer Player. The path is to spawn one Player per connected peer.
I'm about to:
- Add a
Networkautoload (scripts/network.gd) that owns peer setup + spawn lifecycle.- Convert
./World/Playerfrom a static scene node into aMultiplayerSpawner-managed instance, spawned per peer on join.- Add a
MultiplayerSynchronizerto the player scene replicatingposition,rotation.y,health.- Convert player input from local → input intent RPC sent to the authority.
- Add a basic host/join UI on the main menu.
Authority model: host. Transport: ENet. Tick: 30 Hz input, 20 Hz position. Confirm before I touch anything?
scripts/network.gd:
class_name Network
extends Node
const PORT := 7000
const MAX_PLAYERS := 4
signal player_joined(peer_id: int)
signal player_left(peer_id: int)
@export var player_scene: PackedScene
var _peer: MultiplayerPeer
func host_game() -> Error:
var enet := ENetMultiplayerPeer.new()
var err := enet.create_server(PORT, MAX_PLAYERS - 1)
if err != OK:
return err
_peer = enet
multiplayer.multiplayer_peer = _peer
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
# Host spawns its own player at id=1 (server id)
_spawn_player(1)
return OK
func join_game(address: String) -> Error:
var enet := ENetMultiplayerPeer.new()
var err := enet.create_client(address, PORT)
if err != OK:
return err
_peer = enet
multiplayer.multiplayer_peer = _peer
return OK
func _on_peer_connected(peer_id: int) -> void:
# Only the host spawns players. Clients receive via MultiplayerSpawner.
if multiplayer.is_server():
_spawn_player(peer_id)
player_joined.emit(peer_id)
func _on_peer_disconnected(peer_id: int) -> void:
var node_name := str(peer_id)
var players := get_tree().get_first_node_in_group("players_root")
if players != null and players.has_node(node_name):
players.get_node(node_name).queue_free()
player_left.emit(peer_id)
func _spawn_player(peer_id: int) -> void:
var player: Node = player_scene.instantiate()
player.name = str(peer_id) # name = peer id; sync by name
player.set_multiplayer_authority(peer_id)
var players_root := get_tree().get_first_node_in_group("players_root")
players_root.add_child(player, true)
Register as autoload in project.godot:
summer_project_setting(name="autoload/Network", value="*res://scripts/network.gd")
In the player scene, add:
Player (CharacterBody3D)
├── CollisionShape3D
├── Camera3D # local-only, attach in script if peer is local
├── MeshInstance3D
└── MultiplayerSynchronizer
summer_add_node(parent="./World/Players/Player", type="MultiplayerSynchronizer", name="Sync")
Configure the synchronizer to replicate position, rotation.y, health. Save as a SceneReplicationConfig.tres (NOT inline sub_resource — silent-fail trap):
# scripts/player_net.gd
extends CharacterBody3D
@export var move_speed: float = 5.0
@export var max_health: int = 100
@onready var camera: Camera3D = $Camera3D
@onready var sync: MultiplayerSynchronizer = $Sync
var health: int = 100
func _ready() -> void:
health = max_health
var auth := get_multiplayer_authority()
var is_local := auth == multiplayer.get_unique_id()
camera.current = is_local
set_physics_process(is_local) # only the owner runs movement
set_process_input(is_local)
func _physics_process(delta: float) -> void:
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
if direction:
velocity.x = direction.x * move_speed
velocity.z = direction.z * move_speed
else:
velocity.x = move_toward(velocity.x, 0, move_speed)
velocity.z = move_toward(velocity.z, 0, move_speed)
if not is_on_floor():
velocity.y -= 9.8 * delta
move_and_slide()
# position is replicated automatically via MultiplayerSynchronizer
@rpc("any_peer", "call_local", "reliable")
func take_damage(amount: int) -> void:
if not multiplayer.is_server():
return # only host applies damage
health = max(0, health - amount)
if health == 0:
rpc("on_died")
@rpc("authority", "call_local", "reliable")
func on_died() -> void:
queue_free()
Lock the conventions:
| RPC config | Use case |
|---|---|
@rpc("authority", "call_local", "reliable") | Host tells everyone something happened (health change result, death) |
@rpc("any_peer", "call_remote", "unreliable") | Frequent input intent (movement, look) — drop is OK |
@rpc("any_peer", "call_remote", "reliable") | Rare input event (fire weapon, pick up item) — drop NOT OK |
@rpc("any_peer", "call_local", "reliable") | Player asks host to do something (request damage) |
Authority validation is mandatory. Inside any any_peer RPC, check multiplayer.get_remote_sender_id() and validate the sender owns the action.
@rpc("any_peer", "call_local", "reliable")
func request_fire() -> void:
var sender := multiplayer.get_remote_sender_id()
if sender != get_multiplayer_authority():
return # someone else is trying to control this player; drop
# ... actually fire
Defer to a specialist skill, but flag the basics:
multiplayer-and-networking/lag-compensation/SKILL.md when it ships.summer_save_scene
summer_get_script_errors
summer_play
# user tests by running two instances locally (Project → Run Multiple Instances → 2)
summer_stop
Test checklist:
| Don't | Do | Why |
|---|---|---|
| Decide authority model halfway through | Lock it in step 2 before touching code | Refactoring authority is a project-killer |
| Replicate every property | Replicate position, rotation, health, that's it | Bandwidth + bug surface |
| Run physics on every peer for every player | Physics on owner only, others receive transform | Otherwise jitter and divergence |
| Trust the client | Validate get_remote_sender_id() in every any_peer RPC | Cheaters route through the wrong RPC |
Inline SceneReplicationConfig sub_resource | Save as standalone .tres | Silent-fail trap (see references/mcp-tools-reference.md) |
| Roll custom transport | ENet (or WebSocket for browser) | You will hit edge cases ENet already solved |
| Skip authority check in damage RPC | if not multiplayer.is_server(): return at the top of damage handlers | Otherwise damage applied N times |
| Spawn players on every peer | MultiplayerSpawner — host spawns, clients receive | Otherwise N copies of each player |
@rpc("any_peer") everything | Use authority for server→clients, any_peer only for client→server | Mismatched flow = exploits |
| Camera replicated | Camera is local-only, set current = is_local on spawn | Each peer needs their own camera |
This skill rewires the project at the architectural level. Always ask before each block of changes. Group writes — "I'm about to add the autoload + 2 scenes + 3 scripts + InputMap entries. OK?". See references/collaborative-protocol.md.
For a known-good multiplayer scaffold (host + join UI, ENet transport, MultiplayerSpawner, replicated player), point users at:
→ template-id: TBD (template-co-op-online planned, see references/template-registry.md)
For now: this skill produces the scaffold inline. When the template lands, link from here.
references/mcp-tools-reference.md — full MCP tool list (project-setting, autoload registration)references/godot-version.md — MultiplayerAPI is medium-churn; SceneReplicationConfig settled in 4.0+references/collaborative-protocol.md — "May I write" patternreferences/gd-style.md — typed GDScript conventionsmultiplayer-and-networking/multiplayerapi-basics/SKILL.md — deeper MultiplayerAPI patternsmultiplayer-and-networking/client-server-pattern/SKILL.md — dedicated server architecturemultiplayer-and-networking/peer-replication/SKILL.md — synchronizer config patternsmultiplayer-and-networking/enet-vs-websocket/SKILL.md — transport choicemultiplayer-and-networking/lag-compensation/SKILL.md — deeper netcode for competitive (when shipped)multiplayer-and-networking/state-sync/SKILL.md — selective replication patternsmultiplayer-and-networking/lobby-and-matchmaking/SKILL.md — lobby UI + Steam matchmakingnpx claudepluginhub summerengine/summer-engine-agent --plugin summerGuides building peer-to-peer multiplayer architecture in Godot from scratch, covering network manager, authoritative state, routing, and real-time rendering. Activates on keywords like 'multiplayer', 'p2p', 'host'.
Implements multiplayer basics in Godot 4.3+ using MultiplayerAPI, ENet/WebSocket peers, RPCs, and authority model for client-server architecture.
Defines ownership, authority, replication, prediction, rollback, and reconciliation strategies for multiplayer systems. Useful when dealing with latency, authority issues, or networked state sync.