add-multiplayer
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAdd Multiplayer (PartyKit / Cloudflare Durable Objects)
添加多人游戏功能(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 wired through EventBus that mirrors the existing
NetworkManagerexternal-service pattern.playfun.js - Additive edits to ,
EventBus,GameState, andConstants— single-player gameplay must remain identical when the server is unreachable.render_game_to_text()
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 and synchronize via .
network:player-joinednetwork:state-received为现有单人浏览器游戏添加实时或回合制多人功能。本技能可自动生成以下内容:
- PartyKit服务器(每个房间对应一个Durable Object),部署在Cloudflare边缘节点。
- 客户端,通过EventBus连接,遵循现有
NetworkManager外部服务模式。playfun.js - 对、
EventBus、GameState和Constants进行增量式修改——当服务器不可达时,单人游戏玩法必须保持完全一致。render_game_to_text()
默认状态为“单人模式正常运行”。如果WebSocket连接失败,NetworkManager会捕获错误,游戏将像以前一样在本地运行。连接成功后,远程玩家会通过事件加入,并通过事件同步状态。
network:player-joinednetwork:state-receivedReference Files
参考文件
- — event taxonomy, GameState schema, NetworkManager contract, Phaser vs Three.js placement notes.
architecture.md - — server templates (
partykit-server.mdandrealtime.ts), state shape, broadcast helpers, rate limiting.turn-based.ts - —
client-integration.md,MultiplayerClient,NetworkManagersource, EventBus/GameState/Constants append patterns,RemotePlayerRegistryextension.render_game_to_text - —
deploy.mdandnpx partykit devwalkthrough, capturing the deployed URL,npx partykit deployhandling, and client redeploy..env
- — 事件分类、GameState Schema、NetworkManager协议、Phaser与Three.js适配说明。
architecture.md - — 服务器模板(
partykit-server.md和realtime.ts)、状态结构、广播工具、速率限制。turn-based.ts - —
client-integration.md、MultiplayerClient、NetworkManager源码、EventBus/GameState/Constants追加模式、RemotePlayerRegistry扩展方法。render_game_to_text - —
deploy.md和npx partykit dev操作指南、获取部署URL、npx partykit deploy配置、客户端重新部署步骤。.env
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 instead of throwing.
network:disconnected - Additive edits only. Append to ,
EventBus.js,GameState.js,Constants.js, andmain.jsunder arender_game_to_text()banner. Never rename, remove, or change existing fields.// === Multiplayer === - 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 mode validation is light (last-write-wins). In
realtimemode validation is strict (rejects out-of-turn moves).turn-based - Backend-agnostic client API. All calls go through
partysocket. If a future user wants Colyseus or fly.io+ws, onlyMultiplayerClientchanges — game code does not.MultiplayerClient.js - Default room is . No matchmaking UI in v1. Users override by emitting
'lobby'with a custom room id.multiplayer:join-room
以下为必须遵守的规则,而非建议:
- 单人模式必须支持离线运行。当服务器不可达时,游戏必须能正常启动、运行和重置。NetworkManager会捕获所有连接错误,并触发事件,而非抛出异常。
network:disconnected - 仅允许增量式修改。在、
EventBus.js、GameState.js、Constants.js和main.js中,在render_game_to_text()标记下追加内容。绝不能重命名、删除或修改现有字段。// === Multiplayer === - EventBus是唯一交互接口。NetworkManager仅通过事件与游戏其他部分通信——禁止直接从场景、系统或实体导入到NetworkManager(反之亦然)。
- 服务器拥有权威但具备容错性。PartyKit房间持有房间的标准状态。客户端发送操作意图,服务器验证后广播。在模式下验证较为宽松(最后写入获胜);在
realtime模式下验证严格(拒绝非当前回合的操作)。turn-based - 客户端API与后端无关。所有调用均通过
partysocket进行。如果未来用户想要切换到Colyseus或fly.io+ws,只需修改MultiplayerClient——游戏代码无需改动。MultiplayerClient.js - 默认房间为。v1版本不包含匹配UI。用户可通过触发
'lobby'事件并传入自定义房间ID来覆盖默认设置。multiplayer:join-room
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.jswithsrc/main.js).window.render_game_to_text() - Node.js 18+.
- A Cloudflare account for (the CLI walks the user through login on first deploy; free tier is sufficient for prototyping).
npx partykit deploy
- 已使用本插件生成的Phaser 3或Three.js游戏(包含、
src/core/EventBus.js、src/core/GameState.js、带有src/core/Constants.js的window.render_game_to_text())。src/main.js - Node.js 18+版本。
- Cloudflare账户(用于,首次部署时CLI会引导用户登录;免费套餐足以满足原型开发需求)。
npx partykit deploy
Instructions
操作步骤
The user wants to add multiplayer to the game at (or the current directory if no path given). Optional (default) or chooses the server template.
$ARGUMENTS--mode=realtime--mode=turn-based用户希望在指定的游戏中添加多人功能(若未指定路径则使用当前目录)。可选参数(默认)或用于选择服务器模板。
$ARGUMENTS--mode=realtime--mode=turn-basedStep 0: Locate and read the game
步骤0:定位并分析游戏
Parse for the game path and flag. If no path, use cwd. Verify it's a game by reading and confirming Phaser or Three.js dependency.
$ARGUMENTS--modepackage.jsonRead these files in full before touching anything:
- — engine + scripts.
package.json - — orchestrator,
src/main.js,window.render_game_to_text().window.advanceTime() - — exact event names already in use.
src/core/EventBus.js - — current state shape and
src/core/GameState.jssemantics.reset() - — config block conventions.
src/core/Constants.js - if present — pipeline context.
progress.md
Then tell the creator one sentence confirming what you saw:
Game iswith<engine>events and a<N>entity. I'll add a multiplayer layer that broadcasts the local<player|bird|ship>'s state at<entity>and renders remote players from server broadcasts. Single-player will continue to work when the server is offline.TICK_RATE_HZ
解析获取游戏路径和参数。若未指定路径,则使用当前工作目录。通过读取并确认Phaser或Three.js依赖来验证是否为游戏项目。
$ARGUMENTS--modepackage.json在修改任何内容前,完整阅读以下文件:
- — 引擎版本与脚本配置。
package.json - — 编排器、
src/main.js、window.render_game_to_text()。window.advanceTime() - — 已使用的具体事件名称。
src/core/EventBus.js - — 当前状态结构与
src/core/GameState.js语义。reset() - — 配置块约定。
src/core/Constants.js - 若存在— 项目流程上下文。
progress.md
然后向创建者确认你所了解的信息,例如:
该游戏基于**引擎,包含<engine>**个事件和<N>实体。我将添加一个多人游戏层,以<player|bird|ship>频率广播本地TICK_RATE_HZ的状态,并根据服务器广播渲染远程玩家。当服务器离线时,单人模式仍可正常运行。<entity>
Step 1: Choose sync mode
步骤1:选择同步模式
Pick the server template:
| Mode | When to use | Wire model |
|---|---|---|
| Action games, runners, dodgers, platformers, anything with continuous movement | Local |
| Card games, board games, puzzles, anything with discrete moves | EventBus events ( |
If the user did not pass , infer from the game's existing events. If you see continuous-position events (, , position-updating physics), use . If you see discrete actions (, ), use . State the choice and proceed.
--modebird:flapplayer:movedrealtimecard:playedmove:submittedturn-based选择服务器模板:
| 模式 | 使用场景 | 通信模型 |
|---|---|---|
| 动作游戏、跑酷游戏、躲避类游戏、平台游戏等包含连续移动的游戏 | 本地以 |
| 卡牌游戏、棋盘游戏、解谜游戏等包含离散操作的游戏 | EventBus事件(如 |
若用户未指定,则根据游戏现有事件推断。如果存在连续位置事件(如、、更新位置的物理事件),则使用模式。如果存在离散操作事件(如、),则使用模式。说明选择的模式并继续操作。
--modebird:flapplayer:movedrealtimecard:playedmove:submittedturn-basedStep 2: Scaffold the server
步骤2:生成服务器代码
Create a sibling directory inside the game project. See for the full template content.
multiplayer-server/partykit-server.mdCreate:
- — manifest with
multiplayer-server/partykit.json(use the game's directory name),name,main: "src/server.ts".compatibilityDate - —
multiplayer-server/package.jsondep,partykit/devscripts.deploy - — minimal TypeScript config that PartyKit accepts.
multiplayer-server/tsconfig.json - — paste the appropriate template from
multiplayer-server/src/server.ts(partykit-server.mdorrealtime).turn-based - —
multiplayer-server/.gitignore,node_modules..partykit
Run to install (which provides for the client too via npm workspaces, but we'll add explicitly to the client).
cd multiplayer-server && npm installpartykitpartysocketpartysocket在游戏项目内创建同级目录。完整模板内容请参考。
multiplayer-server/partykit-server.md创建以下文件:
- — 清单文件,包含
multiplayer-server/partykit.json(使用游戏目录名称)、name、main: "src/server.ts"。compatibilityDate - —
multiplayer-server/package.json依赖、partykit/dev脚本。deploy - — PartyKit接受的极简TypeScript配置。
multiplayer-server/tsconfig.json - — 从
multiplayer-server/src/server.ts中粘贴对应的模板(partykit-server.md或realtime)。turn-based - — 忽略
multiplayer-server/.gitignore、node_modules。.partykit
运行安装(通过npm工作区为客户端提供,但我们会显式为客户端添加依赖)。
cd multiplayer-server && npm installpartykitpartysocketpartysocketStep 3: Scaffold the client
步骤3:生成客户端代码
Create three new files. See for the full source.
client-integration.md- — backend-agnostic interface around
src/multiplayer/MultiplayerClient.js(partysocket,connect,send,onMessage,disconnect).isConnected - —
src/multiplayer/RemotePlayerRegistry.jswithMap<playerId, RemotePlayer>,upsert,remove,prune(staleMs).list() - — wires MultiplayerClient ↔ EventBus, owns the broadcast tick (in
src/systems/NetworkManager.jsmode), handles reconnect with exponential backoff, emitsrealtimeevents.network:*
Add to the game's deps:
partysocketpackage.jsonbash
cd <game-path> && npm install partysocket创建三个新文件。完整源码请参考。
client-integration.md- — 基于
src/multiplayer/MultiplayerClient.js的后端无关接口(包含partysocket、connect、send、onMessage、disconnect方法)。isConnected - —
src/multiplayer/RemotePlayerRegistry.js结构,包含Map<playerId, RemotePlayer>、upsert、remove、prune(staleMs)方法。list() - — 连接MultiplayerClient与EventBus,管理广播定时器(
src/systems/NetworkManager.js模式下),处理指数退避重连,触发realtime事件。network:*
将添加到游戏的依赖中:
partysocketpackage.jsonbash
cd <game-path> && npm install partysocketStep 4: Append to existing core files
步骤4:修改现有核心文件
Make additive edits only. See for full schemas and for the exact append blocks.
architecture.mdclient-integration.mdsrc/core/EventBus.js// === Multiplayer ===js
// === 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.jsmultiplayerroomIdplayerIdconnectedremotePlayersreset()src/core/Constants.jsMULTIPLAYERSERVER_URLDEFAULT_ROOMMAX_PLAYERSTICK_RATE_HZPROTOCOL_VERSIONsrc/main.jswindow.__NETWORK_MANAGER__window.render_game_to_text()multiplayer: {...}remotePlayers: [...]仅允许增量式修改。完整Schema请参考,具体追加代码块请参考。
architecture.mdclient-integration.mdsrc/core/EventBus.js// === Multiplayer ===js
// === 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.jsmultiplayerroomIdplayerIdconnectedremotePlayersreset()src/core/Constants.jsMULTIPLAYERSERVER_URLDEFAULT_ROOMMAX_PLAYERSTICK_RATE_HZPROTOCOL_VERSIONsrc/main.jswindow.__NETWORK_MANAGER__window.render_game_to_text()multiplayer: {...}remotePlayers: [...]Step 5: Wire the local game into the network tick
步骤5:将本地游戏接入网络定时器
Inspect existing events. The wiring depends on mode:
realtimesetIntervalTICK_RATE_HZGameStateclient.send({type: 'state', payload: {...}})network:state-receivedRemotePlayerRegistry.upsert()turn-basedcard:playedmove:submittednetwork:state-receivedIn Phaser games, remote-player rendering happens in the active GameScene — instantiate sprites on , update positions on , destroy on . In Three.js games, the active orchestrator () creates and updates remote-player meshes.
network:player-joinednetwork:state-receivednetwork:player-leftGame.jsSee for example scene patches for both engines.
client-integration.md检查现有事件。接入方式取决于所选模式:
realtimeTICK_RATE_HZsetIntervalGameStateclient.send({type: 'state', payload: {...}})network:state-receivedRemotePlayerRegistry.upsert()turn-basedcard:playedmove:submittednetwork:state-received在Phaser游戏中,远程玩家渲染在活跃的GameScene中完成——在事件触发时实例化精灵,在事件触发时更新位置,在事件触发时销毁精灵。在Three.js游戏中,活跃的编排器()创建并更新远程玩家网格。
network:player-joinednetwork:state-receivednetwork:player-leftGame.js两种引擎的场景补丁示例请参考。
client-integration.mdStep 6: Deploy the server
步骤6:部署服务器
Run the dev server first to confirm everything works locally:
bash
cd <game-path>/multiplayer-server && npx partykit devThis starts a local CF Worker emulator on . In another terminal, set in and run the client ().
http://127.0.0.1:1999VITE_MULTIPLAYER_SERVER_URL=http://127.0.0.1:1999<game-path>/.envcd <game-path> && npm run devFor first-time deployment, the user must authenticate with PartyKit. Always pass — the default flow is broken in 2026 (the dashboard.partykit.io callback was retired after Cloudflare absorbed PartyKit, and login hangs forever):
--provider githubclerkbash
cd <game-path>/multiplayer-server && npx partykit login --provider githubThis uses GitHub's device-code OAuth flow. The CLI prints a code; the user visits , pastes it, and authorizes. Credentials persist in . See for the full walkthrough and troubleshooting.
https://github.com/login/device~/.partykit/config.jsondeploy.mdAfter login, deploy:
bash
cd <game-path>/multiplayer-server && npx partykit deployCapture the deployed URL from the output (format: ). The TLS cert may take 30-60 seconds to provision after the deploy reports success.
https://<project>.<cloudflare-username>.partykit.devUpdate three places with the deployed URL:
- →
src/core/Constants.jsMULTIPLAYER.SERVER_URL - →
<game-path>/.envVITE_MULTIPLAYER_SERVER_URL=https://... - →
<game-path>/.env.exampleVITE_MULTIPLAYER_SERVER_URL=https://your-project.your-username.partykit.dev
Add to if not already present.
.env.gitignoreSee for the full walkthrough including offline-first authentication and troubleshooting.
deploy.md先启动开发服务器,确认本地运行正常:
bash
cd <game-path>/multiplayer-server && npx partykit dev这会在启动本地CF Worker模拟器。在另一个终端中,在中设置,然后启动客户端()。
http://127.0.0.1:1999<game-path>/.envVITE_MULTIPLAYER_SERVER_URL=http://127.0.0.1:1999cd <game-path> && npm run dev首次部署时,用户必须通过PartyKit认证。请始终使用参数——默认的流程在2026年已失效(Cloudflare收购PartyKit后,dashboard.partykit.io的回调已停用,登录会无限挂起):
--provider githubclerkbash
cd <game-path>/multiplayer-server && npx partykit login --provider github这会使用GitHub的设备码OAuth流程。CLI会打印一个代码;用户访问,粘贴代码并授权。凭证会保存在中。完整操作指南和故障排查请参考。
https://github.com/login/device~/.partykit/config.jsondeploy.md登录完成后,进行部署:
bash
cd <game-path>/multiplayer-server && npx partykit deploy从输出中获取部署后的URL(格式:)。部署成功后,TLS证书可能需要30-60秒才能生效。
https://<project>.<cloudflare-username>.partykit.dev在以下三个位置更新部署后的URL:
- →
src/core/Constants.jsMULTIPLAYER.SERVER_URL - →
<game-path>/.envVITE_MULTIPLAYER_SERVER_URL=https://... - →
<game-path>/.env.exampleVITE_MULTIPLAYER_SERVER_URL=https://your-project.your-username.partykit.dev
若未在中,添加进去。
.env.gitignore完整操作指南包括离线优先认证和故障排查,请参考。
deploy.mdStep 7: Redeploy the client
步骤7:重新部署客户端
Reuse the existing host detection logic (same as Step 5):
monetize-game- If exists → redeploy via
.herenow/state.json.~/.agents/skills/here-now/scripts/publish.sh dist/ - Else if is configured and the repo has a GitHub Pages workflow →
gh.npx gh-pages -d dist - Else if is configured →
vercel.vercel --prod - Else ask the user how they want to redeploy.
Always run first.
npm run build复用现有的主机检测逻辑(与步骤5相同):
monetize-game- 若存在→ 通过
.herenow/state.json重新部署。~/.agents/skills/here-now/scripts/publish.sh dist/ - 否则,若配置了且仓库包含GitHub Pages工作流 →
gh。npx gh-pages -d dist - 否则,若配置了→
vercel。vercel --prod - 否则,询问用户希望如何重新部署。
请始终先运行。
npm run buildStep 8: Verify
步骤8:验证
Build cleanly:
bash
cd <game-path> && npm run build
cd multiplayer-server && npm run build # if a build script existsSingle-player fallback (critical): with the partykit dev server stopped, reload . The game must boot, play, and reset normally. Confirm fired and no uncaught errors in the console. If the game depends on the server to start, you violated Principle 1 — revise.
http://localhost:3000network:disconnectedTwo-tab smoke test: start in one terminal and in another. Open two browser tabs at . Confirm:
npx partykit devnpm run devhttp://localhost:3000- Both tabs fire (check console).
network:connected - Each tab's includes the other tab in
window.render_game_to_text().remotePlayers - Moving the local entity in tab A is reflected in tab B's remote-player rendering within ms.
1000 / TICK_RATE_HZ * 2
Reconnect: kill the partykit dev server, wait, restart it. The client should reconnect within and re-emit .
RECONNECT_MAX_BACKOFF_MSnetwork:connectedRegression: existing must still pass. Single-player invariants (boot, score, game-over, reset) must hold whether the server is up or down.
tests/e2e/*.spec.js构建是否正常:
bash
cd <game-path> && npm run build
cd multiplayer-server && npm run build # 若存在build脚本单人模式回退(关键验证): 停止partykit开发服务器,重新加载。游戏必须能正常启动、运行和重置。确认事件已触发,控制台无未捕获错误。如果游戏依赖服务器才能启动,说明违反了原则1,请修改。
http://localhost:3000network:disconnected双标签冒烟测试: 在一个终端启动,另一个终端启动。打开两个浏览器标签访问。确认:
npx partykit devnpm run devhttp://localhost:3000- 两个标签均触发事件(查看控制台)。
network:connected - 每个标签的输出中包含另一个标签的玩家信息在
window.render_game_to_text()中。remotePlayers - 在标签A中移动本地实体,标签B中的远程玩家渲染应在毫秒内更新。
1000 / TICK_RATE_HZ * 2
重连测试: 停止partykit开发服务器,等待后重启。客户端应在时间内重新连接,并再次触发事件。
RECONNECT_MAX_BACKOFF_MSnetwork:connected回归测试: 现有必须全部通过。无论服务器是否在线,单人模式的不变量(启动、得分、游戏结束、重置)必须保持一致。
tests/e2e/*.spec.jsStep 9: Update progress.md
progress.md步骤9:更新progress.md
progress.mdAppend a section:
## Multiplayermarkdown
undefined追加章节:
## Multiplayermarkdown
undefinedMultiplayer
Multiplayer
- Backend: PartyKit (Cloudflare Durable Objects)
- Server URL: https://<project>.<user>.partykit.dev
- Mode: realtime | turn-based
- Max players per room: 4
- Tick rate: 20 Hz (realtime mode)
- Default room: lobby
- Known limitations (v1): no matchmaking UI, no spectator mode, no persistent accounts, server-side rate limiting only.
undefined- 后端: PartyKit (Cloudflare Durable Objects)
- 服务器URL: https://<project>.<user>.partykit.dev
- 模式: realtime | turn-based
- 每个房间最大玩家数: 4
- Tick频率: 20 Hz(实时模式)
- 默认房间: lobby
- 已知限制(v1): 无匹配UI、无 spectator 模式、无持久化账户、仅服务器端速率限制。
undefinedOutput
输出信息
Tell the user:
- What was added — server in , client in
multiplayer-server/+src/multiplayer/, additive edits to four core files.src/systems/NetworkManager.js - The server URL — . Already wired into Constants and
https://<project>.<user>.partykit.dev..env - How to test locally — then
cd multiplayer-server && npx partykit dev, open two tabs.npm run dev - The single seam for backend swaps — point at . Future Colyseus or fly.io migration only changes that one file.
src/multiplayer/MultiplayerClient.js - Costs — free on Cloudflare's Workers free tier (100k requests/day, 1GB DO storage). Mention the user owns the deployed Cloudflare project; PartyKit deploys to their CF account, not OpusGameLabs'.
告知用户以下内容:
- 已添加的内容 — 目录下的服务器代码、
multiplayer-server/目录下的客户端代码 +src/multiplayer/、对四个核心文件的增量修改。src/systems/NetworkManager.js - 服务器URL — 。已在Constants和
https://<project>.<user>.partykit.dev中配置完成。.env - 本地测试方法 — ,然后运行
cd multiplayer-server && npx partykit dev,打开两个标签页。npm run dev - 后端切换的唯一接口 — 指向。未来迁移到Colyseus或fly.io只需修改该文件。
src/multiplayer/MultiplayerClient.js - 成本说明 — Cloudflare Workers免费套餐即可使用(每日10万次请求,1GB DO存储)。说明用户拥有部署的Cloudflare项目;PartyKit部署到用户自己的CF账户,而非OpusGameLabs的账户。
Example Usage
示例用法
Default (realtime mode in current directory)
默认情况(当前目录下的实时模式)
/add-multiplayerResult: detects engine, scaffolds with the realtime template, creates client networking files, deploys server, redeploys client, prints play URL and server URL.
multiplayer-server//add-multiplayer结果:检测引擎,使用实时模板生成,创建客户端网络文件,部署服务器,重新部署客户端,打印游戏URL和服务器URL。
multiplayer-server/Turn-based explicit
显式指定回合制模式
/add-multiplayer ./examples/card-game --mode=turn-basedResult: uses the turn-based server template; NetworkManager forwards the game's existing move events instead of running a position-broadcast tick.
/add-multiplayer ./examples/card-game --mode=turn-based结果:使用回合制服务器模板;NetworkManager转发游戏现有操作事件,而非运行位置广播定时器。
Verbose dry-run for inspection
详细干运行用于检查
/add-multiplayer --dry-runResult: prints the full file list and patches without writing or deploying. Useful for review before committing.
/add-multiplayer --dry-run结果:打印完整文件列表和修改内容,不执行写入或部署操作。适合提交前的审查。
Troubleshooting
故障排查
npx partykit login
redirects to dashboard.partykit.io/patience
and never completes
npx partykit logindashboard.partykit.io/patiencenpx partykit login
重定向到dashboard.partykit.io/patience
且无法完成登录
npx partykit logindashboard.partykit.io/patienceCause: The default provider was retired after Cloudflare absorbed PartyKit; the dashboard the OAuth callback expects is gone.
Fix: Use instead — GitHub device-code flow, prints a code, you paste at . Credentials persist in .
clerknpx partykit login --provider githubhttps://github.com/login/device~/.partykit/config.json原因: Cloudflare收购PartyKit后,默认的认证提供商已停用;OAuth回调依赖的控制台已不存在。
解决方法: 使用替代——GitHub设备码流程,打印一个代码,用户在粘贴并授权。凭证保存在中。
clerknpx partykit login --provider githubhttps://github.com/login/device~/.partykit/config.jsonRemote players don't appear even though the connection succeeded
连接成功但远程玩家未显示
Cause: Welcome-race — the WebSocket arrived before the scene's registered its listener. The events fired into the void.
Fix: After registering the listener, seed from directly. See → "Welcome-race gotcha" for the idempotent pattern.
welcomecreate()NETWORK_PLAYER_JOINEDgameState.multiplayer.remotePlayersclient-integration.md原因: 欢迎事件竞争——WebSocket的消息在场景方法注册监听器之前到达,事件被丢弃。
解决方法: 注册监听器后,直接从加载初始数据。幂等模式请参考 → "欢迎事件竞争问题"。
welcomecreate()NETWORK_PLAYER_JOINEDgameState.multiplayer.remotePlayersclient-integration.mdTwo tabs connect but never see each other
两个标签已连接但无法看到彼此
Cause: They joined different rooms (random room IDs from URL parsing) or the server's broadcast logic excludes the sender by default.
Fix: Check the room id in on both tabs — it should be the same (default ). If different, audit for stray query-string parsing. The server template's excludes the sender, which is correct — each client renders only remote players, not itself.
window.render_game_to_text().multiplayer.roomId'lobby'NetworkManager.connect()room.broadcast(message, [sender.id])原因: 它们加入了不同的房间(URL解析生成的随机房间ID),或者服务器默认广播逻辑排除了发送者。
解决方法: 检查两个标签的是否相同(默认应为)。若不同,检查中的URL查询字符串解析逻辑。服务器模板的会排除发送者,这是正确的——每个客户端仅渲染远程玩家,而非自身。
window.render_game_to_text().multiplayer.roomId'lobby'NetworkManager.connect()room.broadcast(message, [sender.id])Remote players appear stuck at last position when a peer closes the tab
当某个玩家关闭标签页时,远程玩家停留在最后位置
Cause: did not fire (browser killed the tab without a clean close), or the client did not run .
Fix: The server's handler is the canonical "player left" signal. Additionally, NetworkManager runs on every tick — verify this is wired. If a remote player has not sent state in , prune emits even without an explicit close.
onCloseRemotePlayerRegistry.prune()onCloseRemotePlayerRegistry.prune(STALE_PLAYER_MS)STALE_PLAYER_MSnetwork:player-left原因: 事件未触发(浏览器未正常关闭标签页),或者客户端未执行。
解决方法: 服务器的处理程序是“玩家离开”的权威信号。此外,NetworkManager在每次定时器触发时运行——请确认已接入该逻辑。如果远程玩家在时间内未发送状态,即使没有显式关闭事件,prune也会触发事件。
onCloseRemotePlayerRegistry.prune()onCloseRemotePlayerRegistry.prune(STALE_PLAYER_MS)STALE_PLAYER_MSnetwork:player-leftGame lags or stutters when many remote players are present
存在多个远程玩家时游戏卡顿或掉帧
Cause: Either too-high (you're broadcasting and rendering 60 times per second) or the scene re-creates remote-player sprites every frame instead of reusing them.
Fix: Lower to 20 (default) or 10 for slow games. Confirm scenes maintain a and only update positions on , never recreate.
TICK_RATE_HZTICK_RATE_HZMap<playerId, sprite>network:state-received原因: 过高(每秒广播和渲染60次),或者场景每帧重新创建远程玩家精灵而非复用现有精灵。
解决方法: 将降低到20(默认)或10(适合慢节奏游戏)。确认场景维护结构,仅在事件触发时更新位置,绝不重新创建精灵。
TICK_RATE_HZTICK_RATE_HZMap<playerId, sprite>network:state-receivedSingle-player tests fail after adding multiplayer
添加多人功能后单人测试失败
Cause: NetworkManager throws or blocks game boot when the server is unreachable. This violates Principle 1.
Fix: Audit and — both must catch all errors, log a warning, emit , and return. The constructor and must never throw out of .
MultiplayerClient.connect()NetworkManager.init()network:disconnectedinit()main.js原因: 当服务器不可达时,NetworkManager抛出异常或阻止游戏启动。违反了原则1。
解决方法: 检查和——两者必须捕获所有错误,记录警告,触发事件并返回。构造函数和方法绝不能在中抛出异常。
MultiplayerClient.connect()NetworkManager.init()network:disconnectedinit()main.jsrender_game_to_text()
snapshot tests fail
render_game_to_text()render_game_to_text()
快照测试失败
render_game_to_text()Cause: Tests use exact on the output; you added new top-level fields.
Fix: Regenerate baselines — additions are intentional and backward-compatible. The fields added are (object) and (array, may be empty).
toEqualmultiplayerremotePlayers原因: 测试使用精确的断言输出;你添加了新的顶级字段。
解决方法: 重新生成基准快照——添加的内容是有意的且向后兼容。新增字段为(对象)和(数组,可能为空)。
toEqualmultiplayerremotePlayersCloudflare deploy succeeds but the WebSocket fails in the browser
Cloudflare部署成功但浏览器中WebSocket连接失败
Cause: Mixed content (HTTP page → WSS server) or the server URL was written without the scheme.
Fix: Confirm is the full URL. derives the WSS URL by replacing the scheme. The deployed game must also be served over HTTPS for the WSS connection to succeed (here.now and GitHub Pages both serve HTTPS by default).
https://Constants.MULTIPLAYER.SERVER_URLhttps://...partykit.devpartysocket原因: 混合内容(HTTP页面连接WSS服务器)或服务器URL未包含协议。
解决方法: 确认是完整的 URL。会自动替换协议生成WSS URL。部署的游戏也必须通过HTTPS提供服务,才能成功建立WSS连接(here.now和GitHub Pages默认均提供HTTPS服务)。
https://Constants.MULTIPLAYER.SERVER_URLhttps://...partykit.devpartysocketFree tier rate limit hit
触发免费套餐速率限制
Cause: Many concurrent rooms or chatty clients.
Fix: Cloudflare's Workers free tier allows 100k requests/day. Each client tick at 20 Hz is one request — that's for a single 24/7 player. For prototyping you'll never hit this; for production, lower or upgrade to Workers Paid ($5/mo flat for 10M requests/day).
1.7M / dayTICK_RATE_HZ原因: 大量并发房间或频繁发送消息的客户端。
解决方法: Cloudflare Workers免费套餐允许每日10万次请求。单个玩家以20 Hz频率发送消息,每日可达170万次请求。原型开发绝不会触发该限制;生产环境可降低或升级到Workers付费套餐(每月5美元,每日1000万次请求)。
TICK_RATE_HZTips
提示
Runonce per game. If you later change modes, edit/add-multiplayerdirectly — both templates are checked in and the switch is small.multiplayer-server/src/server.tsThe defaultroom is suitable for a single open room. To support private rooms, emit'lobby'with a room id from a URL query string or invite code. NetworkManager listens for that event and reconnects to the new room.multiplayer:join-roomFor graduation to a more featureful backend (matchmaking, schema sync, server-authoritative physics), the only file that needs to change is. Replace thesrc/multiplayer/MultiplayerClient.jscalls with Colyseus'spartysocketclient; keep the same public API (colyseus.js,connect,send,onMessage,disconnect).isConnectedThe server runs in your Cloudflare account, not OpusGameLabs'. Costs and quotas accrue to you. PartyKit itself is open source and free; you only pay Cloudflare's pass-through pricing (free tier is generous).
每个游戏只需运行一次。若后续需要切换模式,直接编辑/add-multiplayer即可——两个模板均已提交,切换改动很小。multiplayer-server/src/server.ts默认的房间适合单个公开房间。若要支持私人房间,可从URL查询字符串或邀请码获取房间ID,触发'lobby'事件。NetworkManager监听该事件并重新连接到新房间。multiplayer:join-room若要升级到功能更丰富的后端(匹配、Schema同步、服务器权威物理),只需修改文件。将src/multiplayer/MultiplayerClient.js调用替换为Colyseus的partysocket客户端;保持相同的公共API(colyseus.js、connect、send、onMessage、disconnect)。isConnected服务器运行在你的Cloudflare账户中,而非OpusGameLabs的账户。成本和配额由你承担。PartyKit本身是开源免费的;你只需支付Cloudflare的实际使用费用(免费套餐非常慷慨)。