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

為少發表於2021-05-09

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

系列

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

Web-Socket Server

Server

Server 負責提供 WebSocket server 來實現伺服器和客戶端之間的通訊。

constructor (options)

options.server

要繫結 WebSocket Server 的 HTTP server。你也可以在你的伺服器上使用 express

// Colyseus + Express
import { Server } from "colyseus";
import { createServer } from "http";
import express from "express";
const port = Number(process.env.port) || 3000;

const app = express();
app.use(express.json());

const gameServer = new Server({
  server: createServer(app)
});

gameServer.listen(port);
// Colyseus (barebones)
import { Server } from "colyseus";
const port = process.env.port || 3000;

const gameServer = new Server();
gameServer.listen(port);

options.pingInterval

伺服器 "ping" 客戶端的毫秒數。預設值: 3000

如果客戶端在 pingMaxRetries 重試後不能響應,則將強制斷開連線。

options.pingMaxRetries

沒有響應的最大允許 ping 數。預設值: 2

options.verifyClient

這個方法發生在 WebSocket 握手之前。如果 verifyClient 沒有設定,那麼握手會被自動接受。

  • info (Object)

    • origin (String) 客戶端指定的 Origin header 中的值。
    • req (http.IncomingMessage) 客戶端 HTTP GET 請求。
    • secure (Boolean) true,如果 req.connection.authorizedreq.connection.encrypted 被設定。
  • next (Function) 使用者必須在檢查 info 欄位後呼叫該回撥。此回撥中的引數為:

    • result (Boolean) 是否接受握手。
    • code (Number) 當 resultfalse 時,該欄位決定傳送給客戶端的 HTTP 錯誤狀態碼。
    • name (String) 當 resultfalse 時,該欄位決定 HTTP 原因短語。
import { Server } from "colyseus";

const gameServer = new Server({
  // ...

  verifyClient: function (info, next) {
    // validate 'info'
    //
    // - next(false) will reject the websocket handshake
    // - next(true) will accept the websocket handshake
  }
});

options.presence

當通過多個程式/機器擴充套件 Colyseus 時,您需要提供一個狀態伺服器。

import { Server, RedisPresence } from "colyseus";

const gameServer = new Server({
  // ...
  presence: new RedisPresence()
});

當前可用的狀態伺服器是:

  • RedisPresence (在單個伺服器和多個伺服器上擴充套件)

options.gracefullyShutdown

自動註冊 shutdown routine。預設為 true。如果禁用,則應在關閉程式中手動呼叫 gracefullyShutdown() 方法。

define (name: string, handler: Room, options?: any)

定義一個新的 room handler

Parameters:

  • name: string - room 的公共名稱。當從客戶端加入 room 時,您將使用這個名稱。
  • handler: Room - 引用 Room handler 類。
  • options?: any - room 初始化的自定義選項。
// Define "chat" room
gameServer.define("chat", ChatRoom);

// Define "battle" room
gameServer.define("battle", BattleRoom);

// Define "battle" room with custom options
gameServer.define("battle_woods", BattleRoom, { map: "woods" });

"多次定義同一個 room handler":

  • 您可以使用不同的 options 多次定義同一個 room handler。當呼叫 Room#onCreate() 時,options 將包含您在 Server#define() 中指定的合併值 + 建立房間時提供的選項。

Matchmaking 過濾器: filterBy(options)

引數

  • options: string[] - 選項名稱的列表

當一個房間由 create()joinOrCreate() 方法建立時,只有 filterBy() 方法定義的 options 將被儲存在內部,並用於在 join()joinOrCreate() 呼叫中過濾出相關 rooms

示例: 允許不同的“遊戲模式”。

gameServer
  .define("battle", BattleRoom)
  .filterBy(['mode']);

無論何時建立房間,mode 選項都將在內部儲存。

client.joinOrCreate("battle", { mode: "duo" }).then(room => {/* ... */});

您可以在 onCreate() 和/或 onJoin() 中處理提供的選項,以在 room 實現中實現請求的功能。

class BattleRoom extends Room {
  onCreate(options) {
    if (options.mode === "duo") {
      // do something!
    }
  }
  onJoin(client, options) {
    if (options.mode === "duo") {
      // put this player into a team!
    }
  }
}

示例: 通過內建的 maxClients 進行過濾

maxClients 是一個用於 matchmaking 的內部變數,也可以用於過濾。

gameServer
  .define("battle", BattleRoom)
  .filterBy(['maxClients']);

然後客戶端可以要求加入一個能夠容納一定數量玩家的房間。

client.joinOrCreate("battle", { maxClients: 10 }).then(room => {/* ... */});
client.joinOrCreate("battle", { maxClients: 20 }).then(room => {/* ... */});

