colyseus について
colyseusは、Node.jsの権威型マルチプレイヤーフレームワークColyseusの利用をコーディングエージェントに案内するスキルです。最初に導入済みバージョンを確認し、特に0.17と0.18のAPI変更に対応した説明を提供します。
主な機能
- 01バージョン別のAPIガイド
- 02Colyseusのサーバーとルームの実装例
- 03Schemaと同期状態の解説
- 04クライアントコールバックと再接続の手順
- 05移行時の注意点と参照先
活用シーン
- 01編集前にColyseusのバージョンを確認
- 02ルームと同期状態の実装
- 03コードをColyseus 0.18向けに更新
- 04クライアントの状態コールバックと再接続の処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ colyseus/ skill --skill colyseusインストール後、Claude Code で/colyseusを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.gitignoresources.jsonREADME.mdpackage.jsonLICENSE.github/workflows/sync.ymlreferences/netcode.mdschema.mdroom.mdclient-sdk.mdtools/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.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 only | schema({ name: t.string() }, "MyState") | ビルダーが主要なスタイルで、コンパイラーフラグは不要です。デコレーターも引き続きサポートされています。 |
schema({ name: "string" }) | schema({ name: t.string() }) | schema 5 では、生の文字列フィールド型を指定すると定義時に例外が発生します。ただし、コレクションの子型としては引き続き有効です:t.array("string")。 |
client.id | client.sessionId | client.id は 0.18 で削除されました。 |
async onLeave(client, consented: boolean) | onLeave(client, code: CloseCode) | 2 番目の引数は切断コードです。4000 は truthy なので、if (!consented) は実行されず、再接続が許可されなくても通知されません。以下の再接続の形式を参照してください。 |
Protocol.WS_CLOSE_CONSENTED | "colyseus" からの CloseCode.CONSENTED | Protocol.WS_* は CloseCode に移動しました。4000~4010 は予約済みで、カスタムコードには 4011~4999 を使用します。 |
this.setSeatReservationTime(15) | this.seatReservationTimeout = 15 | メソッドがプロパティになりました。 |
this.setMetadata({ status: "x" }) expecting a merge | this.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/schema | 0.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。
- 同意なしで接続が閉じられると、両側で
onDropが実行されます。サーバーはallowReconnection(client, seconds)を呼び出し、SDK は同じroomインスタンス上でバックオフを伴って再試行します。 - 成功すると、両側で
onReconnectが実行されます。サーバーはフルスナップショットを送信し、その内容がインプレースで調整されます。既存のコールバックはそのまま維持され、オフライン中に変更された内容に対してonAddとonRemoveが実行され、キューに入ったroom.send()呼び出しがフラッシュされます。 - タイムアウトまたは拒否の場合、両側で
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、ミドルウェア、CORS | https://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 Cloud | https://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です。切断されたクライアントは戻ってくる可能性があります。