mcpField
C
colyseus

colyseus

github.com/colyseus开发3243 次安装10 周前更新
代码仓库

关于 colyseus

colyseus 是一项技能,指导编码 Agent 使用 Node.js 权威式多人游戏框架 Colyseus。它强调先确认已安装版本,并针对 API 变更提供版本对应的指导,尤其涉及 0.17 和 0.18。

主要功能

  1. 01按版本提供 API 指导
  2. 02Colyseus 服务端与房间模式
  3. 03Schema 与同步状态指导
  4. 04客户端回调与重连模式
  5. 05迁移注意事项与参考索引

适用场景

  1. 01修改前确认项目的 Colyseus 版本
  2. 02实现房间与同步状态
  3. 03将代码更新至 Colyseus 0.18
  4. 04处理客户端状态回调与重连

安装

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

$npx skills add https://github.com/colyseus/skill --skill colyseus

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

包含的文件

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

.gitignore
sources.json
README.md
package.json
LICENSE
.github/workflows/
sync.yml
references/
netcode.md
schema.md
room.md
client-sdk.md
tools/
sync.mjs

详细介绍

Colyseus

Colyseus 是一个适用于 Node.js 的权威多人游戏框架。服务器拥有状态;客户端发送输入并接收二进制增量补丁。

API 在 0.17 版本发生了重大变更,在 0.18 版本又有一次,而大多数模型训练数据都早于这两个版本。请假设你对 Colyseus 的了解已经过时,并在编写前查证。

第 1 步:确认版本。不要跳过。

读取项目的 package.json(或 pnpm-lock.yaml / yarn.lock),检查 colyseus 和 @colyseus/schema。

已安装版本操作
colyseus 0.18.x,@colyseus/schema 5.x直接使用本指南中的说明。
0.17.x阅读 https://docs.colyseus.io/migrating/0.18.md,了解哪些内容暂不适用,然后遵循 0.17 的惯用写法。
0.16.x 或更早版本遵循该版本自己的文档。不要应用以下内容。对应版本的网站:https://0-16-x.docs.colyseus.io/。
尚未安装使用 npm create colyseus-app@latest 初始化项目,该命令会创建 0.18 版本。

如果版本不明确,请先询问,不要猜测。把 0.18 API 写进 0.16 项目会导致代码在导入时失败;把 0.16 API 写进 0.18 项目则会导致代码在运行时失败或静默失去同步。

步骤 2:记忆会出什么错

左栏列出了过时记忆会导致的错误写法。检查你自己的输出是否有这些问题,并留意现有项目中的这些写法。

不要这样写改为这样写原因
import { Client } from "colyseus.js"import { Client } from "@colyseus/sdk"JS 客户端包已重命名。
class MyRoom extends Room<MyState>class MyRoom extends Room,并添加 state = new MyState()泛型现在是一个选项对象:Room<{ state, metadata, client }>。单独的状态类型现在无法通过类型检查。
Client<UserData, AuthData>Client<{ userData: UserData, auth: AuthData }>客户端类型也做了同样的更改。
onCreate() { this.onMessage("x", fn) }将 messages = { x: fn } 作为类属性声明式映射是主要 API。this.onMessage() 仍可使用,并会返回一个解绑函数。
仅使用 @type("string") name: stringschema({ name: t.string() }, "MyState")构建器是主要写法,且无需编译器标志。装饰器仍受支持。
schema({ name: "string" })schema({ name: t.string() })在 schema 5 中,原始字符串字段类型会在定义时抛出异常。它们仍可用作集合子类型:t.array("string")。
client.idclient.sessionIdclient.id 已在 0.18 中移除。
async onLeave(client, consented: boolean)onLeave(client, code: CloseCode)第二个参数是关闭代码。4000 的值为真,因此 if (!consented) 永远不会执行,也就会悄无声息地禁止重连。请参阅下方的重连写法。
Protocol.WS_CLOSE_CONSENTED来自 "colyseus" 的 CloseCode.CONSENTEDProtocol.WS_* 已移至 CloseCode。4000–4010 为保留值;自定义代码使用 4011–4999。
this.setSeatReservationTime(15)this.seatReservationTimeout = 15该方法已改为属性。
使用 this.setMetadata({ status: "x" }) 并期待合并this.setMetadata({ ...this.metadata, status: "x" })0.18 会整体替换元数据,而不再进行浅合并。setMatchmaking({ metadata }) 也一样。
@filter() / @filterChildren()在字段上使用 @view(),并在 onJoin 中设置 client.view = new StateView()每个客户端的筛选需要通过视图显式启用。
this.setSimulationInterval(fn)this.setTimestep(fn)已重命名。旧名称仍会转发调用,因此并未失效,但新代码应使用 setTimestep()。对于需要预测的游戏,请使用 setFixedTimestep(step, tickRate)。
包含 64 个或更多字段的 Schema将超出的字段移至嵌套的子 Schema0.18 将 Schema 的字段数上限设为 63,并会在定义时抛出异常。继承的字段也计入其中。
@colyseus/fossil-delta-serializer@colyseus/schema该包已在 0.18 中移除。

