ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(中)_游戏服务器开发

快速上手多人游戏服务器开发。后续会基于 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.authorized 或 req.connection.encrypted 被设置。
  • next (Function) 用户必须在检查 info 字段后调用该回调。此回调中的参数为:

    • result (Boolean) 是否接受握手。
    • code (Number) 当 result 为 false 时,该字段决定发送给客户端的 HTTP 错误状态码。
    • name (String) 当 result 为 false 时,该字段决定 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{
    /**
     * 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 方法(join,joinOrCreate 等):

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 方法。

当客户端成功加入房间时,在 requestJoin 和 onAuth 成功后调用。

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

这个例子演示了一个实现 onCreate,onJoin 和 onMessage 方法的 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();
}

export class GameRoom extends Room{
  // 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 参数可以是 string 或 number。

特定消息类型的回调

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 handler 的 onCreate() 期间调用这个方法一次。

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 可以是 string 或 number。

发送消息:

//
// 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)

强制断开 client 与 room 的连接。

这将在客户端触发 room.onLeave 事件。

error(code, message)

将带有 code 和 message 的 error 发送给客户端。客户端可以在 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 示例显示了一个 Room:setInterval(),setTimeout 和清除以前存储的类型 Delayed 的实例; 以及显示 Room's clock 实例中的 currentTime。在1秒钟的'Time now ' + this.clock.currentTime 被console.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:

  • clientOpts: Redis 客户端选项(host/credentials)。查看选项的完整列表。
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

返回 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)

将指定的成员添加到存储在 key 的 set 中。已经是该 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 处存储的 set 的 set 基数(元素数)。

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

返回与存储在 key 处的 hash 中的 field 关联的值。

hgetall(key: string): Promise

返回存储在 key 处的 hash 的所有字段和值。

hdel(key: string, field: string)

从存储在 key 处的 hash 中删除指定的字段。该 hash 中不存在的指定字段将被忽略。如果 key 不存在,则将其视为空 hash,并且此命令返回 0。

hlen(key: string): Promise

返回 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!");
});