mcpField
A
add-multiplayer

add-multiplayer

github.com/playableintelligence开发350608 次安装22 周前更新
代码仓库

关于 add-multiplayer

面向已有 Phaser 3 或 Three.js 浏览器游戏的技能,可为单机游戏添加实时或回合制多人联机脚手架。它通过 EventBus 接入网络,并要求服务器不可用时单机玩法仍正常运行。

主要功能

  1. 01搭建 PartyKit 房间服务器
  2. 02提供实时与回合制同步模式
  3. 03通过 EventBus 连接网络管理器
  4. 04离线时保留单机玩法
  5. 05支持远程玩家状态与房间事件

适用场景

  1. 01为现有浏览器游戏添加多人房间
  2. 02为动作游戏同步玩家位置
  3. 03为卡牌或棋盘游戏转发回合操作

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/playableintelligence/game-creator --skill add-multiplayer

安装后在 Claude Code 中输入/add-multiplayer确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

architecture.md
client-integration.md
deploy.md
partykit-server.md

详细介绍

添加多人游戏(PartyKit / Cloudflare Durable Objects)

为现有的单人浏览器游戏添加实时或回合制多人游戏。此技能会搭建:

  1. 一个部署到 Cloudflare 边缘网络的 PartyKit 服务(每个房间对应一个 Durable Object)。
  2. 一个通过 EventBus 接入的客户端 NetworkManager,与现有的 playfun.js 外部服务模式保持一致。
  3. 对 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 处理方式及客户端重新部署。

核心原则

以下是规则,而非建议:

  1. 单人游戏必须支持离线运行。 服务器无法访问时,游戏必须能正常启动、运行和重置。NetworkManager 会捕获所有连接错误,并发出 network:disconnected,而不是抛出错误。
  2. 只做增量修改。 在 EventBus.js、GameState.js、Constants.js、main.js 和 render_game_to_text() 中的 // === Multiplayer === 标记下追加内容。绝不重命名、删除或修改现有字段。
  3. EventBus 是唯一的衔接点。 NetworkManager 通过事件与游戏的其他部分通信——场景、系统或实体不得直接导入 NetworkManager,反之亦然。
  4. 服务端拥有权威性,但保持宽容。 PartyKit 房间持有房间的规范状态。客户端发送意图;服务端验证并广播。在 realtime 模式下,验证较宽松(最后写入者胜出);在 turn-based 模式下,验证严格(拒绝不在当前回合的操作)。
  5. 客户端 API 与后端无关。 所有 partysocket 调用都通过 MultiplayerClient 进行。如果将来用户想使用 Colyseus 或 fly.io+ws,只需修改 MultiplayerClient.js——游戏代码无需改动。
  6. 默认房间为 '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:

  1. src/core/Constants.js → MULTIPLAYER.SERVER_URL
  2. <game-path>/.env → VITE_MULTIPLAYER_SERVER_URL=https://...
  3. <game-path>/.env.example → VITE_MULTIPLAYER_SERVER_URL=https://your-project.your-username.partykit.dev

如果 .gitignore 中尚未包含 .env,请将其添加进去。

离线优先身份验证和故障排除的完整操作步骤请参阅 deploy.md。

步骤 7:重新部署客户端

复用现有的主机检测逻辑(与 monetize-game 步骤 5 相同):

  1. 如果 .herenow/state.json 存在 → 通过 ~/.agents/skills/here-now/scripts/publish.sh dist/ 重新部署。
  2. 否则,如果已配置 gh,且仓库有 GitHub Pages 工作流 → npx gh-pages -d dist。
  3. 否则,如果已配置 vercel → vercel --prod。
  4. 否则,询问用户希望如何重新部署。

务必先运行 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.

输出

告知用户:

  1. 新增内容——multiplayer-server/ 中的服务器、src/multiplayer/ + src/systems/NetworkManager.js 中的客户端,以及对四个核心文件的增量修改。
  2. 服务器 URL——https://<project>.<user>.partykit.dev。已配置到 Constants 和 .env 中。
  3. 如何在本地测试——运行 cd multiplayer-server && npx partykit dev,然后运行 npm run dev,并打开两个标签页。
  4. 替换后端的唯一接口——指向 src/multiplayer/MultiplayerClient.js。未来迁移到 Colyseus 或 fly.io 只需修改这个文件。
  5. 费用——使用 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 的转付定价(免费套餐十分慷慨)。

评论

支持 Markdown