add-multiplayer

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Add 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:
  1. A PartyKit server (one Durable Object per room) deployed to Cloudflare's edge.
  2. A client
    NetworkManager
    wired through EventBus that mirrors the existing
    playfun.js
    external-service pattern.
  3. Additive edits to
    EventBus
    ,
    GameState
    ,
    Constants
    , and
    render_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
.
为现有单人浏览器游戏添加实时或回合制多人功能。本技能可自动生成以下内容:
  1. PartyKit服务器(每个房间对应一个Durable Object),部署在Cloudflare边缘节点。
  2. 客户端
    NetworkManager
    ,通过EventBus连接,遵循现有
    playfun.js
    外部服务模式。
  3. EventBus
    GameState
    Constants
    render_game_to_text()
    进行增量式修改——当服务器不可达时,单人游戏玩法必须保持完全一致。
默认状态为“单人模式正常运行”。如果WebSocket连接失败,NetworkManager会捕获错误,游戏将像以前一样在本地运行。连接成功后,远程玩家会通过
network:player-joined
事件加入,并通过
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.ts
    and
    turn-based.ts
    ), state shape, broadcast helpers, rate limiting.
  • client-integration.md
    MultiplayerClient
    ,
    NetworkManager
    ,
    RemotePlayerRegistry
    source, EventBus/GameState/Constants append patterns,
    render_game_to_text
    extension.
  • deploy.md
    npx partykit dev
    and
    npx partykit deploy
    walkthrough, capturing the deployed URL,
    .env
    handling, and client redeploy.
  • architecture.md
    — 事件分类、GameState Schema、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
    配置、客户端重新部署步骤。

Core Principles

核心原则

These are rules, not guidelines:
  1. 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:disconnected
    instead of throwing.
  2. Additive edits only. Append to
    EventBus.js
    ,
    GameState.js
    ,
    Constants.js
    ,
    main.js
    , and
    render_game_to_text()
    under a
    // === Multiplayer ===
    banner. Never rename, remove, or change existing fields.
  3. 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).
  4. Server is authoritative, but tolerant. The PartyKit room owns the canonical room state. Clients send intents; the server validates and broadcasts. In
    realtime
    mode validation is light (last-write-wins). In
    turn-based
    mode validation is strict (rejects out-of-turn moves).
  5. Backend-agnostic client API. All
    partysocket
    calls go through
    MultiplayerClient
    . If a future user wants Colyseus or fly.io+ws, only
    MultiplayerClient.js
    changes — game code does not.
  6. Default room is
    'lobby'
    .
    No matchmaking UI in v1. Users override by emitting
    multiplayer:join-room
    with a custom room id.
以下为必须遵守的规则,而非建议:
  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版本不包含匹配UI。用户可通过触发
    multiplayer:join-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.js
    with
    window.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).
  • 已使用本插件生成的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账户(用于
    npx partykit deploy
    ,首次部署时CLI会引导用户登录;免费套餐足以满足原型开发需求)。

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.
用户希望在
$ARGUMENTS
指定的游戏中添加多人功能(若未指定路径则使用当前目录)。可选参数
--mode=realtime
(默认)或
--mode=turn-based
用于选择服务器模板。

Step 0: Locate and read the game

步骤0:定位并分析游戏

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 and
    reset()
    semantics.
  • src/core/Constants.js
    — config block conventions.
  • progress.md
    if 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 at
TICK_RATE_HZ
and renders remote players from server broadcasts. Single-player will continue to work when the server is offline.
解析
$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>
的状态,并根据服务器广播渲染远程玩家。当服务器离线时,单人模式仍可正常运行。

Step 1: Choose sync mode

步骤1:选择同步模式

