ColyseusJS 輕量級多人遊戲伺服器開發框架 - 中文手冊(下)

為少發表於2021-05-10


快速上手多人遊戲伺服器開發。後續會基於 Google Agones,更新相關 K8S 運維、大規模快速擴充套件專用遊戲伺服器的文章。擁抱☁️原生? Cloud-Native!

系列

狀態處理

Colyseus 中,room handlers有狀態(stateful) 的。每個房間都有自己的狀態。狀態的突變會自動同步到所有連線的客戶端。

序列化方法

  • Schema (default)

狀態同步時

  • user 成功加入 room 後,他將從伺服器接收到完整狀態。
  • 在每個 patchRate 處,狀態的二進位制補丁會傳送到每個客戶端(預設值為50ms
  • 從伺服器接收到每個補丁後,在客戶端呼叫 onStateChange
  • 每種序列化方法都有自己處理傳入狀態補丁的特殊方式。

Schema

SchemaSerializer 是從 Colyseus 0.10 開始引入的,它是預設的序列化方法。

Schema 結構只用於房間的狀態(可同步資料)。對於不能同步的演算法中的資料,您需要使用 Schema 及其其他結構。

服務端

要使用 SchemaSerializer,你必須:

  • 有一個擴充套件 Schema 類的狀態類
  • @type() 裝飾器註釋你所有的可同步屬性
  • 為您的房間例項化狀態(this.setState(new MyState()))
import { Schema, type } from "@colyseus/schema";

class MyState extends Schema {
    @type("string")
    currentTurn: string;
}

原始型別

這些是您可以為 @type() 裝飾器提供的型別及其限制。

如果您確切地知道 number 屬性的範圍,您可以通過為其提供正確的原始型別來優化序列化。
否則,請使用 "number",它將在序列化過程中新增一個額外的位元組來標識自己。

Type Description Limitation
"string" utf8 strings maximum byte size of 4294967295
"number" auto-detects the int or float type to be used. (adds an extra byte on output) 0 to 18446744073709551615
"boolean" true or false 0 or 1
"int8" signed 8-bit integer -128 to 127
"uint8" unsigned 8-bit integer 0 to 255
"int16" signed 16-bit integer -32768 to 32767
"uint16" unsigned 16-bit integer 0 to 65535
"int32" signed 32-bit integer -2147483648 to 2147483647
"uint32" unsigned 32-bit integer 0 to 4294967295
"int64" signed 64-bit integer -9223372036854775808 to 9223372036854775807
"uint64" unsigned 64-bit integer 0 to 18446744073709551615
"float32" single-precision floating-point number -3.40282347e+38 to 3.40282347e+38
"float64" double-precision floating-point number -1.7976931348623157e+308 to 1.7976931348623157e+308

子 schema 屬性

您可以在 "root" 狀態定義中定義更多自定義資料型別,如直接引用(direct reference)、對映(map)或陣列(array)。

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

class World extends Schema {
    @type("number")
    width: number;

    @type("number")
    height: number;

    @type("number")
    items: number = 10;
}

class MyState extends Schema {
    @type(World)
    world: World = new World();
}

ArraySchema

ArraySchema 是內建 JavaScript Array 型別的可同步版本。

可以從陣列中使用更多的方法。看看陣列的 MDN 文件

示例:自定義 Schema 型別的陣列

import { Schema, ArraySchema, type } from "@colyseus/schema";

class Block extends Schema {
    @type("number")
    x: number;

    @type("number")
    y: number;
}

class MyState extends Schema {
    @type([ Block ])
    blocks = new ArraySchema<Block>();
}

示例:基本型別的陣列

您不能在陣列內混合型別。

import { Schema, ArraySchema, type } from "@colyseus/schema";

class MyState extends Schema {
    @type([ "string" ])
    animals = new ArraySchema<string>();
}

array.push()

在陣列的末尾新增一個或多個元素,並返回該陣列的新長度。

const animals = new ArraySchema<string>();
animals.push("pigs", "goats");
animals.push("sheeps");
animals.push("cows");
// output: 4

array.pop()

從陣列中刪除最後一個元素並返回該元素。此方法更改陣列的長度。

animals.pop();
// output: "cows"

animals.length
// output: 3

array.shift()

從陣列中刪除第一個元素並返回被刪除的元素。這個方法改變陣列的長度。

animals.shift();
// output: "pigs"

animals.length
// output: 2

array.unshift()

將一個或多個元素新增到陣列的開頭,並返回陣列的新長度。

animals.unshift("pigeon");
// output: 3

array.indexOf()

返回給定元素在陣列中的第一個下標,如果不存在則返回 -1

const itemIndex = animals.indexOf("sheeps");

array.splice()

通過刪除或替換現有元素和/或在適當位置新增新元素來更改陣列的內容。

// find the index of the item you'd like to remove
const itemIndex = animals.findIndex((animal) => animal === "sheeps");

// remove it!
animals.splice(itemIndex, 1);

array.forEach()

迭代陣列中的每個元素。

this.state.array1 = new ArraySchema<string>('a', 'b', 'c');

this.state.array1.forEach(element => {
    console.log(element);
});
// output: "a"
// output: "b"
// output: "c"

MapSchema

MapSchema 是內建 JavaScript Map 型別的一個可同步版本。

建議使用 MapsID 跟蹤您的遊戲實體(entities),例如玩家(players),敵人(enemies)等。

"目前僅支援字串 key":目前,MapSchema 只允許您提供值型別。key 型別總是 string

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

class Player extends Schema {
    @type("number")
    x: number;

    @type("number")
    y: number;
}

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

map.get()

通過 key 獲取一個 map 條目:

const map = new MapSchema<string>();
const item = map.get("key");

OR

//
// NOT RECOMMENDED
//
// This is a compatibility layer with previous versions of @colyseus/schema
// This is going to be deprecated in the future.
//
const item = map["key"];

map.set()

key 設定 map 項:

const map = new MapSchema<string>();
map.set("key", "value");

OR

//
// NOT RECOMMENDED
//
// This is a compatibility layer with previous versions of @colyseus/schema
// This is going to be deprecated in the future.
//
map["key"] = "value";

map.delete()

key 刪除一個 map 項:

map.delete("key");

OR

//
// NOT RECOMMENDED
//
// This is a compatibility layer with previous versions of @colyseus/schema
// This is going to be deprecated in the future.
//
delete map["key"];

map.size

返回 MapSchema 物件中的元素數量。

const map = new MapSchema<number>();
map.set("one", 1);
map.set("two", 2);

console.log(map.size);
// output: 2

map.forEach()

按插入順序遍歷 map 的每個 key/value 對。

this.state.players.forEach((value, key) => {
    console.log("key =>", key)
    console.log("value =>", value)
});

"所有 Map 方法":您可以從 Maps 中使用更多的方法。看一看 MDN 文件的 Maps

CollectionSchema

"CollectionSchema 僅用 JavaScript 實現":目前為止,CollectionSchema 只能用於 JavaScript。目前還不支援 Haxec#LUAc++ 客戶端。

CollectionSchemaArraySchema 的工作方式相似,但需要注意的是您無法控制其索引。

import { Schema, CollectionSchema, type } from "@colyseus/schema";

class Item extends Schema {
    @type("number")
    damage: number;
}

class Player extends Schema {
    @type({ collection: Item })
    items = new CollectionSchema<Item>();
}

collection.add()

item 追加到 CollectionSchema 物件。

const collection = new CollectionSchema<number>();
collection.add(1);
collection.add(2);
collection.add(3);

collection.at()

獲取位於指定 index 處的 item

const collection = new CollectionSchema<string>();
collection.add("one");
collection.add("two");
collection.add("three");

collection.at(1);
// output: "two"

collection.delete()

根據 item 的值刪除 item

collection.delete("three");

collection.has()

返回一個布林值,無論該 item 是否存在於 set 中。

if (collection.has("two")) {
    console.log("Exists!");
} else {
    console.log("Does not exist!");
}

collection.size

返回 CollectionSchema 物件中的元素數量。

const collection = new CollectionSchema<number>();
collection.add(10);
collection.add(20);
collection.add(30);

console.log(collection.size);
// output: 3

collection.forEach()

對於 CollectionSchema 物件中的每個 index/value 對,forEach() 方法按插入順序執行所提供的函式一次。

collection.forEach((value, at) => {
    console.log("at =>", at)
    console.log("value =>", value)
});

SetSchema

"SetSchema 只在 JavaScript 中實現":SetSchema 目前只能在 JavaScript 中使用。目前還不支援 HaxeC#LUAC++ 客戶端。

SetSchema 是內建 JavaScript Set 型別的可同步版本。

"更多":你可以從 Sets 中使用更多的方法。看一下 MDN 文件的 Sets

SetSchema 的用法與 [CollectionSchema] 非常相似,最大的區別是 Sets 保持唯一的值。Sets 沒有直接訪問值的方法。(如collection.at())

import { Schema, SetSchema, type } from "@colyseus/schema";

class Effect extends Schema {
    @type("number")
    radius: number;
}

class Player extends Schema {
    @type({ set: Effect })
    effects = new SetSchema<Effect>();
}

set.add()

SetSchema 物件追加一個 item

const set = new CollectionSchema<number>();
set.add(1);
set.add(2);
set.add(3);

set.at()

獲取位於指定 index 處的項。

const set = new CollectionSchema<string>();
set.add("one");
set.add("two");
set.add("three");

set.at(1);
// output: "two"

set.delete()

根據項的值刪除項。

set.delete("three");

set.has()

返回一個布林值,無論該項是否存在於集合中。

if (set.has("two")) {
    console.log("Exists!");
} else {
    console.log("Does not exist!");
}

set.size

返回 SetSchema 物件中的元素數量。

const set = new SetSchema<number>();
set.add(10);
set.add(20);
set.add(30);

console.log(set.size);
// output: 3

過濾每個客戶端的資料

"這個特性是實驗性的":@filter()/@filterChildren() 是實驗性的,可能無法針對快節奏的遊戲進行優化。

過濾旨在為特定客戶端隱藏狀態的某些部分,以避免在玩家決定檢查來自網路的資料並檢視未過濾狀態資訊的情況下作弊。

資料過濾器是每個客戶端每個欄位(或每個子結構,在 @filterChildren 的情況下)都會觸發的回撥。如果過濾器回撥返回 true,欄位資料將為該特定客戶端傳送,否則,資料將不為該客戶端傳送。

請注意,如果過濾函式的依賴關係發生變化,它不會自動重新執行,但只有在過濾欄位(或其子欄位)被更新時才會重新執行。請參閱此問題以瞭解解決方法。

@filter() property decorator

@filter() 屬性裝飾器可以用來過濾掉整個 Schema 欄位。

下面是 @filter() 簽名的樣子:

class State extends Schema {
    @filter(function(client, value, root) {
        // client is:
        //
        // the current client that's going to receive this data. you may use its
        // client.sessionId, or other information to decide whether this value is
        // going to be synched or not.

        // value is:
        // the value of the field @filter() is being applied to

        // root is:
        // the root instance of your room state. you may use it to access other
        // structures in the process of decision whether this value is going to be
        // synched or not.
    })
    @type("string") field: string;
}

@filterChildren() 屬性裝飾器

@filterChildren() 屬性裝飾器可以用來過濾出 arraysmapssets 等內部的項。它的簽名與 @filter() 非常相似,只是在 value 之前增加了 key 引數 — 表示 ArraySchemaMapSchemaCollectionSchema 等中的每一項。

class State extends Schema {
    @filterChildren(function(client, key, value, root) {
        // client is:
        //
        // the current client that's going to receive this data. you may use its
        // client.sessionId, or other information to decide whether this value is
        // going to be synched or not.

        // key is:
        // the key of the current value inside the structure

        // value is:
        // the current value inside the structure

        // root is:
        // the root instance of your room state. you may use it to access other
        // structures in the process of decision whether this value is going to be
        // synched or not.
    })
    @type([Cards]) cards = new ArraySchema<Card>();
}

例子: 在一場紙牌遊戲中,每張紙牌的相關資料只應供紙牌擁有者使用,或在某些情況下(例如紙牌已被丟棄)才可使用。

檢視 @filter() 回撥簽名:

import { Client } from "colyseus";

class Card extends Schema {
    @type("string") owner: string; // contains the sessionId of Card owner
    @type("boolean") discarded: boolean = false;

    /**
     * DO NOT USE ARROW FUNCTION INSIDE `@filter`
     * (IT WILL FORCE A DIFFERENT `this` SCOPE)
     */
    @filter(function(
        this: Card, // the instance of the class `@filter` has been defined (instance of `Card`)
        client: Client, // the Room's `client` instance which this data is going to be filtered to
        value: Card['number'], // the value of the field to be filtered. (value of `number` field)
        root: Schema // the root state Schema instance
    ) {
        return this.discarded || this.owner === client.sessionId;
    })
    @type("uint8") number: number;
}

向後/向前相容性

向後/向前相容性可以通過在現有結構的末尾宣告新的欄位來實現,以前的宣告不被刪除,但在需要時被標記為 @deprecated()

這對於原生編譯的目標特別有用,比如 C#, C++, Haxe 等 — 在這些目標中,客戶端可能沒有最新版本的 schema 定義。

限制和最佳實踐

  • 每個 Schema 結構最多可以容納 64 個欄位。如果需要更多欄位,請使用巢狀的 Schema 結構。
  • NaNnull 數字被編碼為 0
  • null 字串被編碼為 ""
  • Infinity 被編碼為 Number.MAX_SAFE_INTEGER 的數字。
  • 不支援多維陣列。瞭解如何將一維陣列用作多維陣列
  • ArraysMaps 中的項必須都是同一型別的例項。
  • @colyseus/schema 只按照指定的順序編碼欄位值。
    • encoder(伺服器)和decoder(客戶端)必須有相同的 schema 定義。
    • 欄位的順序必須相同。

客戶端

Callbacks

您可以在客戶端 schema 結構中使用以下回撥來處理來自伺服器端的更改。

  • onAdd (instance, key)
  • onRemove (instance, key)
  • onChange (changes) (on Schema instance)
  • onChange (instance, key) (on collections: MapSchema, ArraySchema, etc.)
  • listen()

"C#, C++, Haxe":當使用靜態型別語言時,需要根據 TypeScript schema 定義生成客戶端 schema 檔案。參見在客戶端生成 schema

onAdd (instance, key)

onAdd 回撥只能在 maps (MapSchema)和陣列(ArraySchema)中使用。呼叫 onAdd 回撥函式時,會使用新增的例項及其 holder 物件上的 key 作為引數。

room.state.players.onAdd = (player, key) => {
    console.log(player, "has been added at", key);

    // add your player entity to the game world!

    // If you want to track changes on a child object inside a map, this is a common pattern:
    player.onChange = function(changes) {
        changes.forEach(change => {
            console.log(change.field);
            console.log(change.value);
            console.log(change.previousValue);
        })
    };

    // force "onChange" to be called immediatelly
    player.triggerAll();
};

onRemove (instance, key)

onRemove 回撥只能在 maps (MapSchema) 和 arrays (ArraySchema) 中使用。呼叫 onRemove 回撥函式時,會使用被刪除的例項及其 holder 物件上的 key 作為引數。

room.state.players.onRemove = (player, key) => {
    console.log(player, "has been removed at", key);

    // remove your player entity from the game world!
};

onChange (changes: DataChange[])

onChange 對於直接 Schema 引用和集合結構的工作方式不同。關於集合結構 (array,map 等)的 onChange,請點選這裡

您可以註冊 onChange 來跟蹤 Schema 例項的屬性更改。onChange 回撥是由一組更改過的屬性以及之前的值觸發的。

room.state.onChange = (changes) => {
    changes.forEach(change => {
        console.log(change.field);
        console.log(change.value);
        console.log(change.previousValue);
    });
};

你不能在未與客戶端同步的物件上註冊 onChange 回撥。


onChange (instance, key)

onChange 對於直接 Schema 引用和 collection structures 的工作方式不同。

每當 primitive 型別(string, number, boolean等)的集合更新它的一些值時,這個回撥就會被觸發。

room.state.players.onChange = (player, key) => {
    console.log(player, "have changes at", key);
};

如果您希望檢測 non-primitive 型別(包含 Schema 例項)集合中的更改,請使用onAdd 並在它們上註冊 onChange

"onChangeonAddonRemoveexclusive(獨佔) 的":
onAddonRemove 期間不會觸發 onChange 回撥。

如果在這些步驟中還需要檢測更改,請考慮註冊 `onAdd` 和 `onRemove`。

.listen(prop, callback)

監聽單個屬性更改。

.listen() 目前只適用於 JavaScript/TypeScript

引數:

  • property: 您想要監聽更改的屬性名。
  • callback: 當 property 改變時將被觸發的回撥。
state.listen("currentTurn", (currentValue, previousValue) => {
    console.log(`currentTurn is now ${currentValue}`);
    console.log(`previous value was: ${previousValue}`);
});

.listen() 方法返回一個用於登出監聽器的函式:

const removeListener = state.listen("currentTurn", (currentValue, previousValue) => {
    // ...
});

// later on, if you don't need the listener anymore, you can call `removeListener()` to stop listening for `"currentTurn"` changes.
removeListener();

listenonChange 的區別是什麼?

.listen() 方法是單個屬性上的 onChange 的簡寫。下面是

state.onChange = function(changes) {
    changes.forEach((change) => {
        if (change.field === "currentTurn") {
            console.log(`currentTurn is now ${change.value}`);
            console.log(`previous value was: ${change.previousValue}`);
        }
    })
}

客戶端 schema 生成

這隻適用於使用靜態型別語言(如 C#、C++ 或 Haxe)的情況。

在伺服器專案中,可以執行 npx schema-codegen 自動生成客戶端 schema 檔案。

npx schema-codegen --help

輸出:

schema-codegen [path/to/Schema.ts]

Usage (C#/Unity)
    schema-codegen src/Schema.ts --output client-side/ --csharp --namespace MyGame.Schema

Valid options:
    --output: fhe output directory for generated client-side schema files
    --csharp: generate for C#/Unity
    --cpp: generate for C++
    --haxe: generate for Haxe
    --ts: generate for TypeScript
    --js: generate for JavaScript
    --java: generate for Java

Optional:
    --namespace: generate namespace on output code

Built-in room » Lobby Room

"大廳房間的客戶端 API 將在 Colyseus 1.0.0 上更改":

  • 內建的大廳房間目前依賴於傳送訊息來通知客戶可用的房間。當 @filter() 變得穩定時,LobbyRoom 將使用 state 代替。

伺服器端

內建的 LobbyRoom 將自動通知其連線的客戶端,每當房間 "realtime listing" 有更新。

import { LobbyRoom } from "colyseus";

// Expose the "lobby" room.
gameServer
  .define("lobby", LobbyRoom);

// Expose your game room with realtime listing enabled.
gameServer
  .define("your_game", YourGameRoom)
  .enableRealtimeListing();

onCreate()onJoin()onLeave()onDispose() 期間,會自動通知 LobbyRoom

如果你已經更新了你房間的metadata,並且需要觸發一個 lobby 的更新,你可以在後設資料更新之後呼叫 updateLobby()

import { Room, updateLobby } from "colyseus";

class YourGameRoom extends Room {

  onCreate() {

    //
    // This is just a demonstration
    // on how to call `updateLobby` from your Room
    //
    this.clock.setTimeout(() => {

      this.setMetadata({
        customData: "Hello world!"
      }).then(() => updateLobby(this));

    }, 5000);

  }

}

客戶端

您需要通過從 LobbyRoom 傳送給客戶端的資訊來跟蹤正在新增、刪除和更新的房間。

import { Client, RoomAvailable } from "colyseus.js";

const client = new Client("ws://localhost:2567");
const lobby = await client.joinOrCreate("lobby");

let allRooms: RoomAvailable[] = [];

lobby.onMessage("rooms", (rooms) => {
  allRooms = rooms;
});

lobby.onMessage("+", ([roomId, room]) => {
  const roomIndex = allRooms.findIndex((room) => room.roomId === roomId);
  if (roomIndex !== -1) {
    allRooms[roomIndex] = room;

  } else {
    allRooms.push(room);
  }
});

lobby.onMessage("-", (roomId) => {
  allRooms = allRooms.filter((room) => room.roomId !== roomId);
});

Built-in room » Relay Room

內建的 RelayRoom 對於簡單的用例非常有用,在這些用例中,除了連線到它的客戶端之外,您不需要在伺服器端儲存任何狀態。

通過簡單地中繼訊息(將訊息從客戶端轉發給其他所有人) — 伺服器端不能驗證任何訊息 — 客戶端應該執行驗證。

RelayRoom 的原始碼非常簡單。一般的建議是在您認為合適的時候使用伺服器端驗證來實現您自己的版本。

伺服器端

import { RelayRoom } from "colyseus";

// Expose your relayed room
gameServer.define("your_relayed_room", RelayRoom, {
  maxClients: 4,
  allowReconnectionTime: 120
});

客戶端

請參閱如何註冊來自 relayed room 的玩家加入、離開、傳送和接收訊息的回撥。

連線到房間

import { Client } from "colyseus.js";

const client = new Client("ws://localhost:2567");

//
// Join the relayed room
//
const relay = await client.joinOrCreate("your_relayed_room", {
  name: "This is my name!"
});

在玩家加入和離開時註冊回撥

//
// Detect when a player joined the room
//
relay.state.players.onAdd = (player, sessionId) => {
  if (relay.sessionId === sessionId) {
    console.log("It's me!", player.name);

  } else {
    console.log("It's an opponent", player.name, sessionId);
  }
}

//
// Detect when a player leave the room
//
relay.state.players.onRemove = (player, sessionId) => {
  console.log("Opponent left!", player, sessionId);
}

//
// Detect when the connectivity of a player has changed
// (only available if you provided `allowReconnection: true` in the server-side)
//
relay.state.players.onChange = (player, sessionId) => {
  if (player.connected) {
    console.log("Opponent has reconnected!", player, sessionId);

  } else {
    console.log("Opponent has disconnected!", player, sessionId);
  }
}

傳送和接收訊息

//
// By sending a message, all other clients will receive it under the same name
// Messages are only sent to other connected clients, never the current one.
//
relay.send("fire", {
  x: 100,
  y: 200
});

//
// Register a callback for messages you're interested in from other clients.
//
relay.onMessage("fire", ([sessionId, message]) => {

  //
  // The `sessionId` from who sent the message
  //
  console.log(sessionId, "sent a message!");

  //
  // The actual message sent by the other client
  //
  console.log("fire at", message);
});

Colyseus 的最佳實踐

這一部分需要改進和更多的例子!每一段都需要有自己的一頁,有詳盡的例子和更好的解釋。

  • 保持你的 room 類儘可能小,沒有遊戲邏輯
  • 使可同步的資料結構儘可能小
    • 理想情況下,擴充套件 Schema 的每個類應該只有欄位定義。
    • 自定義 getter 和 setter 方法可以實現,只要它們中沒有遊戲邏輯。
  • 你的遊戲邏輯應該由其他結構來處理,例如:

為什麼?

  • Models (@colyseus/schema) 應該只包含資料,不包含遊戲邏輯。
  • Rooms 應該有儘可能少的程式碼,並將動作轉發給其他結構

命令模式有幾個優點,例如:

  • 它將呼叫該操作的類與知道如何執行該操作的物件解耦。
  • 它允許你通過提供一個佇列系統來建立一個命令序列。
  • 實現擴充套件來新增一個新的命令很容易,可以在不改變現有程式碼的情況下完成。
  • 嚴格控制命令的呼叫方式和呼叫時間。
  • 由於命令簡化了程式碼,因此程式碼更易於使用、理解和測試。

用法

安裝

npm install --save @colyseus/command

在您的 room 實現中初始化 dispatcher

import { Room } from "colyseus";
import { Dispatcher } from "@colyseus/command";

import { OnJoinCommand } from "./OnJoinCommand";

class MyRoom extends Room<YourState> {
  dispatcher = new Dispatcher(this);

  onCreate() {
    this.setState(new YourState());
  }

  onJoin(client, options) {
    this.dispatcher.dispatch(new OnJoinCommand(), {
        sessionId: client.sessionId
    });
  }

  onDispose() {
    this.dispatcher.stop();
  }
}
const colyseus = require("colyseus");
const command = require("@colyseus/command");

const OnJoinCommand = require("./OnJoinCommand");

class MyRoom extends colyseus.Room {

  onCreate() {
    this.dispatcher = new command.Dispatcher(this);
    this.setState(new YourState());
  }

  onJoin(client, options) {
    this.dispatcher.dispatch(new OnJoinCommand(), {
        sessionId: client.sessionId
    });
  }

  onDispose() {
    this.dispatcher.stop();
  }
}

命令實現的樣子:

// OnJoinCommand.ts
import { Command } from "@colyseus/command";

export class OnJoinCommand extends Command<YourState, {
    sessionId: string
}> {

  execute({ sessionId }) {
    this.state.players[sessionId] = new Player();
  }

}
// OnJoinCommand.js
const command = require("@colyseus/command");

exports.OnJoinCommand = class OnJoinCommand extends command.Command {

  execute({ sessionId }) {
    this.state.players[sessionId] = new Player();
  }

}

檢視更多

Refs

中文手冊同步更新在:

  • https:/colyseus.hacker-linner.com
我是為少
微信:uuhells123
公眾號:黑客下午茶
加我微信(互相學習交流),關注公眾號(獲取更多學習資料~)

相關文章