mcpField
C
colyseus

colyseus

github.com/colyseus開発3243 回インストール10 週間前に更新
リポジトリ

colyseus について

colyseusは、Node.jsの権威型マルチプレイヤーフレームワークColyseusの利用をコーディングエージェントに案内するスキルです。最初に導入済みバージョンを確認し、特に0.17と0.18のAPI変更に対応した説明を提供します。

主な機能

  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.xhttps://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.16 のプロジェクトに 0.18 の API を記述すると、インポート時にコードが失敗します。0.18 のプロジェクトに 0.16 の API を記述すると、実行時にコードが失敗するか、気づかないうちに同期がずれます。

ステップ 2:メモリが誤る内容

左の列は、古い情報に基づいて思い出した場合に出力される内容です。 自分の出力を確認し、既存のプロジェクトでも見つけられるようにしてください。

書いてはいけない内容代わりに書く内容理由
import { Client } from "colyseus.js"import { Client } from "@colyseus/sdk"JS クライアントパッケージの名前が変更されました。
class MyRoom extends Room<MyState>class MyRoom extends Room with state = new MyState()ジェネリック型は現在、Room<{ state, metadata, client }> のようなオプションオブジェクトです。状態型だけを指定しても、型チェックに通らなくなりました。
Client<UserData, AuthData>Client<{ userData: UserData, auth: AuthData }>クライアントの型も同様に変更されました。
onCreate() { this.onMessage("x", fn) }messages = { x: fn } as a class property宣言的なマップが主要な API です。this.onMessage() も引き続き動作し、バインド解除関数を返します。
@type("string") name: string onlyschema({ 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)2 番目の引数は切断コードです。4000 は truthy なので、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" }) expecting a mergethis.setMetadata({ ...this.metadata, status: "x" })0.18 ではメタデータは浅いマージではなく、全体が置き換えられます。setMatchmaking({ metadata }) も同様です。
@filter() / @filterChildren()@view() on the field, client.view = new StateView() in onJoinクライアントごとのフィルタリングは、ビューを通じてオプトインします。
this.setSimulationInterval(fn)this.setTimestep(fn)名前が変更されました。旧名も引き続き動作するため問題はありませんが、新しいコードでは setTimestep() を使用してください。予測ゲームには setFixedTimestep(step, tickRate) を使用します。
64 個以上のフィールドを持つ Schema超過分をネストされた子 Schema に移動する0.18 では Schema のフィールド数は 63 個までで、上限を超えると定義時に例外が発生します。継承したフィールドも数に含まれます。
@colyseus/fossil-delta-serializer@colyseus/schema0.18 で削除されました。

本番環境で問題になるものが、さらに 2 つあります。

  • Playground は、use オプションでガードミドルウェアを渡さない限り、 本番環境のマウント(NODE_ENV=production)では 404 を返します。
  • @colyseus/auth のパスワードハッシュ方式は 0.18 で変更されました。0.18 より前のハッシュは 新しいハッシュ方式では検証できないため、既存のメールアドレス/パスワードユーザーは パスワードをリセットする必要があります。従来の scrypt ハッシュ方式とグローバルな AUTH_SALT は 非推奨です。
名前だけでなく、形も変わったもの

0.17 より前のコードとは構造が異なるため、名前の対応表だけでは修正方法を示せない箇所が3つあります。左側に注目してください。

サーバー定義。 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"。サーバーオプション、トランスポート、プレゼンス、ドライバーについては、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 を追加すると、各ページを raw 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
サーバー設定、トランスポート、プレゼンス、ドライバー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

必要なページが同梱されておらず、一覧にもない場合は、URL を推測するのではなく、 https://docs.colyseus.io/llms.txt を取得して、そこから選んでください。

既存プロジェクトでの症状

0.18 で実行したときの、0.17 より前のコードの動作です。特に、エラーが表示されない症状を確認してください。

症状原因修正方法
プレイヤーが再接続できず、エラーも出ないonLeave の第 2 引数は切断コードなので、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 が 404 になる0.18 ではアクセスが制限されていますuse 経由でガード用ミドルウェアを渡します

作業ルール

  • サーバーが正です。ルールの適用をクライアントコードだけに任せてはいけません。ルーム内で検証してください。
  • state を変更できるのはサーバーだけです。クライアントはメッセージまたは入力を送信し、コールバックに応答します。
  • 命令型の onMessage() 呼び出しより、宣言型の messages マップを優先し、ペイロードを検証してください。メッセージ本文で渡された sessionId は決して信用せず、ハンドラーが受け取る client 引数を使用してください。
  • スキーマは同期状態専用です。メッセージとリクエストのペイロードはプレーンオブジェクトです。
  • 同期状態は小さく保ってください。パッチのたびにブロードキャストされるため、クライアントが必要としないものはスキーマの外に置いてください。
  • 状態構造を変更した後は、親クラスも含め、影響を受けるすべてのクラスでフィールド数が上限の 63 を超えていないか確認してください。
  • グローバル関数ではなく、this.clock.setTimeout() と this.clock.setInterval() を使用してください。ルームのタイマーは破棄時にクリアされます。
  • プレイヤーを削除するのは onDrop ではなく onLeave です。切断されたクライアントは戻ってくる可能性があります。

コメント