メインコンテンツまでスキップ

Colyseusを使ったリアルタイムマルチプレイヤー

「create game」を選択し、新しいゲームを立ち上げます。床のいずれかをクリックしてオブジェクトを移動します。

このチュートリアルでは、次のことを学びます:

  • Colyseusサーバーの設定
  • サーバーとクライアント間での状態同期
  • クライアントとサーバー間でのメッセージ交換
  • マッチメイキング: ゲームの作成、参加、利用可能なゲームのリストアップ

必要なもの

開始する前に

前提知識

ソフトウェア要件

サーバーの作成

プレイヤーの状態を保持するために、基本的なサーバーをローカルに作成します。変更はクライアントと自動的に同期されます。

新しいColyseusサーバーを作成するには、コマンドラインから以下を実行します:

npm init colyseus-app ./playcanvas-demo-server

次にnpm startを実行して、ローカルでサーバーを動かすことができるか確認しましょう。

cd playcanvas-demo-server
npm start

成功すれば、コマンドラインに以下のような出力が表示されます。

> my-app@1.0.0 start
> ts-node-dev --respawn --transpile-only src/index.ts

✅ development.env loaded.
✅ Express initialized
🏟 Your Colyseus App
⚔️ Listening on ws://localhost:2567

Colyseus JavaScript SDKのインポート

PlayCanvasにColyseus JavaScript SDKを追加する必要があります。

"PlayCanvasプロジェクトの設定"を使用して、"外部スクリプト"として追加できます。

**「メニュー」→ 「設定」**を開いてください:

settings

設定パネルから、**「外部スクリプト」を展開し、「URL」**の数を増やします。

CDN

新しい**「URL」**フィールドに、CDNからColyseus JavaScript SDKを含めてください:

https://unpkg.com/colyseus.js@^0.15.0-preview.2/dist/colyseus.js

これにより、PlayCanvasスクリプトの Colyseus JavaScript SDK を使用できます。

クライアント - サーバー接続の確立

新しいPlayCanvasスクリプトから、Colyseus.Client インスタンスを作成しましょう(「新しいスクリプトの作成方法」を参照してください)。

このスクリプトは、「NetworkManager」という新しい空のエンティティにアタッチできます。

var NetworkManager = pc.createScript('networkManager');

NetworkManager.prototype.initialize = async function () {
//
// SDKをインスタンス化します
//(接続はまだ確立されていません)
//
this.app.colyseus = new Colyseus.Client("ws://localhost:2567");

//
// ルーム「my_room」を作成または参加するようにリクエストします
//(サーバーとの接続を確立します)
//
this.room = await this.app.colyseus.joinOrCreate("my_room");
}

ここで、ローカルの ws://localhost:2567 エンドポイントを使用しています。他の人とオンラインでプレイするには、 サーバーをデプロイして、公共のインターネットを使用する必要があります。Glitchを使ってサーバーを公開することもできます。

PlayCanvasプロジェクトを**「起動」**すると、クライアントはサーバーと接続し、サーバーは必要に応じてmy_roomという部屋を作成します。

my_room は、Colyseusサーバーのデフォルトのルーム識別子です。 arena.config.ts ファイルでこの識別子を変更することができます。

クライアントがルームに正常に参加したことを意味するサーバーログに以下のメッセージが表示されます。

19U8WkmoK joined!

ルーム状態とスキーマ

Colyseusでは、共有データを Schema 構造を使用して定義します。

SchemaはColyseusからの特別なデータ型で、その変更/変異を_増分的に_エンコードする能力があります。エンコードとデコードのプロセスはフレームワークとそのSDKによって内部的に行われます。

ステート同期のループは次のようになります。

  1. 状態の変更(変異)は、サーバー→クライアント間で自動的に同期されます。
  2. クライアントは、ローカルの_読み取り専用_のSchema構造体にコールバックをアタッチすることで、状態の変化を観察し、それに対応することができます。
  3. クライアントは任意のメッセージをサーバーに送信することができます - それが何をするかはサーバーが決定します - そして状態を変化させることができます(ステップ**1.**に戻ります)