Matchmaking 優先順序: sortBy(options)

您還可以根據建立時加入房間的資訊為加入房間賦予不同的優先順序。

options 引數是一個鍵值物件,在左側包含欄位名稱,在右側包含排序方向。排序方向可以是以下值之一:-1"desc""descending"1"asc""ascending"

示例: 按內建的 clients 排序

clients 是為 matchmaking 而儲存的內部變數,其中包含當前已連線客戶端的數量。在以下示例中,連線最多客戶端的房間將具有優先權。使用 -1"desc""descending" 降序排列:

gameServer
  .define("battle", BattleRoom)
  .sortBy({ clients: -1 });

要按最少數量的玩家進行排序,您可以做相反的事情。將 1"asc""ascending" 用於升序:

gameServer
  .define("battle", BattleRoom)
  .sortBy({ clients: 1 });

啟用大廳的實時 room 列表

為了允許 LobbyRoom 接收來自特定房間型別的更新,您應該在啟用實時列表的情況下對其進行定義:

gameServer
  .define("battle", BattleRoom)
  .enableRealtimeListing();

監聽 room 例項事件

define 方法將返回已註冊的 handler 例項,您可以從 room 例項範圍之外監聽 match-making 事件。如:

  • "create" - 當 room 被建立時
  • "dispose" - 當 room 被銷燬時
  • "join" - 當客戶端加入一個 room
  • "leave" - 當客戶端離開一個 room
  • "lock" - 當 room 已經被鎖定時
  • "unlock" - 當 room 已經被解鎖時

Usage:

gameServer
  .define("chat", ChatRoom)
  .on("create", (room) => console.log("room created:", room.roomId))
  .on("dispose", (room) => console.log("room disposed:", room.roomId))
  .on("join", (room, client) => console.log(client.id, "joined", room.roomId))
  .on("leave", (room, client) => console.log(client.id, "left", room.roomId));

不鼓勵通過這些事件來操縱房間的 state。而是在您的 room handler 中使用 abstract methods

simulateLatency (milliseconds: number)

這是一種便捷的方法,適用於您希望本地測試"laggy(滯後)"客戶端的行為而不必將伺服器部署到遠端雲的情況。

// Make sure to never call the `simulateLatency()` method in production.
if (process.env.NODE_ENV !== "production") {

  // simulate 200ms latency between server and client.
  gameServer.simulateLatency(200);
}

attach (options: any)

你通常不需要呼叫它。只有在你有非常明確的理由時才使用它。

連線或建立 WebSocket server。

  • options.server:用於繫結 WebSocket 伺服器的 HTTP 伺服器。
  • options.ws:現有的可重用 WebSocket 伺服器。

Express

import express from "express";
import { Server } from "colyseus";

const app = new express();
const gameServer = new Server();

gameServer.attach({ server: app });

http.createServer

import http from "http";
import { Server } from "colyseus";

const httpServer = http.createServer();
const gameServer = new Server();

gameServer.attach({ server: httpServer });

WebSocket.Server

import http from "http";
import express from "express";
import ws from "ws";
import { Server } from "colyseus";

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({
    // your custom WebSocket.Server setup.
});

const gameServer = new Server();
gameServer.attach({ ws: wss });

listen (port: number)

將 WebSocket 伺服器繫結到指定埠。

onShutdown (callback: Function)

註冊一個應該在程式關閉之前呼叫的回撥。詳見 graceful shutdown

gracefullyShutdown (exit: boolean)

關閉所有房間並清理快取資料。當清理完成時,返回一個 promise

除非 Server 建構函式中提供了 gracefullyShutdown: false,否則該方法將被自動呼叫。

Room API (Server-side)

考慮到您已經設定了伺服器,現在是時候註冊 room handlers 並開始接受使用者的連線了。

您將定義 room handlers,以建立從 Room 擴充套件的類。

import http from "http";
import { Room, Client } from "colyseus";

export class MyRoom extends Room {
    // When room is initialized
    onCreate (options: any) { }

    // Authorize client based on provided options before WebSocket handshake is complete
    onAuth (client: Client, options: any, request: http.IncomingMessage) { }

    // When client successfully join the room
    onJoin (client: Client, options: any, auth: any) { }

    // When a client leaves the room
    onLeave (client: Client, consented: boolean) { }

    // Cleanup callback, called after there are no more clients in the room. (see `autoDispose`)
    onDispose () { }
}

Room lifecycle

這些方法對應於房間(room)的生命週期。

onCreate (options)

房間初始化後被呼叫一次。您可以在註冊房間處理程式時指定自定義初始化選項。