Pick the server template:
ModeWhen to useWire model
realtime
(default)
Action games, runners, dodgers, platformers, anything with continuous movementLocal
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 movesEventBus 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.
选择服务器模板:
模式使用场景通信模型
realtime
(默认)
动作游戏、跑酷游戏、躲避类游戏、平台游戏等包含连续移动的游戏本地以
TICK_RATE_HZ
频率通过
setInterval
广播本地实体的
{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
模式。说明选择的模式并继续操作。

Step 2: Scaffold the server

步骤2:生成服务器代码

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 with
    name
    (use the game's directory name),
    main: "src/server.ts"
    ,
    compatibilityDate
    .
  • multiplayer-server/package.json
    partykit
    dep,
    dev
    /
    deploy
    scripts.
  • multiplayer-server/tsconfig.json
    — minimal TypeScript config that PartyKit accepts.
  • multiplayer-server/src/server.ts
    — paste the appropriate template from
    partykit-server.md
    (
    realtime
    or
    turn-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).
在游戏项目内创建同级目录
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
依赖)。

Step 3: Scaffold the client

步骤3:生成客户端代码

Create three new files. See
client-integration.md
for the full source.
  • src/multiplayer/MultiplayerClient.js
    — backend-agnostic interface around
    partysocket
    (
    connect
    ,
    send
    ,
    onMessage
    ,
    disconnect
    ,
    isConnected
    ).
  • src/multiplayer/RemotePlayerRegistry.js
    Map<playerId, RemotePlayer>
    with
    upsert
    ,
    remove
    ,
    prune(staleMs)
    ,
    list()
    .
  • src/systems/NetworkManager.js
    — wires MultiplayerClient ↔ EventBus, owns the broadcast tick (in
    realtime
    mode), handles reconnect with exponential backoff, emits
    network:*
    events.
Add
partysocket
to the game's
package.json
deps:
bash
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()
    方法。
  • src/systems/NetworkManager.js
    — 连接MultiplayerClient与EventBus,管理广播定时器(
    realtime
    模式下),处理指数退避重连,触发
    network:*
    事件。
partysocket
添加到游戏的
package.json
依赖中:
bash
cd <game-path> && npm install partysocket

Step 4: Append to existing core files

步骤4:修改现有核心文件

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:
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.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.
src/main.js
— instantiate NetworkManager after EventBus + GameState, before the engine starts. Expose
window.__NETWORK_MANAGER__
for tests. Extend
window.render_game_to_text()
to additively include
multiplayer: {...}
and
remotePlayers: [...]
.
仅允许增量式修改。完整Schema请参考
architecture.md
,具体追加代码块请参考
client-integration.md
src/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.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: [...]
内容。

Step 5: Wire the local game into the network tick

步骤5:将本地游戏接入网络定时器

Inspect existing events. The wiring depends on mode:
realtime
: NetworkManager owns a
setInterval
at
TICK_RATE_HZ
. Each tick it reads the local entity from
GameState
and calls
client.send({type: 'state', payload: {...}})
. No EventBus subscription needed — it just samples GameState. Add a single
network:state-received
listener in the relevant scene/system that calls
RemotePlayerRegistry.upsert()
and triggers a re-render.
turn-based
: NetworkManager subscribes to the game's existing move events (e.g.,
card:played
,
move:submitted
) and forwards them. The scene/system listens for
network:state-received
and applies the validated remote move. Local optimistic UI is allowed, but the server is the source of truth.
In Phaser games, remote-player rendering happens in the active GameScene — instantiate sprites on
network:player-joined
, update positions on
network:state-received
, destroy on
network:player-left
. In Three.js games, the active orchestrator (
Game.js
) creates and updates remote-player meshes.
See
client-integration.md
for example scene patches for both engines.
检查现有事件。接入方式取决于所选模式:
realtime
模式
:NetworkManager以
TICK_RATE_HZ
频率运行
setInterval
。每次定时器触发时,从
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

Step 6: Deploy the server

步骤6:部署服务器

Run the dev server first to confirm everything works locally:
bash
cd <game-path>/multiplayer-server && npx partykit dev
This starts a local CF Worker emulator on
http://127.0.0.1:1999
. In another terminal, set
VITE_MULTIPLAYER_SERVER_URL=http://127.0.0.1:1999
in
<game-path>/.env
and run the client (
cd <game-path> && npm run dev
).
For first-time deployment, the user must authenticate with PartyKit. Always pass
--provider github
— the default
clerk
flow is broken in 2026 (the dashboard.partykit.io callback was retired after Cloudflare absorbed PartyKit, and login hangs forever):
bash
cd <game-path>/multiplayer-server && npx partykit login --provider github
This uses GitHub's device-code OAuth flow. The CLI prints a code; the user visits
https://github.com/login/device
, pastes it, and authorizes. Credentials persist in
~/.partykit/config.json
. See
deploy.md
for the full walkthrough and troubleshooting.
After login, deploy:
bash
cd <game-path>/multiplayer-server && npx partykit deploy
Capture the deployed URL from the output (format:
https://<project>.<cloudflare-username>.partykit.dev
). The TLS cert may take 30-60 seconds to provision after the deploy reports success.
Update three places with the deployed 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
Add
.env
to
.gitignore
if not already present.
See
deploy.md
for the full walkthrough including offline-first authentication and troubleshooting.
先启动开发服务器,确认本地运行正常:
bash
cd <game-path>/multiplayer-server && npx partykit dev
这会在
http://127.0.0.1:1999
启动本地CF Worker模拟器。在另一个终端中,在
<game-path>/.env
中设置
VITE_MULTIPLAYER_SERVER_URL=http://127.0.0.1:1999
,然后启动客户端(
cd <game-path> && npm run dev
)。
首次部署时,用户必须通过PartyKit认证。请始终使用
--provider github
参数
——默认的
clerk
流程在2026年已失效(Cloudflare收购PartyKit后,dashboard.partykit.io的回调已停用,登录会无限挂起):
bash
cd <game-path>/multiplayer-server && npx partykit login --provider github
这会使用GitHub的设备码OAuth流程。CLI会打印一个代码;用户访问
https://github.com/login/device
,粘贴代码并授权。凭证会保存在
~/.partykit/config.json
中。完整操作指南和故障排查请参考
deploy.md
登录完成后,进行部署:
bash
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
.env
未在
.gitignore
中,添加进去。
完整操作指南包括离线优先认证和故障排查,请参考
deploy.md