サーバーコードを編集して、サーバー側でのルーム状態を定義しましょう。

複数の Player インスタンスを処理する必要があります。各 Player には、 xyz座標があります。

// MyRoomState.ts
import { MapSchema, Schema, type } from "@colyseus/schema";

export class Player extends Schema {
@type("number") x: number;
@type("number") y: number;
@type("number") z: number;
}

export class MyRoomState extends Schema {
@type({ map: Player }) players = new MapSchema<Player>();
}

スキーマ構造についてもご覧ください。

次に、サーバーサイドで onJoin() メソッドを変更して、ルームとの新しい接続が確立されるたびに Player インスタンスを作成します。

// MyRoom.ts
// ...
onJoin(client: Client, options: any) {
console.log(client.sessionId, "joined!");

// create Player instance
const player = new Player();

// place Player at a random position
const FLOOR_SIZE = 4;
player.x = -(FLOOR_SIZE/2) + (Math.random() * FLOOR_SIZE);
player.y = 1.031;
player.z = -(FLOOR_SIZE/2) + (Math.random() * FLOOR_SIZE);

// place player in the map of players by its sessionId
// (client.sessionId is unique per connection!)
this.state.players.set(client.sessionId, player);
}
// ...
}

また、クライアントが切断された場合には、プレイヤーをプレイヤーマップから削除するようにしましょう。

// MyRoom.ts
// ...
onLeave(client: Client, consented: boolean) {
console.log(client.sessionId, "left!");

this.state.players.delete(client.sessionId);
}
// ...

サーバーサイドで行った状態変化は、クライアントサイドで 観察できます 。次のセクションでやることです。

同期のためのシーンのセットアップ

このデモ用に、シーンに2つのオブジェクトを作成する必要があります。

  • 床を表す Plane
  • プレイヤーを表す Capsule。新しいプレイヤーがルームに参加するたびに複製します。

Planeの作成

スケール8のPlaneを作成しましょう。

Plane

Playerの作成

スケール1のPlayerカプセルを作成しましょう。

"Enabled"プロパティのチェックを外すことを確認してください。サーバーとのアクティブな接続があるまでは、Playerのインスタンスは有効化されません。

Player

Stateの変更を監視

ルームとの接続が確立した後、クライアント側はStateの変更を監視し、サーバー上のデータの視覚的な表現を作成できます。

Playerの追加

Room State and Schemaセクションによると、サーバーが新しい接続を受け入れると、 onJoin() メソッドがState内に新しいPlayerインスタンスを作成します。

これをクライアント側でリッスンするようにします。

// ...
this.room.state.players.onAdd((player, sessionId) => {
//
// プレイヤーが参加しました!
//
console.log("A player has joined! Their unique session id is", sessionId);
});
// ...

Sceneをプレイすると、新しいクライアントがルームに参加するたびに、ブラウザのコンソールにメッセージが表示されます。

視覚的な表現については、"Player"オブジェクトをクローンし、そのsessionIdに基づいてクローンされたオブジェクトのローカル参照を保持しておく必要があります。これにより、後で操作できます。

// ...

// `sessionId`ごとにそれぞれのプレイヤーのビジュアル表現を割り当てます
this.playerEntities = {};

// 新しいプレイヤーをリッスンします
this.room.state.players.onAdd((player, sessionId) => {
// 基本のPlayer表現を検索します(有効になっていません)
const playerEntityToClone = this.app.root.findByName("Player");

// Player表現をクローンし、有効にします!
const entity = playerEntityToClone.clone();
entity.enabled = true;

// サーバーデータに基づいて位置を設定します
entity.setPosition(player.x, player.y, player.z);

// クローンをSceneに追加します
playerEntityToClone.parent.addChild(entity);

// `sessionId`によってビジュアル表現を割り当てます
this.playerEntities[sessionId] = entity;
});
// ...

現在のプレイヤー

sessionIdを接続されたroom.sessionIdと照合して、現在のプレイヤーオブジェクトに対する特別な参照を保持することができます。