options 將包含您在 Server#define() 上指定的合併值 + client.joinOrCreate()client.create() 所提供的選項。

onAuth (client, options, request)

onAuth() 方法將在 onJoin() 之前執行。它可以用來驗證加入房間的客戶端的真實性。

  • 如果 onAuth() 返回一個真值,onJoin() 將被呼叫,並將返回值作為第三個引數。
  • 如果 onAuth() 返回一個假值,客戶端將立即被拒絕,導致客戶端 matchmaking 函式呼叫失敗。
  • 您還可以丟擲 ServerError 來公開要在客戶端處理的自定義錯誤。

如果不實現,它總是返回 true - 允許任何客戶端連線。

"獲取玩家的 IP 地址":您可以使用 request 變數來檢索使用者的 IP 地址、http 頭等等。例如:request.headers['x-forwarded-for'] || request.connection.remoteAddress

實現示例

async / await

import { Room, ServerError } from "colyseus";

class MyRoom extends Room {
  async onAuth (client, options, request) {
    /**
     * Alternatively, you can use `async` / `await`,
     * which will return a `Promise` under the hood.
     */
    const userData = await validateToken(options.accessToken);
    if (userData) {
        return userData;

    } else {
        throw new ServerError(400, "bad access token");
    }
  }
}

Synchronous

import { Room } from "colyseus";

class MyRoom extends Room {
  onAuth (client, options, request): boolean {
    /**
     * You can immediatelly return a `boolean` value.
     */
     if (options.password === "secret") {
       return true;

     } else {
       throw new ServerError(400, "bad access token");
     }
  }
}

Promises

import { Room } from "colyseus";

class MyRoom extends Room {
  onAuth (client, options, request): Promise<any> {
    /**
     * You can return a `Promise`, and perform some asynchronous task to validate the client.
     */
    return new Promise((resolve, reject) => {
      validateToken(options.accessToken, (err, userData) => {
        if (!err) {
          resolve(userData);
        } else {
          reject(new ServerError(400, "bad access token"));
        }
      });
    });
  }
}

客戶端示例

在客戶端,您可以使用您選擇的某些身份驗證服務(例如 Facebook )中的 token,來呼叫 matchmaking 方法(joinjoinOrCreate 等):

client.joinOrCreate("world", {
  accessToken: yourFacebookAccessToken

}).then((room) => {
  // success

}).catch((err) => {
  // handle error...
  err.code // 400
  err.message // "bad access token"
});

onJoin (client, options, auth?)

引數:

  • client: 客戶端例項。
  • options: 合併在 Server#define() 上指定的值和在 client.join() 上提供的選項。
  • auth: (可選) auth 資料返回 onAuth 方法。

當客戶端成功加入房間時,在 requestJoinonAuth 成功後呼叫。

onLeave (client, consented)

當客戶端離開房間時被呼叫。如果斷開連線是由客戶端發起的,則 consented 引數將為 true,否則為 false

你可以將這個函式定義為 async。參閱 graceful shutdown

Synchronous

onLeave(client, consented) {
    if (this.state.players.has(client.sessionId)) {
        this.state.players.delete(client.sessionId);
    }
}

Asynchronous

async onLeave(client, consented) {
    const player = this.state.players.get(client.sessionId);
    await persistUserOnDatabase(player);
}

onDispose ()

在房間被銷燬之前呼叫 onDispose() 方法,以下情況會發生:

  • 房間裡沒有更多的客戶端,並且 autoDispose 被設定為 true(預設)
  • 你手動呼叫 .disconnect()

您可以定義 async onDispose() 非同步方法,以將一些資料持久化在資料庫中。實際上,這是在比賽結束後將玩家資料保留在資料庫中的好地方。

示例 room

這個例子演示了一個實現 onCreateonJoinonMessage 方法的 room

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

// An abstract player object, demonstrating a potential 2D world position
export class Player extends Schema {
  @type("number")
  x: number = 0.11;

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

// Our custom game state, an ArraySchema of type Player only at the moment
export class State extends Schema {
  @type({ map: Player })
  players = new MapSchema<Player>();
}

export class GameRoom extends Room<State> {
  // Colyseus will invoke when creating the room instance
  onCreate(options: any) {
    // initialize empty room state
    this.setState(new State());

    // Called every time this room receives a "move" message
    this.onMessage("move", (client, data) => {
      const player = this.state.players.get(client.sessionId);
      player.x += data.x;
      player.y += data.y;
      console.log(client.sessionId + " at, x: " + player.x, "y: " + player.y);
    });
  }