Step 7: Redeploy the client

步骤7:重新部署客户端

Reuse the existing host detection logic (same as
monetize-game
Step 5):
  1. If
    .herenow/state.json
    exists → redeploy via
    ~/.agents/skills/here-now/scripts/publish.sh dist/
    .
  2. Else if
    gh
    is configured and the repo has a GitHub Pages workflow →
    npx gh-pages -d dist
    .
  3. Else if
    vercel
    is configured →
    vercel --prod
    .
  4. Else ask the user how they want to redeploy.
Always run
npm run build
first.
复用现有的主机检测逻辑(与
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

Step 8: Verify

步骤8:验证

Build cleanly:
bash
cd <game-path> && npm run build
cd multiplayer-server && npm run build  # if a build script exists
Single-player fallback (critical): with the partykit dev server stopped, reload
http://localhost:3000
. The game must boot, play, and reset normally. Confirm
network:disconnected
fired and no uncaught errors in the console. If the game depends on the server to start, you violated Principle 1 — revise.
Two-tab smoke test: start
npx partykit dev
in one terminal and
npm run dev
in another. Open two browser tabs at
http://localhost:3000
. Confirm:
  • Both tabs fire
    network:connected
    (check console).
  • Each tab's
    window.render_game_to_text()
    includes the other tab in
    remotePlayers
    .
  • Moving the local entity in tab A is reflected in tab B's remote-player rendering within
    1000 / TICK_RATE_HZ * 2
    ms.
Reconnect: kill the partykit dev server, wait, restart it. The client should reconnect within
RECONNECT_MAX_BACKOFF_MS
and re-emit
network:connected
.
Regression: existing
tests/e2e/*.spec.js
must still pass. Single-player invariants (boot, score, game-over, reset) must hold whether the server is up or down.
构建是否正常:
bash
cd <game-path> && npm run build
cd multiplayer-server && npm run build  # 若存在build脚本
单人模式回退(关键验证): 停止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
必须全部通过。无论服务器是否在线,单人模式的不变量(启动、得分、游戏结束、重置)必须保持一致。

Step 9: Update
progress.md

步骤9:更新
progress.md

Append a
## Multiplayer
section:
markdown
undefined
追加
## Multiplayer
章节:
markdown
undefined

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.
undefined
  • 后端: PartyKit (Cloudflare Durable Objects)
  • 服务器URL: https://<project>.<user>.partykit.dev
  • 模式: realtime | turn-based
  • 每个房间最大玩家数: 4
  • Tick频率: 20 Hz(实时模式)
  • 默认房间: lobby
  • 已知限制(v1): 无匹配UI、无 spectator 模式、无持久化账户、仅服务器端速率限制。
undefined

Output

输出信息

Tell the user:
  1. What was added — server in
    multiplayer-server/
    , client in
    src/multiplayer/
    +
    src/systems/NetworkManager.js
    , additive edits to four core files.
  2. The server URL
    https://<project>.<user>.partykit.dev
    . Already wired into Constants and
    .env
    .
  3. How to test locally
    cd multiplayer-server && npx partykit dev
    then
    npm run dev
    , open two tabs.
  4. The single seam for backend swaps — point at
    src/multiplayer/MultiplayerClient.js
    . Future Colyseus or fly.io migration only changes that one file.
  5. 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'.
告知用户以下内容:
  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免费套餐即可使用(每日10万次请求,1GB DO存储)。说明用户拥有部署的Cloudflare项目;PartyKit部署到用户自己的CF账户,而非OpusGameLabs的账户。

Example Usage

示例用法

Default (realtime mode in current directory)

默认情况(当前目录下的实时模式)

/add-multiplayer
Result: detects engine, scaffolds
multiplayer-server/
with the realtime template, creates client networking files, deploys server, redeploys client, prints play URL and server URL.
/add-multiplayer
结果:检测引擎,使用实时模板生成
multiplayer-server/
,创建客户端网络文件,部署服务器,重新部署客户端,打印游戏URL和服务器URL。

Turn-based explicit

显式指定回合制模式

/add-multiplayer ./examples/card-game --mode=turn-based
Result: 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-run
Result: 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 login
重定向到
dashboard.partykit.io/patience
且无法完成登录

Cause: The default
clerk
provider was retired after Cloudflare absorbed PartyKit; the dashboard the OAuth callback expects is gone. Fix: Use
npx partykit login --provider github
instead — GitHub device-code flow, prints a code, you paste at
https://github.com/login/device
. Credentials persist in
~/.partykit/config.json
.
原因: Cloudflare收购PartyKit后,默认的
clerk
认证提供商已停用;OAuth回调依赖的控制台已不存在。 解决方法: 使用
npx partykit login --provider github
替代——GitHub设备码流程,打印一个代码,用户在
https://github.com/login/device
粘贴并授权。凭证保存在
~/.partykit/config.json
中。

Remote players don't appear even though the connection succeeded

连接成功但远程玩家未显示

Cause: Welcome-race — the WebSocket
welcome
arrived before the scene's
create()
registered its
NETWORK_PLAYER_JOINED
listener. The events fired into the void. Fix: After registering the listener, seed from
gameState.multiplayer.remotePlayers
directly. See
client-integration.md
→ "Welcome-race gotcha" for the idempotent pattern.
原因: 欢迎事件竞争——WebSocket的
welcome
消息在场景
create()
方法注册
NETWORK_PLAYER_JOINED
监听器之前到达,事件被丢弃。 解决方法: 注册监听器后,直接从
gameState.multiplayer.remotePlayers
加载初始数据。幂等模式请参考
client-integration.md
→ "欢迎事件竞争问题"。

Two 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
window.render_game_to_text().multiplayer.roomId
on both tabs — it should be the same (default
'lobby'
). If different, audit
NetworkManager.connect()
for stray query-string parsing. The server template's
room.broadcast(message, [sender.id])
excludes the sender, which is correct — each client renders only remote players, not itself.
原因: 它们加入了不同的房间(URL解析生成的随机房间ID),或者服务器默认广播逻辑排除了发送者。 解决方法: 检查两个标签的
window.render_game_to_text().multiplayer.roomId
是否相同(默认应为
'lobby'
)。若不同,检查
NetworkManager.connect()
中的URL查询字符串解析逻辑。服务器模板的
room.broadcast(message, [sender.id])
会排除发送者,这是正确的——每个客户端仅渲染远程玩家,而非自身。

Remote players appear stuck at last position when a peer closes the tab

当某个玩家关闭标签页时,远程玩家停留在最后位置

Cause:
onClose
did not fire (browser killed the tab without a clean close), or the client did not run
RemotePlayerRegistry.prune()
. Fix: The server's
onClose
handler is the canonical "player left" signal. Additionally, NetworkManager runs
RemotePlayerRegistry.prune(STALE_PLAYER_MS)
on every tick — verify this is wired. If a remote player has not sent state in
STALE_PLAYER_MS
, prune emits
network:player-left
even without an explicit close.
原因:
onClose
事件未触发(浏览器未正常关闭标签页),或者客户端未执行
RemotePlayerRegistry.prune()
解决方法: 服务器的
onClose
处理程序是“玩家离开”的权威信号。此外,NetworkManager在每次定时器触发时运行
RemotePlayerRegistry.prune(STALE_PLAYER_MS)
——请确认已接入该逻辑。如果远程玩家在
STALE_PLAYER_MS
时间内未发送状态,即使没有显式关闭事件,prune也会触发
network:player-left
事件。

Game lags or stutters when many remote players are present

存在多个远程玩家时游戏卡顿或掉帧

Cause: Either too-high
TICK_RATE_HZ
(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
TICK_RATE_HZ
to 20 (default) or 10 for slow games. Confirm scenes maintain a
Map<playerId, sprite>
and only update positions on
network:state-received
, never recreate.
原因:
TICK_RATE_HZ
过高(每秒广播和渲染60次),或者场景每帧重新创建远程玩家精灵而非复用现有精灵。 解决方法:
TICK_RATE_HZ
降低到20(默认)或10(适合慢节奏游戏)。确认场景维护
Map<playerId, sprite>
结构,仅在
network:state-received
事件触发时更新位置,绝不重新创建精灵。

Single-player tests fail after adding multiplayer

添加多人功能后单人测试失败

Cause: NetworkManager throws or blocks game boot when the server is unreachable. This violates Principle 1. Fix: Audit
MultiplayerClient.connect()
and
NetworkManager.init()
— both must catch all errors, log a warning, emit
network:disconnected
, and return. The constructor and
init()
must never throw out of
main.js
.
原因: 当服务器不可达时,NetworkManager抛出异常或阻止游戏启动。违反了原则1。 解决方法: 检查
MultiplayerClient.connect()
NetworkManager.init()
——两者必须捕获所有错误,记录警告,触发
network:disconnected
事件并返回。构造函数和
init()
方法绝不能在
main.js
中抛出异常。

render_game_to_text()
snapshot tests fail

render_game_to_text()
快照测试失败

Cause: Tests use exact
toEqual
on the output; you added new top-level fields. Fix: Regenerate baselines — additions are intentional and backward-compatible. The fields added are
multiplayer
(object) and
remotePlayers
(array, may be empty).
原因: 测试使用精确的
toEqual
断言输出;你添加了新的顶级字段。 解决方法: 重新生成基准快照——添加的内容是有意的且向后兼容。新增字段为
multiplayer
(对象)和
remotePlayers
(数组,可能为空)。

Cloudflare 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
https://
scheme. Fix: Confirm
Constants.MULTIPLAYER.SERVER_URL
is the full
https://...partykit.dev
URL.
partysocket
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).
原因: 混合内容(HTTP页面连接WSS服务器)或服务器URL未包含
https://
协议。 解决方法: 确认
Constants.MULTIPLAYER.SERVER_URL
是完整的
https://...partykit.dev
URL。
partysocket
会自动替换协议生成WSS URL。部署的游戏也必须通过HTTPS提供服务,才能成功建立WSS连接(here.now和GitHub Pages默认均提供HTTPS服务)。

Free 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
1.7M / day
for a single 24/7 player. For prototyping you'll never hit this; for production, lower
TICK_RATE_HZ
or upgrade to Workers Paid ($5/mo flat for 10M requests/day).
原因: 大量并发房间或频繁发送消息的客户端。 解决方法: Cloudflare Workers免费套餐允许每日10万次请求。单个玩家以20 Hz频率发送消息,每日可达170万次请求。原型开发绝不会触发该限制;生产环境可降低
TICK_RATE_HZ
或升级到Workers付费套餐(每月5美元,每日1000万次请求)。

Tips

提示

Run
/add-multiplayer
once per game. If you later change modes, edit
multiplayer-server/src/server.ts
directly — both templates are checked in and the switch is small.
The default
'lobby'
room is suitable for a single open room. To support private rooms, emit
multiplayer:join-room
with a room id from a URL query string or invite code. NetworkManager listens for that event and reconnects to the new room.
For graduation to a more featureful backend (matchmaking, schema sync, server-authoritative physics), the only file that needs to change is
src/multiplayer/MultiplayerClient.js
. Replace the
partysocket
calls with Colyseus's
colyseus.js
client; keep the same public API (
connect
,
send
,
onMessage
,
disconnect
,
isConnected
).
The 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
即可——两个模板均已提交,切换改动很小。
默认的
'lobby'
房间适合单个公开房间。若要支持私人房间,可从URL查询字符串或邀请码获取房间ID,触发
multiplayer:join-room
事件。NetworkManager监听该事件并重新连接到新房间。
若要升级到功能更丰富的后端(匹配、Schema同步、服务器权威物理),只需修改
src/multiplayer/MultiplayerClient.js
文件。将
partysocket
调用替换为Colyseus的
colyseus.js
客户端;保持相同的公共API(
connect
send
onMessage
disconnect
isConnected
)。
服务器运行在你的Cloudflare账户中,而非OpusGameLabs的账户。成本和配额由你承担。PartyKit本身是开源免费的;你只需支付Cloudflare的实际使用费用(免费套餐非常慷慨)。