// ...
this.room.state.players.onAdd((player, sessionId) => {
// ...
if (this.room.sessionId === sessionId) {
this.currentPlayerEntity = this.playerEntities[sessionId];
}
// ...
});

接続が切断されたプレイヤーの削除

プレイヤーがStateから削除された場合(サーバーサイドで onLeave() が呼び出された場合)、その視覚的な表現も削除する必要があります。

// ...
this.room.state.players.onRemove((player, sessionId) => {
// destroy entity
this.playerEntities[sessionId].destroy();

// clear local reference
delete this.playerEntities[sessionId];
});
// ...

プレイヤーの移動

新しい位置をサーバーに送信する

マウスダウンイベントを許可し、ray castを使用して、プレイヤーが移動するべき正確なVec3位置を決定し、それをメッセージとしてサーバーに送信するようにします。

// ...
this.app.mouse.on(pc.EVENT_MOUSEDOWN, (event) => {
// 床の「バウンディングボックス」を作成します
const boundingBox = new pc.BoundingBox(new pc.Vec3(0, 0, 0), new pc.Vec3(4, 0.001, 4));;

// rayを初期化し、rayの方向を決定します
// スクリーン位置からのrayの方向を決定します
const ray = new pc.Ray();
const targetPosition = new pc.Vec3();

const cameraEntity = this.app.root.findByName("Camera");
cameraEntity.camera.screenToWorld(event.x, event.y, cameraEntity.camera.farClip, ray.direction);
ray.origin.copy(cameraEntity.getPosition());
ray.direction.sub(ray.origin).normalize();

// 地面に対してrayをテストします
const result = boundingBox.intersectsRay(ray, targetPosition);

if (result) {
// 位置の高さを調整
targetPosition.y = 1.031;

//
// 新しい目標プレイヤー位置をサーバーに送信します。
//
this.room.send("updatePosition", {
x: targetPosition.x,
y: targetPosition.y,
z: targetPosition.z,
});
}
});

サーバーからのメッセージの受信

サーバーから "updatePosition" メッセージを受信するたびに、メッセージを送信したプレイヤーをそのsessionIdを通じて変更します。

// MyRoom.ts
// ...
onCreate(options: any) {
this.setState(new MyRoomState());

this.onMessage("updatePosition", (client, data) => {
const player = this.state.players.get(client.sessionId);
player.x = data.x;
player.y = data.y;
player.z = data.z;
});
}
// ...

プレイヤーの視覚表現の更新

サーバーでの変更を持っているため、クライアント側では、player.onChange()またはplayer.listen()を介して変更を検出できます。

  • player.onChange()は、 スキーマインスタンスごと にトリガーされます。
  • player.listen(prop) は、 プロパティー の変更ごとにトリガーされます。

1つずつ変更があった場合でも、すべての新しい座標が同時に必要なため、 .onChange() を使用する必要があります。

// ...
this.room.state.players.onAdd((player, sessionId) => {
// ...
player.onChange(() => {
this.playerEntities[sessionId].setPosition(player.x, player.y, player.z);
});

// Alternative, listening to individual properties:
// player.listen("x", (newX, prevX) => console.log(newX, prevX));
// player.listen("y", (newY, prevY) => console.log(newY, prevY));
// player.listen("z", (newZ, prevZ) => console.log(newZ, prevZ));
});

スキーマコールバックについてもっと読む

追加: ルームと接続のモニタリング

Colyseusには、ゲーム開発中に役立つオプションのモニタリングパネルが付属しています。

ローカルサーバーからモニターパネルを表示するには、http://localhost:2567/colyseus にアクセスしてください。

monitor

このパネルを通じて、スポーンしたすべてのルームやアクティブなクライアント接続を見ることができ、相互作用することができます。

モニタリングパネルの詳細についてはこちらをご覧ください。

さらに詳しく

このチュートリアルが役立ったことを願っています。Colyseusについてもっと学びたい場合は、Colyseusドキュメントを参照して、Colyseus Discordコミュニティに参加してください。