关于 add-multiplayer
面向已有 Phaser 3 或 Three.js 浏览器游戏的技能,可为单机游戏添加实时或回合制多人联机脚手架。它通过 EventBus 接入网络,并要求服务器不可用时单机玩法仍正常运行。
主要功能
- 01搭建 PartyKit 房间服务器
- 02提供实时与回合制同步模式
- 03通过 EventBus 连接网络管理器
- 04离线时保留单机玩法
- 05支持远程玩家状态与房间事件
适用场景
- 01为现有浏览器游戏添加多人房间
- 02为动作游戏同步玩家位置
- 03为卡牌或棋盘游戏转发回合操作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ playableintelligence/ game-creator --skill add-multiplayer安装后在 Claude Code 中输入/add-multiplayer确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
architecture.mdclient-integration.mddeploy.mdpartykit-server.md详细介绍
添加多人游戏(PartyKit / Cloudflare Durable Objects)
为现有的单人浏览器游戏添加实时或回合制多人游戏。此技能会搭建:
- 一个部署到 Cloudflare 边缘网络的 PartyKit 服务(每个房间对应一个 Durable Object)。
- 一个通过 EventBus 接入的客户端
NetworkManager,与现有的playfun.js外部服务模式保持一致。 - 对
EventBus、GameState、Constants和render_game_to_text()进行增量修改——服务器无法访问时,单人游戏的玩法必须保持不变。
默认状态是“单人游戏可正常运行”。如果 WebSocket 连接失败,NetworkManager 会吞掉错误,游戏仍像以前一样在本地运行。连接成功后,其他玩家会通过 network:player-joined 加入,并通过 network:state-received 同步状态。
参考文件
architecture.md— 事件分类、GameState 架构、NetworkManager 契约,以及 Phaser 与 Three.js 的代码放置说明。partykit-server.md— 服务端模板(realtime.ts和turn-based.ts)、状态结构、广播辅助函数和速率限制。client-integration.md—MultiplayerClient、NetworkManager、RemotePlayerRegistry源码,以及 EventBus/GameState/Constants 的追加模式和render_game_to_text扩展。deploy.md—npx partykit dev和npx partykit deploy部署流程、已部署 URL 的获取、.env处理方式及客户端重新部署。
核心原则
以下是规则,而非建议:
- 单人游戏必须支持离线运行。 服务器无法访问时,游戏必须能正常启动、运行和重置。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调用都通过MultiplayerClient进行。如果将来用户想使用 Colyseus 或 fly.io+ws,只需修改MultiplayerClient.js——游戏代码无需改动。 - 默认房间为
'lobby'。 v1 中没有匹配界面。用户可通过发出带有自定义房间 ID 的multiplayer:join-room来覆盖默认房间。
前置条件
- 已使用此插件搭建的 Phaser 3 或 Three.js 游戏框架(包含
src/core/EventBus.js、src/core/GameState.js、src/core/Constants.js、src/main.js,且具有window.render_game_to_text())。 - Node.js 18+。
- 用于运行
npx partykit deploy的 Cloudflare 账号(首次部署时,CLI 会引导用户登录;原型开发使用免费套餐即可)。
说明
用户希望为 $ARGUMENTS 指定的游戏(如果未指定路径,则使用当前目录)添加多人游戏。可选参数 --mode=realtime(默认)或 --mode=turn-based 用于选择服务端模板。
步骤 0:定位并读取游戏
解析 $ARGUMENTS,获取游戏路径和 --mode 标志。如果没有指定路径,则使用当前工作目录。读取 package.json 并确认其中包含 Phaser 或 Three.js 依赖,以验证这是一个游戏。
在修改任何内容之前,完整读取以下文件:
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>的状态,并根据服务器广播渲染远程玩家。服务器离线时,单人游戏仍可继续运行。
步骤 1:选择同步模式
选择服务器模板:
| 模式 | 适用场景 | 线路模型 |
|---|---|---|
realtime(默认) | 动作游戏、跑酷游戏、躲避游戏、平台游戏,以及任何具有连续移动的游戏 | 本地 setInterval 以 TICK_RATE_HZ 广播本地实体的 {x, y, [z], score, state};服务器转发消息;客户端渲染最近一次已知的远程状态 |
turn-based | 卡牌游戏、棋盘游戏、谜题,以及任何采用离散操作的游戏 | EventBus 事件(player:moved、player:played-card)以 {type, payload} 消息转发;服务器验证并广播;客户端在 network:state-received 时应用 |
如果用户没有传入 --mode,则根据游戏现有事件推断。如果看到连续位置事件(bird:flap、player:moved、更新位置的物理事件),则使用 realtime。如果看到离散操作(card:played、move:submitted),则使用 turn-based。说明选择的模式并继续。
步骤 2:搭建服务器
在游戏项目中创建一个同级目录 multiplayer-server/。完整模板内容请参阅 partykit-server.md。
创建以下文件:
multiplayer-server/partykit.json— 清单,包含name(使用游戏目录名称)、main: "src/server.ts"、compatibilityDate。multiplayer-server/package.json—partykit依赖项,以及dev/deploy脚本。multiplayer-server/tsconfig.json— PartyKit 可接受的最精简 TypeScript 配置。multiplayer-server/src/server.ts— 粘贴partykit-server.md中相应的模板(realtime或turn-based)。multiplayer-server/.gitignore—node_modules、.partykit。
运行 cd multiplayer-server && npm install 以安装 partykit(它也会通过 npm 工作区为客户端提供 partysocket,但我们会在客户端显式添加 partysocket)。
步骤 3:搭建客户端
创建三个新文件。完整源码请参阅 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()。src/systems/NetworkManager.js— 连接 MultiplayerClient 和 EventBus,负责广播计时(在realtime模式下),处理带指数退避的重连,并发出network:*事件。
将 partysocket 添加到游戏的 package.json 依赖项中:
cd <game-path> && npm install partysocket
步骤 4:追加到现有核心文件
仅进行增量修改。完整架构请参阅 architecture.md,准确的追加代码块请参阅 client-integration.md。
src/core/EventBus.js — 在 // === Multiplayer === 标记下追加:
// === 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 — 追加一个 multiplayer 字段,包含持久部分(roomId、playerId)和临时部分(connected、remotePlayers)。更新 reset(),使其仅清除临时部分,以便游戏重启后可以重新加入。
src/core/Constants.js — 追加一个 MULTIPLAYER 区块,包含 SERVER_URL(在步骤 6 中填写)、DEFAULT_ROOM、MAX_PLAYERS、TICK_RATE_HZ、重连退避时间、玩家过期阈值和 PROTOCOL_VERSION。不要使用魔法数字——每个值都必须是命名常量。
src/main.js — 在实例化 EventBus 和 GameState 后、引擎启动前实例化 NetworkManager。将其暴露为 window.__NETWORK_MANAGER__,供测试使用。扩展 window.render_game_to_text(),以增量方式包含 multiplayer: {...} 和 remotePlayers: [...]。
步骤 5:将本地游戏接入网络 tick
检查现有事件。接入方式取决于模式:
realtime:NetworkManager 使用 setInterval,按 TICK_RATE_HZ 运行。每次 tick 都会从 GameState 读取本地实体,并调用 client.send({type: 'state', payload: {...}})。无需订阅 EventBus——它只需采样 GameState。在相关场景或系统中添加一个 network:state-received 监听器,调用 RemotePlayerRegistry.upsert() 并触发重新渲染。
turn-based:NetworkManager 订阅游戏现有的移动事件(例如 card:played、move:submitted)并转发这些事件。场景或系统监听 network:state-received,并应用经过验证的远程移动。本地乐观 UI 可以使用,但服务器才是事实来源。
在 Phaser 游戏中,远程玩家的渲染发生在当前活动的 GameScene 中——在 network:player-joined 时实例化精灵,在 network:state-received 时更新位置,在 network:player-left 时销毁精灵。在 Three.js 游戏中,当前活动的协调器(Game.js)会创建并更新远程玩家网格。
两种引擎的场景补丁示例请参阅 client-integration.md。
步骤 6:部署服务端
先运行开发服务器,确认所有功能在本地正常工作:
cd <game-path>/multiplayer-server && npx partykit dev
这会在 http://127.0.0.1:1999 启动本地 CF Worker 模拟器。在另一个终端中,将 VITE_MULTIPLAYER_SERVER_URL=http://127.0.0.1:1999 设置到 <game-path>/.env 中,然后运行客户端(cd <game-path> && npm run dev)。
首次部署时,用户必须通过 PartyKit 进行身份验证。始终传入 --provider github——默认的 clerk 流程在 2026 年已不可用(Cloudflare 收购 PartyKit 后,dashboard.partykit.io 的回调已停用,登录会一直卡住):
cd <game-path>/multiplayer-server && npx partykit login --provider github
此命令使用 GitHub 的设备代码 OAuth 流程。CLI 会打印一个代码;用户访问 https://github.com/login/device,粘贴该代码并授权。凭据会保存在 ~/.partykit/config.json 中。完整操作步骤和故障排除请参阅 deploy.md。
登录后,进行部署:
cd <game-path>/multiplayer-server && npx partykit deploy
从输出中获取已部署的 URL(格式:https://<project>.<cloudflare-username>.partykit.dev)。部署报告成功后,TLS 证书可能需要 30–60 秒才能配置完成。
在以下三个位置更新已部署的 URL:
src/core/Constants.js→MULTIPLAYER.SERVER_URL<game-path>/.env→VITE_MULTIPLAYER_SERVER_URL=https://...<game-path>/.env.example→VITE_MULTIPLAYER_SERVER_URL=https://your-project.your-username.partykit.dev
如果 .gitignore 中尚未包含 .env,请将其添加进去。
离线优先身份验证和故障排除的完整操作步骤请参阅 deploy.md。
步骤 7:重新部署客户端
复用现有的主机检测逻辑(与 monetize-game 步骤 5 相同):
- 如果
.herenow/state.json存在 → 通过~/.agents/skills/here-now/scripts/publish.sh dist/重新部署。 - 否则,如果已配置
gh,且仓库有 GitHub Pages 工作流 →npx gh-pages -d dist。 - 否则,如果已配置
vercel→vercel --prod。 - 否则,询问用户希望如何重新部署。
务必先运行 npm run build。
步骤 8:验证
确保构建干净:
cd <game-path> && npm run build
cd multiplayer-server && npm run build # if a build script exists
**单人游戏回退(关键):**停止 partykit 开发服务器后,重新加载 http://localhost:3000。游戏必须能够正常启动、游玩和重置。确认触发了 network:disconnected,且控制台中没有未捕获的错误。如果游戏依赖服务器才能启动,就违反了原则 1——请修改。
**双标签页冒烟测试:**在一个终端中启动 npx partykit dev,并在另一个终端中运行 npm run dev。在 http://localhost:3000 打开两个浏览器标签页。确认:
- 两个标签页都触发了
network:connected(检查控制台)。 - 每个标签页的
window.render_game_to_text()都会在remotePlayers中包含另一个标签页。 - 在标签页 A 中移动本地实体后,标签页 B 中的远程玩家渲染会在
1000 / TICK_RATE_HZ * 2毫秒内反映这一变化。
**重连:**关闭 partykit 开发服务器,等待后重新启动。客户端应在 RECONNECT_MAX_BACKOFF_MS 内重连,并再次触发 network:connected。
**回归:**现有的 tests/e2e/*.spec.js 必须继续通过。无论服务器是否运行,单人游戏的不变量(启动、得分、游戏结束、重置)都必须成立。
步骤 9:更新 progress.md
追加一个 ## Multiplayer 章节:
## 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.
输出
告知用户:
- 新增内容——
multiplayer-server/中的服务器、src/multiplayer/+src/systems/NetworkManager.js中的客户端,以及对四个核心文件的增量修改。 - 服务器 URL——
https://<project>.<user>.partykit.dev。已配置到 Constants 和.env中。 - 如何在本地测试——运行
cd multiplayer-server && npx partykit dev,然后运行npm run dev,并打开两个标签页。 - 替换后端的唯一接口——指向
src/multiplayer/MultiplayerClient.js。未来迁移到 Colyseus 或 fly.io 只需修改这个文件。 - 费用——使用 Cloudflare Workers 免费套餐可免费运行(每天 100k 次请求、1GB DO 存储)。说明用户拥有已部署的 Cloudflare 项目;PartyKit 部署到用户自己的 CF 账户,而不是 OpusGameLabs' 的账户。
用法示例
默认(在当前目录中使用实时模式)
/add-multiplayer
结果:检测游戏引擎,使用实时模板搭建 multiplayer-server/,创建客户端网络文件,部署服务器,重新部署客户端,并输出游戏 URL 和服务器 URL。
显式指定回合制
/add-multiplayer ./examples/card-game --mode=turn-based
结果:使用回合制服务器模板;NetworkManager 转发游戏现有的移动事件,而不是运行位置广播计时器。
用于检查的详细试运行
/add-multiplayer --dry-run
结果:输出完整的文件列表和补丁,但不会写入文件或部署。适合在提交前审查。
故障排除
npx partykit login 重定向到 dashboard.partykit.io/patience 后一直无法完成
**原因:**Cloudflare 收购 PartyKit 后,默认的 clerk 提供方已停用;OAuth 回调所需的控制面板已不存在。
**修复方法:**改用 npx partykit login --provider github——通过 GitHub 设备码流程登录,命令会输出一个代码,将其粘贴到 https://github.com/login/device。凭据会保存在 ~/.partykit/config.json 中。
连接成功,但看不到远程玩家
**原因:**欢迎消息竞态——WebSocket 的 welcome 消息在场景的 create() 注册 NETWORK_PLAYER_JOINED 监听器之前到达。事件触发时没有监听器接收。
**修复方法:**注册监听器后,直接从 gameState.multiplayer.remotePlayers 初始化。幂等模式请参阅 client-integration.md →“欢迎消息竞态注意事项”。
两个标签页都已连接,但始终无法看到彼此
原因: 它们加入了不同的房间(从 URL 解析出随机房间 ID),或者服务端的广播逻辑默认排除了发送者。
解决方法: 检查两个标签页中的 window.render_game_to_text().multiplayer.roomId — 它们应当相同(默认值为 'lobby')。如果不同,请检查 NetworkManager.connect(),排查是否存在多余的查询字符串解析。服务端模板中的 room.broadcast(message, [sender.id]) 会排除发送者,这是正确的 — 每个客户端只渲染远程玩家,而不会渲染自身。
对等端关闭标签页后,远程玩家仍停留在最后的位置
原因: onClose 未触发(浏览器未正常关闭就终止了标签页),或者客户端未运行 RemotePlayerRegistry.prune()。
解决方法: 服务端的 onClose 处理程序是“玩家离开”的权威信号。此外,NetworkManager 会在每个 tick 上运行 RemotePlayerRegistry.prune(STALE_PLAYER_MS) — 请确认此逻辑已接入。如果远程玩家在 STALE_PLAYER_MS 时间内未发送状态,即使没有明确的关闭事件,prune 也会发出 network:player-left。
远程玩家较多时,游戏出现延迟或卡顿
原因: TICK_RATE_HZ 过高(每秒广播和渲染 60 次),或者场景每帧都会重新创建远程玩家精灵,而不是复用它们。
解决方法: 将 TICK_RATE_HZ 降至 20(默认值),慢节奏游戏可设为 10。确认场景维护了 Map<playerId, sprite>,并且只在 network:state-received 时更新位置,绝不重新创建。
添加多人游戏功能后,单人游戏测试失败
原因: 服务器无法访问时,NetworkManager 抛出异常或阻止游戏启动。这违反了原则 1。
解决方法: 检查 MultiplayerClient.connect() 和 NetworkManager.init() — 两者都必须捕获所有错误、记录警告、发出 network:disconnected,然后返回。构造函数和 init() 绝不能让异常从 main.js 中向外传播。
render_game_to_text() 快照测试失败
原因: 测试对输出使用精确的 toEqual,而你添加了新的顶层字段。
解决方法: 重新生成基线 — 新增字段是有意添加的,且向后兼容。新增字段为 multiplayer(对象)和 remotePlayers(数组,可能为空)。
Cloudflare 部署成功,但浏览器中的 WebSocket 连接失败
原因: 混合内容(HTTP 页面 → WSS 服务端),或者服务端 URL 未写入 https:// 协议。
解决方法: 确认 Constants.MULTIPLAYER.SERVER_URL 是完整的 https://...partykit.dev URL。partysocket 会通过替换协议来生成 WSS URL。游戏也必须通过 HTTPS 提供服务,WSS 连接才能成功(here.now 和 GitHub Pages 默认都使用 HTTPS)。
触发免费套餐的速率限制
原因: 并发房间过多,或客户端发送消息过于频繁。
解决方法: Cloudflare Workers 免费套餐每天允许 100k 次请求。每个客户端每秒以 20 Hz 发送一次 tick,相当于每次请求 — 单个玩家全天候运行时,每天会产生 1.7M / day 次请求。原型开发不会触发此限制;用于生产环境时,请降低 TICK_RATE_HZ 或升级到 Workers Paid(每月固定收费 $5,每天 10M 次请求)。
提示
每个游戏运行一次
/add-multiplayer。如果之后更改模式,直接编辑multiplayer-server/src/server.ts——两个模板都已纳入版本控制,切换只需少量改动。默认的
'lobby'房间适用于单个开放房间。要支持私人房间,请通过 URL 查询字符串或邀请码发送带有房间 ID 的multiplayer:join-room事件。NetworkManager 会监听此事件,并重新连接到新房间。如果要升级到功能更丰富的后端(匹配、架构同步、由服务器权威控制的物理模拟),只需更改
src/multiplayer/MultiplayerClient.js。将partysocket调用替换为 Colyseus 的colyseus.js客户端;保留相同的公共 API(connect、send、onMessage、disconnect、isConnected)。服务运行在你的 Cloudflare 账户中,而不是 OpusGameLabs 的账户中。费用和配额由你承担。PartyKit 本身是开源且免费的;你只需支付 Cloudflare 的转付定价(免费套餐十分慷慨)。