Add Multiplayer
Add real-time or turn-based multiplayer to an existing browser game using PartyKit (Cloudflare Durable Objects). Scaffolds a room-based server, a NetworkManager client, EventBus events, GameState fields, Constants, and extends render_game_to_text(). Use when the user says "add multiplayer", "make this multiplayer", "add real-time co-op", "add online play", "scaffold multiplayer", or "add netcode". Do NOT use for solo leaderboards (use monetize-game), single-player AI opponents (use add-feature), or peer-to-peer/WebRTC games.
- Skill ID
- playableintelligence/game-creator/add-multiplayer
- Publisher
- playableintelligence
- Repository
- game-creator
- Installs
- 0
- Files
- 5
- License
- MIT
- Requires
- Requires an existing Phaser 3 or Three.js game with game-creator architecture (EventBus, GameState, Constants.js, render_game_to_text). Requires Node.js and a Cloudflare account for `npx partykit deploy` (free tier is sufficient).
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
playableintelligence/game-creator/add-multiplayerInstalls these files- SKILL.md
- architecture.md
- client-integration.md
- deploy.md
- partykit-server.md
What this skill tells the agent
Add Multiplayer (PartyKit / Cloudflare Durable Objects)
Add real-time or turn-based multiplayer to an existing single-player browser game. This skill scaffolds:
- A PartyKit server (one Durable Object per room) deployed to Cloudflare's edge.
- A client `NetworkManager` wired through EventBus that mirrors the existing
playfun.jsexternal-service pattern. - Additive edits to
EventBus,GameState,Constants, andrender_game_to_text()— single-player gameplay must remain identical when the server is unreachable.
The default state is "single-player works." If the WebSocket connection fails, NetworkManager swallows the error and the game runs locally as before. When connected, remote players appear via network:player-joined and synchronize via network:state-received.
Reference Files
architecture.md— event taxonomy, GameState schema, NetworkManager contract, Phaser vs Three.js placement notes.partykit-server.md— server templates (realtime.tsandturn-based.ts), state shape, broadcast helpers, rate limiting.client-integration.md—MultiplayerClient,NetworkManager,RemotePlayerRegistrysource, EventBus/GameState/Constants append patterns,render_game_to_textextension.deploy.md—npx partykit devandnpx partykit deploywalkthrough, capturing the deployed URL,.envhandling, and client redeploy.
Core Principles
These are rules, not guidelines:
- Single-player must work offline. With the server unreachable, the game must boot, play, and reset normally. NetworkManager catches all connection errors and emits
network:disconnectedinstead of throwing. - Additive edits only. Append to
EventBus.js,GameState.js,Constants.js,main.js, andrender_game_to_text()under a// === Multiplayer ===banner. Never rename, remove, or change existing fields. - EventBus is the only seam. NetworkManager talks to the rest of the game through events — no direct imports from scenes, systems, or entities into NetworkManager (or vice versa).
- Server is authoritative, but tolerant. The PartyKit room owns the canonical room state. Clients send intents; the server validates and broadcasts. In
realtimemode validation is light (last-write-wins). Inturn-basedmode validation is strict (rejects out-of-turn moves). - Backend-agnostic client API. All
partysocketcalls go throughMultiplayerClient. If a future user wants Colyseus or fly.io+ws, onlyMultiplayerClient.jschanges — game code does not. - Default room is `'lobby'`. No matchmaking UI in v1. Users override by emitting
multiplayer:join-roomwith a custom room id.
Prerequisites
- An existing Phaser 3 or Three.js game scaffolded with this plugin (has
src/core/EventBus.js,src/core/GameState.js,src/core/Constants.js,src/main.jswithwindow.render_game_to_text()). - Node.js 18+.
- A Cloudflare account for
npx partykit deploy(the CLI walks the user through login on first deploy; free tier is sufficient for prototyping).
Instructions
The user wants to add multiplayer to the game at $ARGUMENTS (or the current directory if no path given). Optional --mode=realtime (default) or --mode=turn-based chooses the server template.
Step 0: Locate and read the game
Parse $ARGUMENTS for the game path and --mode flag. If no path, use cwd. Verify it's a game by reading package.json and confirming Phaser or Three.js dependency.
Read these files in full before touching anything:
package.json— engine + scripts.src/main.js— orchestrator,window.render_game_to_text(),window.advanceTime().src/core/EventBus.js— exact event names already in use.src/core/GameState.js— current state shape andreset()semantics.src/core/Constants.js— config block conventions.progress.mdif present — pipeline context.
Then tell the creator one sentence confirming what you saw:
Game is `<engine>` with `<N>` events and a<player|bird|ship>entity. I'll add a multiplayer layer that broadcasts the local<entity>'s state atTICK_RATE_HZand renders remote players from server broadcasts. Single-player will continue to work when the server is offline.
Step 1: Choose sync mode
Pick the server template:
| Mode | When to use | Wire model |
|---|---|---|
realtime (default) | Action games, runners, dodgers, platformers, anything with continuous movement | Local setInterval at TICK_RATE_HZ broadcasts the local entity's {x, y, [z], score, state}; server fans out; clients render last-known remote state |
turn-based | Card games, board games, puzzles, anything with discrete moves | EventBus events (player:moved, player:played-card) forward as {type, payload} messages; server validates and broadcasts; clients apply on network:state-received |
If the user did not pass --mode, infer from the game's existing events. If you see continuous-position events (bird:flap, player:moved, position-updating physics), use realtime. If you see discrete actions (card:played, move:submitted), use turn-based. State the choice and proceed.
Step 2: Scaffold the server
Create a sibling multiplayer-server/ directory inside the game project. See partykit-server.md for the full template content.
Create:
multiplayer-server/partykit.json— manifest withname(use the game's directory name),main: "src/server.ts",compatibilityDate.multiplayer-server/package.json—partykitdep,dev/deployscripts.multiplayer-server/tsconfig.json— minimal TypeScript config that PartyKit accepts.multiplayer-server/src/server.ts— paste the appropriate template frompartykit-server.md(realtimeorturn-based).multiplayer-server/.gitignore—node_modules,.partykit.
Run cd multiplayer-server && npm install to install partykit (which provides partysocket for the client too via npm workspaces, but we'll add partysocket explicitly to the client).
Step 3: Scaffold the client
Create three new files. See client-integration.md for the full source.
src/multiplayer/MultiplayerClient.js— backend-agnostic interface aroundpartysocket(connect,send,onMessage,disconnect,isConnected).src/multiplayer/RemotePlayerRegistry.js—Map<playerId, RemotePlayer>withupsert,remove,prune(staleMs),list().src/systems/NetworkManager.js— wires MultiplayerClient ↔ EventBus, owns the broadcast tick (inrealtimemode), handles reconnect with exponential backoff, emitsnetwork:*events.
Add partysocket to the game's package.json deps:
cd <game-path> && npm install partysocketStep 4: Append to existing core files
Make additive edits only. See architecture.md for full schemas and client-integration.md for the exact append blocks.
`src/core/EventBus.js` — append under // === Multiplayer === banner:
// === Multiplayer ===
NETWORK_CONNECTED: 'network:connected',
NETWORK_DISCONNECTED: 'network:disconnected',
NETWORK_PLAYER_JOINED: 'network:player-joined',
NETWORK_PLAYER_LEFT: 'network:player-left',
NETWORK_STATE_RECEIVED: 'network:state-received',
MULTIPLAYER_JOIN_ROOM: 'multiplayer:join-room',
MULTIPLAYER_LEAVE_ROOM: 'multiplayer:leave-room',`src/core/GameState.js` — append a multiplayer field with persistent (roomId, playerId) and transient (connected, remotePlayers) parts. Update reset() to clear only the transient parts so rejoin works after a game restart.
`src/core/Constants.js` — append a MULTIPLAYER block with SERVER_URL (filled by Step 6), DEFAULT_ROOM, MAX_PLAYERS, TICK_RATE_HZ, reconnect backoff, stale-player threshold, PROTOCOL_VERSION. No magic numbers — every value is a named constant.
More from playableintelligence
Use this skill in a project
Open any RiverX project, search the Skills panel for playableintelligence/game-creator/add-multiplayer, and install it in one click.