另有两个在生产环境中容易踩坑的问题:

  • Playground 在生产环境挂载时(NODE_ENV=production)会返回 404,除非你通过其 use 选项传入保护中间件。
  • @colyseus/auth 的密码哈希方式已在 0.18 中更改。新哈希器无法验证 0.18 之前的哈希,因此现有的电子邮件/密码用户必须重置密码。旧版 scrypt 哈希器及其全局变量 AUTH_SALT 已弃用。
改变的不只是名称,还有结构

以下是 pre-0.17 代码中结构有所不同的三个地方,因此无法通过重命名对照表展示修复方式。左侧是需要识别的旧写法。

服务端定义。 new Server() + gameServer.define(),以及 @colyseus/tools 的 config({ initializeGameServer }) 包装器,都改成了 defineServer():

-import config from "@colyseus/tools";
-export default config({
-    initializeGameServer: (gameServer) => {
-        gameServer.define("my_room", MyRoom);
-    },
-    initializeExpress: (app) => {},
-});
+import { defineServer, defineRoom } from "colyseus";
+export default defineServer({
+    rooms: { my_room: defineRoom(MyRoom) },
+    express: (app) => {},
+});

状态回调。 处理器不再是围绕状态对象的可调用包装器,而是一个对象,其方法接收集合名称或实例:

-import { Client, getStateCallbacks } from "colyseus.js";
-const $ = getStateCallbacks(room);
-$(room.state).players.onAdd((player, sessionId) => {
-    $(player).listen("x", (value, previous) => { /* ... */ });
+import { Client, Callbacks } from "@colyseus/sdk";
+const callbacks = Callbacks.get(room);
+callbacks.onAdd("players", (player, sessionId) => {
+    callbacks.listen(player, "x", (value, previous) => { /* ... */ });
 });

callbacks.onRemove("players", fn)、callbacks.onChange(player, fn)、callbacks.bindTo(player, sprite),以及针对根属性的 callbacks.listen("phase", fn) 都遵循相同形式。每次注册都会返回一个解绑函数。对于已存在的项目,onAdd 会立即触发。

重新连接。 重新连接从 onLeave 中移出,成为独立的钩子;客户端也不再需要手动重新连接:

-async onLeave(client, consented: boolean) {
-    try {
-        if (consented) throw new Error("consented leave");
-        await this.allowReconnection(client, 20);
-    } catch (e) {
-        this.state.players.delete(client.sessionId);
-    }
-}
+onDrop(client: Client, code: number) {
+    this.allowReconnection(client, 30); // no await; the outcome routes to the hooks below
+}
+onReconnect(client: Client) { /* mark the player connected again */ }
+onLeave(client: Client, code: number) {
+    this.state.players.delete(client.sessionId); // gone for good
+}
-room.onLeave(async () => {
-    room = await client.reconnect(room.reconnectionToken); // new instance: re-attach everything
-});
+room.onDrop(() => showReconnectingUI());      // the SDK is already retrying
+room.onReconnect(() => hideReconnectingUI()); // same instance, callbacks intact
+room.onLeave((code) => {
+    if (code === CloseCode.FAILED_TO_RECONNECT) offerRejoin();
+});

页面重新加载时仍可使用 client.reconnect(token)。它会返回一个新的 Room,因此在这种情况下需要重新绑定回调。

生命周期如何运行

房间:onCreate → … → onDispose。每个客户端: onAuth → onJoin → (onDrop → onReconnect)* → onLeave。

  1. 任何未经同意的连接关闭都会在双方触发 onDrop。服务端调用 allowReconnection(client, seconds);SDK 会采用退避策略在同一个 room 实例上重试。
  2. 成功时,双方都会触发 onReconnect。服务端会发送完整快照,并在原位进行协调:现有回调会继续绑定,onAdd 和 onRemove 会针对离线期间发生的变化触发,排队等待的 room.send() 调用会被刷新。
  3. 超时或拒绝会在双方触发 onLeave;客户端收到 FAILED_TO_RECONNECT(4003)。onLeave 表示永久离开:应在此处移除玩家,而不要在 onDrop 中移除。

room.leave()(CONSENTED,4000)会跳过 onDrop。没有 onDrop 的房间会在 onLeave 中收到每次断开连接,并且在 code !== CloseCode.CONSENTED 时可在那里调用 allowReconnection()。

步骤 3:规范的 0.18 代码结构

每个代码块末尾都附有完整说明该内容的参考章节。references/<file> § "Heading" 形式指向捆绑参考文档中的某个标题;可使用 grep -n 跳转到该标题,而不必阅读整个文件。

服务端

src/index.ts:

import { listen } from "@colyseus/tools";
import app from "./app.config.js";

listen(app);

src/app.config.ts:

import { defineServer, defineRoom } from "colyseus";
import { MyRoom } from "./rooms/MyRoom.js";

export const server = defineServer({
    rooms: {
        my_room: defineRoom(MyRoom),
    },
});

export default server;

命名导出可供 TypeScript 客户端导入,用于端到端类型检查。 参考:references/room.md § "Defining a Room";服务端选项、 传输、presence 和 driver:https://docs.colyseus.io/server.md。

状态

src/rooms/schema/MyRoomState.ts:

import { schema, t, type SchemaType } from "@colyseus/schema";

export const Player = schema({
    x: t.number(),
    y: t.number(),
    connected: t.boolean().default(true),
}, "Player");
export type Player = SchemaType<typeof Player>;

export const MyRoomState = schema({
    players: t.map(Player),
}, "MyRoomState");
export type MyRoomState = SchemaType<typeof MyRoomState>;

schema() 返回一个真正的类,因此 new MyRoomState() 和 instanceof 都可用;export type 别名让同一个名称也能用作类型。每种结构都要传入名称:序列化器、调试输出和代码生成都通过这个名称标识类。集合在没有默认值时会以空集合开始。 参考:references/schema.md § "Defining a Schema structure";字段 类型和带宽:references/schema.md § "Data Types";每客户端 过滤:references/schema.md § "View tags"。

房间

src/rooms/MyRoom.ts:

import { Room, Client, CloseCode, validate } from "colyseus";
import { z } from "zod";
import { MyRoomState, Player } from "./schema/MyRoomState.js";

export class MyRoom extends Room {
    maxClients = 4;
    state = new MyRoomState();

    messages = {
        // validate() rejects a malformed payload before it reaches the handler
        move: validate(z.object({ x: z.number(), y: z.number() }), (client: Client, { x, y }) => {
            const player = this.state.players.get(client.sessionId);
            player.x = x;
            player.y = y;
        }),
        // return a value to answer a client's room.request()
        "get-profile": async (client: Client, { userId }: { userId: number }) => {
            return await db.profiles.findById(userId);
        },
    };

    onCreate(options: any) {}

    onJoin(client: Client, options: any) {
        this.state.players.set(client.sessionId, new Player());
    }

    onDrop(client: Client, code: CloseCode) {
        this.allowReconnection(client, 30);
        this.state.players.get(client.sessionId).connected = false;
    }

    onReconnect(client: Client) {
        this.state.players.get(client.sessionId).connected = true;
    }

    onLeave(client: Client, code: CloseCode) {
        this.state.players.delete(client.sessionId);
    }

    onDispose() {}
}

参考:references/room.md § "Message Handling";重新连接: references/room.md § "Complete Server Example";计时器: references/room.md § "Clock"。

客户端
import { Client, Callbacks, CloseCode } from "@colyseus/sdk";
import type { server } from "../../server/src/app.config.js";

// typeof server types room names, options, state and messages end to end
const client = new Client<typeof server>("http://localhost:2567");
const room = await client.joinOrCreate("my_room");

const callbacks = Callbacks.get(room);
callbacks.onAdd("players", (player, sessionId) => {
    callbacks.listen(player, "x", (x) => { /* move the sprite */ });
});
callbacks.onRemove("players", (player, sessionId) => { /* destroy the sprite */ });

room.send("move", { x: 1, y: 2 });
const profile = await room.request("get-profile", { userId: 42 });

room.onDrop(() => { /* show reconnecting UI */ });
room.onReconnect(() => { /* hide it */ });
room.onLeave((code) => {
    if (code === CloseCode.FAILED_TO_RECONNECT) { /* offer a rejoin */ }
});

参考:references/client-sdk.md § "State Sync Callbacks (Recommended)"; references/client-sdk.md § "Request/Response"; references/client-sdk.md § "Automatic Reconnection";页面重新加载的情况: references/client-sdk.md § "Manual Reconnection"。

第 4 步:查看位置

在该领域编写非简单代码之前,先阅读随附的参考文档。这些文档是从网站生成的完整文档文本,每份有 1,200–2,200 行;上方的章节指引会带你找到正确部分。

任务阅读
定义或更改同步状态、集合、筛选references/schema.md
房间生命周期、消息、请求/响应、计时器、重新连接references/room.md
在客户端连接、加入、发送、处理状态回调、重新连接references/client-sdk.md
客户端预测、协调、延迟补偿、固定时间步长references/netcode.md

其他所有内容都在网站上,每个页面都可以通过在 URL 后附加 .md 来获取原始 Markdown:

主题URL
匹配、大厅、队列、可见性https://docs.colyseus.io/matchmaker.md
HTTP 路由:createEndpoint、createRouter、中间件、CORShttps://docs.colyseus.io/server/http-routes.md
身份验证https://docs.colyseus.io/auth.md
数据库和持久化https://docs.colyseus.io/database.md
服务端配置、传输、presence、驱动程序https://docs.colyseus.io/server.md
横向扩展https://docs.colyseus.io/scalability.md
部署https://docs.colyseus.io/deployment.md
Colyseus Cloudhttps://docs.colyseus.io/cloud.md
使用 @colyseus/testing 进行单元测试https://docs.colyseus.io/tools/unit-testing.md
Playground、监控、负载测试https://docs.colyseus.io/tools.md
命令模式(@colyseus/command)https://docs.colyseus.io/recipes/command-pattern.md
从早期版本升级https://docs.colyseus.io/migrating/0.18.md
所有页面的完整索引https://docs.colyseus.io/llms.txt

如果任务需要的页面既未随附也未列出,请获取 https://docs.colyseus.io/llms.txt 并从中选择,而不要猜测 URL。

现有项目中的症状

查看在 0.18 中运行时,0.17 之前的代码会是什么表现。要留意那些没有明显提示的症状。

你看到的情况原因修复方法
玩家始终无法重新连接;没有错误onLeave 的第二个参数是关闭代码,因此 if (!consented) 永远不会执行使用 onDrop(),或比较 code !== CloseCode.CONSENTED
调用 setMetadata({ x }) 后,元数据字段消失0.18 会替换而非合并展开 this.metadata
client.id 的值为 undefined已移除使用 client.sessionId
Cannot find module "colyseus.js",或未导出 getStateCallbacks包已重命名,处理程序 API 已更改使用 @colyseus/sdk、Callbacks.get(room)
定义架构时抛出错误,提及 "string" 或其他类型名称字段类型使用了原始字符串使用 t.string() 等方法
定义架构时因字段数量抛出错误字段上限为 63 个,包含父级字段嵌套一个子架构
Room<MyState> 不再通过类型检查泛型是一个选项对象移除它并赋值 state = new MyState()
生产环境中 Playground 返回 4040.18 中已限制访问通过 use 传入一个保护中间件

工作规则

  • 服务端具有权威性。客户端代码绝不能成为执行规则的唯一手段;应在房间中进行验证。
  • 只有服务端可以修改 state。客户端发送消息或输入,并响应回调。
  • 优先使用声明式的 messages 映射,而不是命令式的 onMessage() 调用,并验证负载。绝不信任消息正文中提供的 sessionId;应使用处理程序收到的 client 参数。
  • Schema 仅用于同步状态。消息和请求负载都是普通对象。
  • 保持同步状态精简。每次状态补丁都会广播,因此客户端不需要的内容应放在 Schema 之外。
  • 更改状态结构后,检查所有受影响类(包括父类)是否达到 63 个字段的上限。
  • 使用 this.clock.setTimeout() 和 this.clock.setInterval(),而非全局函数;房间计时器会在释放时清除。
  • 在 onLeave 中移除玩家,绝不要在 onDrop 中移除;掉线的客户端可能会重新连接。

评论

支持 Markdown