  // Called every time a client joins
  onJoin(client: Client, options: any) {
    this.state.players.set(client.sessionId, new Player());
  }
}

Public methods

Room handlers 有這些方法可用。

onMessage (type, callback)

註冊一個回撥來處理客戶端傳送的訊息型別。

type 引數可以是 stringnumber

特定訊息型別的回撥

onCreate () {
    this.onMessage("action", (client, message) => {
        console.log(client.sessionId, "sent 'action' message: ", message);
    });
}

回撥所有訊息

您可以註冊一個回撥來處理所有其他型別的訊息。

onCreate () {
    this.onMessage("action", (client, message) => {
        //
        // Triggers when 'action' message is sent.
        //
    });

    this.onMessage("*", (client, type, message) => {
        //
        // Triggers when any other type of message is sent,
        // excluding "action", which has its own specific handler defined above.
        //
        console.log(client.sessionId, "sent", type, message);
    });
}

setState (object)

設定新的 room state 例項。關於 state object 的更多細節,請參見 State Handling。強烈建議使用新的 Schema Serializer 來處理您的 state

不要在 room state 下呼叫此方法進行更新。每次呼叫二進位制補丁演算法(binary patch algorithm)時都會重新設定。

你通常只會在 room handleronCreate() 期間呼叫這個方法一次。

setSimulationInterval (callback[, milliseconds=16.6])

(可選)設定可以更改遊戲狀態的模擬間隔。模擬間隔是您的遊戲迴圈。預設模擬間隔:16.6ms (60fps)

onCreate () {
    this.setSimulationInterval((deltaTime) => this.update(deltaTime));
}

update (deltaTime) {
    // implement your physics or world updates here!
    // this is a good place to update the room state
}

setPatchRate (milliseconds)

設定向所有客戶端傳送補丁狀態的頻率。預設是 50ms (20fps)

setPrivate (bool)

將房間列表設定為私有(如果提供了 false 則恢復為公開)。

Private rooms 沒有在 getAvailableRooms() 方法中列出。

setMetadata (metadata)

設定後設資料(metadata)到這個房間。每個房間例項都可以附加後設資料 — 附加後設資料的唯一目的是從客戶端獲取可用房間列表時將一個房間與另一個房間區分開來,使用 client.getAvailableRooms(),通過它的 roomId 連線到它。

// server-side
this.setMetadata({ friendlyFire: true });

現在一個房間有了附加的後設資料,例如,客戶端可以檢查哪個房間有 friendlyFire,並通過它的 roomId 直接連線到它:

// client-side
client.getAvailableRooms("battle").then(rooms => {
  for (var i=0; i<rooms.length; i++) {
    if (room.metadata && room.metadata.friendlyFire) {
      //
      // join the room with `friendlyFire` by id:
      //
      var room = client.join(room.roomId);
      return;
    }
  }
});

setSeatReservationTime (seconds)

設定一個房間可以等待客戶端有效加入房間的秒數。你應該考慮你的 onAuth() 將不得不等待多長時間來設定一個不同的座位預訂時間。預設值是 15 秒。

如果想要全域性更改座位預訂時間,可以設定 COLYSEUS_SEAT_RESERVATION_TIME 環境變數。

send (client, message)

this.send() 已經被棄用。請使用 client.send() 代替

broadcast (type, message, options?)

向所有連線的客戶端傳送訊息。

可用的選項有:

  • except: 一個 Client 例項不向其傳送訊息
  • afterNextPatch: 等到下一個補丁廣播訊息

廣播示例

向所有客戶端廣播訊息:

onCreate() {
    this.onMessage("action", (client, message) => {
        // broadcast a message to all clients
        this.broadcast("action-taken", "an action has been taken!");
    });
}

向除傳送者外的所有客戶端廣播訊息。

onCreate() {
    this.onMessage("fire", (client, message) => {
        // sends "fire" event to every client, except the one who triggered it.
        this.broadcast("fire", message, { except: client });
    });
}

只有在 state 發生變化後,才廣播訊息給所有客戶端:

onCreate() {
    this.onMessage("destroy", (client, message) => {
        // perform changes in your state!
        this.state.destroySomething();

        // this message will arrive only after new state has been applied
        this.broadcast("destroy", "something has been destroyed", { afterNextPatch: true });
    });
}

廣播一個 schema-encoded 的訊息:

class MyMessage extends Schema {
  @type("string") message: string;
}

// ...
onCreate() {
    this.onMessage("action", (client, message) => {
        const data = new MyMessage();
        data.message = "an action has been taken!";
        this.broadcast(data);
    });
}

lock ()

鎖定房間將把它從可供新客戶連線的可用房間池中移除。

unlock ()

解鎖房間將其返回到可用房間池中,以供新客戶端連線。

allowReconnection (client, seconds?)

允許指定的客戶端 reconnect 到房間。必須在 onLeave() 方法中使用。

如果提供 seconds,則在提供的秒數後將取消重新連線。

async onLeave (client: Client, consented: boolean) {
  // flag client as inactive for other users
  this.state.players[client.sessionId].connected = false;

  try {
    if (consented) {
        throw new Error("consented leave");
    }

    // allow disconnected client to reconnect into this room until 20 seconds
    await this.allowReconnection(client, 20);

    // client returned! let's re-activate it.
    this.state.players[client.sessionId].connected = true;

  } catch (e) {

    // 20 seconds expired. let's remove the client.
    delete this.state.players[client.sessionId];
  }
}

或者,您可以不提供 seconds 的數量來自動拒絕重新連線,而使用自己的邏輯拒絕它。

async onLeave (client: Client, consented: boolean) {
  // flag client as inactive for other users
  this.state.players[client.sessionId].connected = false;

  try {
    if (consented) {
        throw new Error("consented leave");
    }

    // get reconnection token
    const reconnection = this.allowReconnection(client);

    //
    // here is the custom logic for rejecting the reconnection.
    // for demonstration purposes of the API, an interval is created
    // rejecting the reconnection if the player has missed 2 rounds,
    // (assuming he's playing a turn-based game)
    //
    // in a real scenario, you would store the `reconnection` in
    // your Player instance, for example, and perform this check during your
    // game loop logic
    //
    const currentRound = this.state.currentRound;
    const interval = setInterval(() => {
      if ((this.state.currentRound - currentRound) > 2) {
        // manually reject the client reconnection
        reconnection.reject();
        clearInterval(interval);
      }
    }, 1000);

    // allow disconnected client to reconnect
    await reconnection;

    // client returned! let's re-activate it.
    this.state.players[client.sessionId].connected = true;

  } catch (e) {

    // 20 seconds expired. let's remove the client.
    delete this.state.players[client.sessionId];
  }
}

disconnect ()

斷開所有客戶端,然後銷燬。

broadcastPatch ()

"你可能不需要這個!":該方法由框架自動呼叫。

該方法將檢查 state 中是否發生了突變,並將它們廣播給所有連線的客戶端。

如果你想控制什麼時候廣播補丁,你可以通過禁用預設的補丁間隔來做到這一點:

onCreate() {
    // disable automatic patches
    this.setPatchRate(null);

    // ensure clock timers are enabled
    this.setSimulationInterval(() => {/* */});

    this.clock.setInterval(() => {
        // only broadcast patches if your custom conditions are met.
        if (yourCondition) {
            this.broadcastPatch();
        }
    }, 2000);
}

Public properties

roomId: string

一個唯一的,自動生成的,9 個字元長的 room id

您可以在 onCreate() 期間替換 this.roomId。您需要確保 roomId 是唯一的。

roomName: string

您為 gameServer.define() 的第一個引數提供的 room 名稱。

state: T

您提供給 setState()state 例項

clients: Client[]

已連線的客戶端 array。參見 Web-Socket Client

maxClients: number

允許連線到房間的最大客戶端數。當房間達到這個限制時,就會自動鎖定。除非您通過 lock() 方法明確鎖定了房間,否則一旦客戶端斷開連線,該房間將被解鎖。

patchRate: number

將房間狀態傳送到連線的客戶端的頻率(以毫秒為單位)。預設值為 50ms(20fps)

autoDispose: boolean

當最後一個客戶端斷開連線時,自動銷燬房間。預設為 true

locked: boolean (read-only)

在以下情況下,此屬性將更改:

  • 已達到允許的最大客戶端數量(maxClients
  • 您使用 lock()unlock() 手動鎖定或解鎖了房間

clock: ClockTimer

一個 ClockTimer 例項,用於 timing events

presence: Presence

presence 例項。檢視 Presence API 瞭解更多資訊。

Web-Socket Client

client 例項存在於:

  • Room#clients
  • Room#onJoin()
  • Room#onLeave()
  • Room#onMessage()

這是來自 ws 包的原始 WebSocket 連線。有更多可用的方法,但不鼓勵與 Colyseus 一起使用。

Properties

sessionId: string

每個會話唯一的 id。

在客戶端,你可以在 room 例項中找到 sessionId

auth: any

onAuth() 期間返回的自定義資料。

Methods

send(type, message)

傳送一種 message 型別的訊息到客戶端。訊息是用 MsgPack 編碼的,可以儲存任何 JSON-seriazeable 的資料結構。

type 可以是 stringnumber

傳送訊息:

//
// sending message with a string type ("powerup")
//
client.send("powerup", { kind: "ammo" });

//
// sending message with a number type (1)
//
client.send(1, { kind: "ammo"});

leave(code?: number)

強制斷開 clientroom 的連線。

這將在客戶端觸發 room.onLeave 事件。

error(code, message)

將帶有 codemessageerror 傳送給客戶端。客戶端可以在 onError 上處理它。

對於 timing events,建議從您的 Room 例項中使用 this.clock 方法。

所有的間隔和超時註冊在 this.clock
Room 被清除時,會自動清除。

內建的 setTimeout
setInterval 方法依賴於 CPU 負載,這可能會延遲到意想不到的執行時間。

Clock

clock 是一種有用的機制,用於對有狀態模擬之外的事件進行計時。一個例子可以是:當玩家收集道具時,你可能會計時。您可以 clock.setTimeout 建立一個新的可收集物件。使用 clock. 的一個優點。您不需要關注 room 更新和增量,而可以獨立於房間狀態關注事件計時。

Public methods

注意:time 引數的單位是毫秒

clock.setInterval(callback, time, ...args): Delayed

setInterval() 方法重複呼叫一個函式或執行一個程式碼片段,每次呼叫之間有固定的時間延遲。
它返回標識間隔的 Delayed 例項,因此您可以稍後對它進行操作。

clock.setTimeout(callback, time, ...args): Delayed

setTimeout() 方法設定一個 timer,在 timer 過期後執行一個函式或指定的程式碼段。它返回標識間隔的 Delayed 例項,因此您可以稍後對它進行操作。

示例

這個 MVP 示例顯示了一個 RoomsetInterval()setTimeout 和清除以前儲存的型別 Delayed 的例項; 以及顯示 Room's clock 例項中的 currentTime。在1秒鐘的'Time now ' + this.clock.currentTimeconsole.log 之後,然後10秒鐘之後,我們清除間隔:this.delayedInterval.clear();

// Import Delayed
import { Room, Client, Delayed } from "colyseus";

export class MyRoom extends Room {
    // For this example
    public delayedInterval!: Delayed;

    // When room is initialized
    onCreate(options: any) {
        // start the clock ticking
        this.clock.start();

        // Set an interval and store a reference to it
        // so that we may clear it later
        this.delayedInterval = this.clock.setInterval(() => {
            console.log("Time now " + this.clock.currentTime);
        }, 1000);

        // After 10 seconds clear the timeout;
        // this will *stop and destroy* the timeout completely
        this.clock.setTimeout(() => {
            this.delayedInterval.clear();
        }, 10_000);
    }
}

clock.clear()

清除 clock.setInterval()clock.setTimeout() 中註冊的所有間隔和超時。

clock.start()

開始計時。

clock.stop()

停止計時。

clock.tick()

在每個模擬間隔步驟都會自動呼叫此方法。在 tick 期間檢查所有 Delayed 例項。

參閱 Room#setSimiulationInterval() 瞭解更多資訊。

Public properties

clock.elapsedTime

呼叫 clock.start() 方法後經過的時間(以毫秒為單位)。只讀的。

clock.currentTime

當前時間(毫秒)。只讀的。

clock.deltaTime

上一次和當前 clock.tick() 呼叫之間的毫秒差。只讀的。

Delayed

建立延遲的例項

clock.setInterval() or clock.setTimeout()

Public methods

delayed.pause()

暫停特定的 Delayed 例項的時間。(elapsedTime.resume() 被呼叫之前不會增加。)

delayed.resume()

恢復特定 Delayed 例項的時間。(elapsedTime 將繼續正常增長)

delayed.clear()

清除超時時間或間隔。

delayed.reset()

重置經過的時間(elapsed time)。

Public properties

delayed.elapsedTime: number

Delayed 例項的執行時間,以毫秒為單位。

delayed.active: boolean

如果 timer 仍在執行,返回 true

delayed.paused: boolean

如果計時器通過 .pause() 暫停,則返回 true

Match-maker API

"您可能不需要這個!"
本節用於高階用途。通常使用 client-side methods 比較好。如果您認為您不能通過客戶端方法實現您的目標,您應該考慮使用本頁面中描述的方法。

下面描述的方法由 matchMaker 單例提供,可以從 "colyseus" 包中匯入:

import { matchMaker } from "colyseus";
const matchMaker = require("colyseus").matchMaker;

.createRoom(roomName, options)

建立一個新房間

引數:

  • roomName: 您在 gameServer.define() 上定義的識別符號。
  • options: onCreate 的選項。
const room = await matchMaker.createRoom("battle", { mode: "duo" });
console.log(room);
/*
  { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
*/

.joinOrCreate(roomName, options)

加入或建立房間並返回客戶端位置預訂。

引數:

  • roomName: 您在 gameServer.define() 上定義的識別符號。
  • options: 客戶端位置預訂的選項(如 onJoin/onAuth)。
const reservation = await matchMaker.joinOrCreate("battle", { mode: "duo" });
console.log(reservation);
/*
  {
    "sessionId": "zzzzzzzzz",
    "room": { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
  }
*/

"消費位置預訂":您可以使用 consumeSeatReservation() 從客戶端開始通過預訂位置加入房間。

.reserveSeatFor(room, options)

在房間(room)裡為客戶端(client)預訂位置。

"消費位置預訂":您可以使用 consumeSeatReservation() 從客戶端開始通過預訂位置加入房間。

引數:

  • room: 房間資料 (結果來自 createRoom() 等)
  • options: onCreate 選項
const reservation = await matchMaker.reserveSeatFor("battle", { mode: "duo" });
console.log(reservation);
/*
  {
    "sessionId": "zzzzzzzzz",
    "room": { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
  }
*/

.join(roomName, options)

加入房間並返回位置預訂。如果沒有可用於 roomName 的房間,則丟擲異常。

引數:

  • roomName: 您在 gameServer.define() 上定義的識別符號。
  • options: 客戶端位置預訂的選項(用於 onJoin/onAuth
const reservation = await matchMaker.join("battle", { mode: "duo" });
console.log(reservation);
/*
  {
    "sessionId": "zzzzzzzzz",
    "room": { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
  }
*/

"消費位置預訂":您可以使用 consumeSeatReservation() 從客戶端開始通過預訂位置加入房間。

.joinById(roomId, options)

id 加入房間並返回客戶端位置預訂。如果沒有為 roomId 找到 room,則會引發異常。

引數:

  • roomId: 特定 room 例項的 ID
  • options: 客戶端位置預訂的選項(用於 onJoin/onAuth
const reservation = await matchMaker.joinById("xxxxxxxxx", {});
console.log(reservation);
/*
  {
    "sessionId": "zzzzzzzzz",
    "room": { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
  }
*/

"消費位置預訂":您可以使用 consumeSeatReservation()從客戶端開始通過預訂位置加入房間。

.create(roomName, options)

建立一個新的房間並返回客戶端位置預訂。

引數:

  • roomName: 你在 gameServer.define() 上定義的識別符號。
  • options: 客戶端位置預訂的選項(用於 onJoin/onAuth
const reservation = await matchMaker.create("battle", { mode: "duo" });
console.log(reservation);
/*
  {
    "sessionId": "zzzzzzzzz",
    "room": { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
  }
*/

"消費位置預訂":您可以使用 consumeSeatReservation()從客戶端開始通過預訂位置加入房間。

.query(conditions)

對快取的房間執行查詢。

const rooms = await matchMaker.query({ name: "battle", mode: "duo" });
console.log(rooms);
/*
  [
    { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false },
    { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false },
    { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
  ]
*/

.findOneRoomAvailable(roomName, options)

尋找一個可用公開的和沒上鎖的房間

引數:

  • roomId: 特定 room 例項的 ID
  • options: 客戶端位置預訂的選項(用於 onJoin/onAuth
const room = await matchMaker.findOneRoomAvailable("battle", { mode: "duo" });
console.log(room);
/*
  { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
*/

.remoteRoomCall(roomId, method, args)

在遠端 room 中呼叫一個方法或返回一個屬性。

引數:

  • roomId: 特定 room 例項的 ID
  • method: 要呼叫或檢索的方法或屬性。
  • args: 引數陣列。
// call lock() on a remote room by id
await matchMaker.remoteRoomCall("xxxxxxxxx", "lock");

Presence

當需要在多個程式和/或機器上擴充套件伺服器時,需要向 Server 提供 Presence 選項。Presence 的目的是允許不同程式之間通訊和共享資料,特別是在配對(match-making)過程中。

  • LocalPresence (default)
  • RedisPresence

每個 Room 處理程式上也可以使用 presence 例項。您可以使用它的 API 來持久化資料,並通過 PUB/SUB 在房間之間通訊。

LocalPresence

這是預設選項。它用於在單個程式中執行 Colyseus 時使用。

RedisPresence (clientOpts?)

當您在多個程式和/或機器上執行 Colyseus 時,請使用此選項。

Parameters:

import { Server, RedisPresence } from "colyseus";

// This happens on the slave processes.
const gameServer = new Server({
    // ...
    presence: new RedisPresence()
});

gameServer.listen(2567);
const colyseus = require('colyseus');

// This happens on the slave processes.
const gameServer = new colyseus.Server({
    // ...
    presence: new colyseus.RedisPresence()
});

gameServer.listen(2567);

API

Presence API 高度基於 Redis 的 API,這是一個鍵值資料庫。

每個 Room 例項都有一個 presence 屬性,該屬性實現以下方法:

subscribe(topic: string, callback: Function)

訂閱給定的 topic。每當在 topic 上訊息被髮布時,都會觸發 callback

unsubscribe(topic: string)

退訂給定的topic

publish(topic: string, data: any)

將訊息釋出到給定的 topic

exists(key: string): Promise<boolean>

返回 key 是否存在的布林值。

setex(key: string, value: string, seconds: number)

設定 key 以保留 string 值,並將 key 設定為在給定的秒數後超時。

get(key: string)

獲取 key 的值。

del(key: string): void

刪除指定的 key

sadd(key: string, value: any)

將指定的成員新增到儲存在 keyset 中。已經是該 set 成員的指定成員將被忽略。如果 key 不存在,則在新增指定成員之前建立一個新 set

smembers(key: string)

返回儲存在 key 中的 set 值的所有成員。

sismember(member: string)

如果 member 是儲存在 key 處的 set 的成員,則返回

Return value

  • 1 如果元素是 set 中的元素。
  • 0 如果元素不是 set 的成員,或者 key 不存在。

srem(key: string, value: any)

key 處儲存的 set 中刪除指定的成員。不是該 set 成員的指定成員將被忽略。如果 key 不存在,則將其視為空set,並且此命令返回 0

scard(key: string)

返回 key 處儲存的 setset 基數(元素數)。

sinter(...keys: string[])

返回所有給定 set 的交集所得的 set 成員。

hset(key: string, field: string, value: string)

key 儲存在 hash 中的欄位設定為 value。如果 key 不存在,則建立一個包含 hash 的新 key。如果欄位已經存在於 hash 中,則將覆蓋該欄位。

hincrby(key: string, field: string, value: number)

以增量的方式遞增儲存在 key 儲存的 hash 中的欄位中儲存的數字。如果 key 不存在,則建立一個包含 hash 的新 key。如果欄位不存在,則在執行操作前將該值設定為 0

hget(key: string, field: string): Promise<string>

返回與儲存在 key 處的 hash 中的 field 關聯的值。

hgetall(key: string): Promise<{[field: string]: string}>

返回儲存在 key 處的 hash 的所有欄位和值。

hdel(key: string, field: string)

從儲存在 key 處的 hash 中刪除指定的欄位。該 hash 中不存在的指定欄位將被忽略。如果 key 不存在,則將其視為空 hash,並且此命令返回 0

hlen(key: string): Promise<number>

返回 key 處儲存的 hash 中包含的欄位數

incr(key: string)

將儲存在 key 值上的數字加 1。如果 key 不存在,則將其設定為 0,然後再執行操作。如果 key 包含錯誤型別的值或包含不能表示為整數的字串,則返回錯誤。該操作僅限於 64 位有符號整數。

decr(key: string)

將儲存在 key 中的數字減 1。如果 key 不存在,則將其設定為 0,然後再執行操作。如果 key 包含錯誤型別的值或包含不能表示為整數的字串,則返回錯誤。該操作僅限於 64 位有符號整數。

Graceful Shutdown

Colyseus 預設提供優雅的關閉機制。這些操作將在程式殺死自己之前執行:

  • 非同步斷開所有已連線的客戶端 (Room#onLeave)
  • 非同步銷燬所有生成的房間 (Room#onDispose)
  • 在關閉程式 Server#onShutdown 之前執行可選的非同步回撥

如果您要在 onLeave / onDispose 上執行非同步任務,則應返回 Promise,並在任務準備就緒時 resolve 它。 onShutdown(callback) 也是如此。

Returning a Promise

通過返回一個 Promise,伺服器將在殺死 worker 程式之前等待它們完成。

import { Room } from "colyseus";

class MyRoom extends Room {
    onLeave (client) {
        return new Promise((resolve, reject) => {
            doDatabaseOperation((err, data) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(data);
                }
            });
        });
    }

    onDispose () {
        return new Promise((resolve, reject) => {
            doDatabaseOperation((err, data) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(data);
                }
            });
        });
    }
}

使用 async

async 關鍵字將使函式在底層返回一個 Promise閱讀更多關於Async / Await的內容

import { Room } from "colyseus";

class MyRoom extends Room {
    async onLeave (client) {
        await doDatabaseOperation(client);
    }

    async onDispose () {
        await removeRoomFromDatabase();
    }
}

程式關閉回撥

你也可以通過設定 onShutdown 回撥來監聽程式關閉。

import { Server } from "colyseus";

let server = new Server();

server.onShutdown(function () {
    console.log("master process is being shut down!");
});

Refs

中文手冊同步更新在:

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

相關文章