Client Engine 开发指南 · Node.js
请先阅读 Client Engine 快速入门 · Node.js 及你的第一个 Client Engine 小游戏来初步了解如何使用初始项目来开发游戏。本文档将在初始项目的基础上深入讲解 Client Engine SDK。
Client Engine 初始项目依赖了专门的 Client Engine SDK, Client Engine SDK 在多人在线对战 SDK 的基础上进行了封装,帮助您更好的撰写服务端游戏逻辑。您可以通过快速入门安装依赖。
组件
SDK 提供以下组件:
Game
:负责房间内游戏的具体逻辑。Client Engine 维护了许多游戏房间,每一个游戏房间都是一个 Game 实例,即每个 Game 实例对应一个唯一的 Play Room 与 MasterClient。游戏房间内的逻辑由 Game 中的代码来控制,因此房间内的游戏逻辑必须继承自该类。GameManager
:负责创建、管理及分配具体的 Game 对象。Game 的管理及销毁由 SDK 负责,不需要您再自己额外写代码。
GameManager
GameManager 实例化
GameManager
会帮您自动创建、管理并销毁 Game,因此在项目启动时,您需要实例化 GameManager
。相关示例代码如下:
自定义 GameManager
首先需要自定义一个 Class 继承自 GameManager
,例如示例代码中的 SampleGameManager
:
import { Game, GameManager, ICreateGameOptions } from "@leancloud/client-engine";
export default class SampleGameManager<T extends Game> extends GameManager<T> {
}
在 GameManager 中自定义方法
Client Engine 的核心用法之一是负责创建 Game 并返回 roomName 给客户端,因此在 SampleGameManager
这个类中,我们需要撰写创建 Game
的方法提供给 Web API 使用。例如示例项目中的快速开始和创建新游戏。这里的示例代码我们以创建新游戏为例:
import { Game, GameManager, ICreateGameOptions } from "@leancloud/client-engine";
export default class SampleGameManager<T extends Game> extends GameManager<T> {
/**
* 创建一个新的游戏。
* @param playerId 预约的玩家 ID
* @param options 创建新游戏时可以指定的一些配置项
* @return 创建的游戏的房间 name
*/
public async createGameAndGetName(playerId: string, options?: ICreateGameOptions) {
const game = await this.createGame(playerId, options);
return game.room.name;
}
}
写完自定义方法之后,在下文我们还需要为这里的方法配置负载均衡,需要注意的是,受负载均衡系统的要求,GameManager
子类中的 public
方法,其参数与返回值必须是 string
、number
、boolean
、null
、Object
、Array
中的一种。以上代码中可以看到 GameManager
的 createGame()
方法返回的是一个 Game
,不符合负载均衡的要求,因此我们在这里封装为自己的方法 createGameAndGetName()
。
创建 GameManager 子类对象
接下来创建 GameManager
的子类对象,在创建 SampleGameManager
的时候,需要在第一个参数内传入[自定义的 Game ](#实现自己的 Game),这里使用的是示例 Demo 猜拳游戏中的 RPSGame
。
import PRSGame from "./rps-game";
const gameManager = new SampleGameManager(
gameConstructor: PRSGame,
appId: {{appid}},
appKey: {{appkey}},
playServer: "https://please-replace-with-your-customized.domain.com",
concurrency: 2,
);
设置负载均衡
GameManager
需要配置负载均衡,以确保 GameManager
创建的 Game
能够尽可能均匀分配到每一个 Client Engine 实例中。负载均衡的详细文档请看下文,在这里,我们先讲解如何配置。
在这里我们创建一个负载均衡对象,然后将上面的 gameManager
绑定到负载均衡中:
import { ICreateGameOptions,LoadBalancerFactory } from "@leancloud/client-engine";
// 创建负责负载均衡的对象,请不要更改,使用时复制粘贴即可
const loadBalancerFactory = new LoadBalancerFactory({
poolId: `${APP_ID.slice(0, 5)}-${process.env.LEANCLOUD_APP_ENV || "development"}`,
redisUrl: process.env.REDIS_URL__CLIENT_ENGINE,
});
// 将 reception 及我们自定义的方法 makeReservation 配置负载均衡。
const loadBalancer = loadBalancerFactory.bind(gameManager, ["createGameAndGetName"]);
loadBalancerFactory
的 bind()
方法中,第一个参数是 gameManager
对象,第二个参数是一个数组,传入需要进行负载均衡的方法名 ["createGameAndGetName"]
。
到这里,gameManager
的配置就完成了,您可以在自己定义的 Web API 处这样调用相关方法: gameManager.createGameAndGetName()
。
创建房间
在 GameManager 实例化这一节中,我们在子类中使用了 GameManager
的 createGame()
来创建房间。
createGame()
接受以下参数:
- playerId:发起请求的客户端在多人对战服务中的 userId。
- createGameOptions(可选):创建指定条件的房间。
- roomName(可选):创建指定 roomName 的房间。例如您需要和好友一起玩时,可以用这个接口创建房间后,把 roomName 分享给好友。如果您不关心 roomName,可以不指定这个参数。
- roomOptions(可选):通过这个参数,客户端在请求 Client Engine 创建房间时,可以设置
customRoomProperties
,customRoomPropertyKeysForLobby
,visible
,对这三个参数的说明请参考创建房间。 - seatCount(可选):创建房间时,指定本次游戏需要多少人,这个值需要在设置房间内玩家数量的
minSeatCount
和maxSeatCount
之间,否则 Client Engine 会拒绝创建房间。如果不指定,则以defaultSeatCount
为准。
例如创建一个带有匹配条件的新房间时,可以这样调用 createGame()
:
// 您可以从客户端发来的请求中获得 playerId 和 createGameOptions
const props = {
level: 2,
};
const roomOptions = {
customRoomPropertyKeysForLobby: ['level'],
customRoomProperties: props,
};
const createGameOptions = {
roomOptions
};
gameManager.createGame(playerId, createGameOptions);
在你的第一个 Client Engine 小游戏中,reception.ts
中使用 createGame()
撰写了两个自定义方法用于「快速开始」和「创建新游戏」,并通过 index.ts
中的 Web API /reservation
和 /game
来调用相关逻辑,如果没有自定义需求您可以直接使用示例 Demo 中的接口并传入以上参数。
获取当前可用的房间
GameManager 提供了 getAvailableGames()
方法来获取当前 GameManager 对象所在的 Client Engine 实例中的可用游戏列表。这里的可用指的是房间还有空位。使用示例代码如下:
var games = gameManager.getAvailableGames();
需要注意的是,这个方法获取的不是多人对战服务中所有的可用房间,仅限于当前所在 Client Engine 实例中的可用房间,Client Engine 多实例负载均衡请参考负载均衡。
匹配
GameManager
暂时没有提供匹配机制,如果客户端只需要随机加入某个房间,可以参考示例项目中「快速开始」的实现方案。这个实现方案会在负载最低的实例中寻找可用房间或创建房间,最终返回给客户端一个可加入的房间名称。
如果您希望实现有条件的匹配,可以这样实现:
- 客户端向多人对战服务请求有条件的匹配,如果有空余的房间,则会触发加入成功事件。
- 如果的多人对战服务此时没有空余的房间,客户端会收到「加入房间失败」事件,在这个事件中,发现错误码是 4301,则向 Client Engine 请求创建房间。
- Client Engine 收到请求后创建房间并返回 roomName 给客户端。这一部分的逻辑可以使用示例项目中的
/game
入口。 - 客户端拿到 Client Engine 返回的 roomName 后加入房间,等待其他人匹配加入。
这个流程在客户端中的示例代码如下(非 Client Engine):
客户端首先向多人对战服务发起有条件的加入房间请求:
const matchProps = {level: 2};
play.joinRandomRoom({matchProperties: matchProps});
如果多人对战服务此时有可以加入的新房间,您会自动加入到新房间中,并触发加入房间成功事件:
play.on(Event.ROOM_JOINED, () => {
// TODO 可以做跳转场景之类的操作
});
如果没有可以加入的房间,会触发加入房间失败事件。在这个事件中,4301 错误码代表着没有可以加入的空房间,此时我们向 Client Engine 请求创建一个新的房间,获得新房间的 roomName 后加入新房间:
// 加入房间失败后请求 Client Engine 创建房间
play.on(Event.ROOM_JOIN_FAILED, (error) => {
if (error.code === 4301) {
// 设置创建带有匹配属性的房间
const props = {level: 2};
const options = {customRoomPropertyKeysForLobby: ['level']};
// 这里通过 HTTP 调用在 Client Engine 中实现的 `/game` 接口
const { roomName } = await (await fetch(
`${CLIENT_ENGINE_SERVER}/game`,
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
playerId: play.userId,
options
})
}
)).json();
// 加入房间
return play.joinRoom(roomName);
} else {
console.log(error);
}
});
Game
Game 生命周期
- 创建:
Game
由 SDK 中的GameManager
管理,GameManager
会在收到创建房间的请求时根据情况创建Game
。 - 运行: 创建后,
Game
的控制权从 SDK 中GameManager
移交给Game
本身。从这个时刻开始,会有玩家陆续加入游戏房间。 - 销毁: 所有玩家离开房间后,意味着游戏结束,
Game
将控制权交回GameManager
,GameManager
做最后的清理工作,包括断开并销毁该房间的 masterClient、将Game
从管理的游戏列表中删除等。
Game 通用属性
在实现游戏逻辑的过程中,Game
类提供下面这些属性来简化常见需求的实现,您可以在继承 Game
的自己的类中方便的获得以下属性:
room
属性:游戏对应的房间,这是一个 Play SDK 中的 Room 实例。masterClient
属性:游戏对应的 masterClient,这是一个 Play SDK 中的 Play 实例。players
属性:不包含 masterClient 的玩家列表。注意,如果您通过 Play SDK Room 实例的playerList
属性获取的房间成员列表是包括 masterClient 的。
Game 通用方法
Game 类在多人对战 SDK 的基础上封装了以下方法,使得 MasterClient 可以更便利的发送自定义事件:
broadcast()
方法:向所有玩家广播自定义事件。示例代码请参考广播自定义事件。forwardToTheRests()
方法:将一个玩家发送的自定义事件转发给其他玩家。示例代码请参考转发自定义事件。
实现自己的 Game
实现自己的房间内游戏逻辑时,您需要创建一个继承自 Game
的类来撰写自己的游戏逻辑,示例方法如下:
import { Game } from "@leancloud/client-engine";
export default class SampleGame extends Game {
constructor(room: Room, masterClient: Play) {
super(room, masterClient);
}
}
设置房间内玩家数量
这里的玩家数量指的是不包括 MasterClient 的玩家数量,根据多人对战服务的限制,最多不能超过 9 个人。
在 Game
中需要指定 defaultSeatCount
静态属性作为默认的玩家数量,Client Engine 会根据这个值向多人对战服务请求创建房间。例如斗地主需要 3 个人才能玩,可以这样设置:
export default class SampleGame extends Game {
public static defaultSeatCount = 3; // 最大不能超过 9
}
如果您的游戏需要的玩家数量在某个范围内,除了设置 defaultSeatCount
外,还需要使用 minSeatCount
静态属性限定最小玩家数量,maxSeatCount
静态属性设定最大玩家数量。例如三国杀要求至少 2 个人,最多 8 个人才能玩,默认 5 个人可以玩,可以这样设置:
export default class SampleGame extends Game {
public static minSeatCount = 2;
public static maxSeatCount = 8; // 最大不能超过 9
public static defaultSeatCount = 5;
}
在创建房间的接口中,可以将客户端 request 请求中的 seatCount
参数来动态覆盖掉 defaultSeatCount
。
当房间人数达到 seatCount
时,您可以选择配置触发房间人满事件,如果您的客户端没有指定 seatCount
,人满事件时将以 defaultSeatCount
的值为准。
加入房间事件
当客户端成功加入房间后,位于 Client Engine 的 MasterClient 会收到新玩家加入事件,如果您需要监听此事件,可以在自定义的 Game
中的 constructor()
方法中撰写监听的代码:
import { Game } from "@leancloud/client-engine";
export default class SampleGame extends Game {
constructor(room: Room, masterClient: Play) {
super(room, masterClient);
this.masterClient.on(Event.PLAYER_ROOM_JOINED, () => {
console.log('有人来了');
});
}
}
房间人满事件
当房间的人数满足设置房间内玩家数量的人满逻辑时,watchRoomFull
装饰器会让您收到 Game 抛出的 AutomaticGameEvent.ROOM_FULL
事件,您可以在这个事件中撰写相应的游戏逻辑,例如关闭房间,向客户端广播游戏开始:
import { AutomaticGameEvent, Game, watchRoomFull } from "@leancloud/client-engine";
enum Event {
GameStart = 15,
};
@watchRoomFull()
export default class SampleGame extends Game {
constructor(room: Room, masterClient: Play) {
super(room, masterClient);
// 监听 ROOM_FULL 事件,收到此事件后调用 `start() 方法`
this.once(AutomaticGameEvent.ROOM_FULL, this.start);
}
protected start = async () => {
// 在这里撰写自己房间人满后的逻辑
// 标记房间不再可加入
this.masterClient.setRoomOpened(false);
// 向客户端广播游戏开始事件
this.broadcast(Event.GameStart);
}
}
广播自定义事件
在房间人满事件中,Game
向房间内所有成员广播了游戏开始:
enum Event {
GameStart = 15,
};
this.broadcast(Event.GameStart);
在广播事件时您还可以带有一些数据:
enum Event {
GameStart = 15,
};
const gameData = {someGameData};
this.broadcast(Event.GameStart, gameData);
此时客户端的接收自定义事件方法会被触发,如果发现是 game-start
事件,客户端可以在 UI 上展示对战开始。
转发自定义事件
MasterClient 可以转发某个客户端发来的事件给其他客户端,在转发时还可以处理数据:
enum Event {
SomeEvent = 15,
};
this.forwardToTheRests(event, (eventData) => {
// 准备要转发的数据
const actUserId = event.senderId;
const result = {actUserId};
return result;
// Event.SomeEvent 是自定义事件的 ID,如果省略则使用原 event 事件的 ID
}, Event.SomeEvent)
在这个代码中,event
参数是某个客户端发来的原始事件,eventData
是原始事件的数据,您可以在转发事件给其他客户端时处理该数据,例如抹去或增加一些信息。MasterClient 发送该事件后,客户端的接收自定义事件会被触发。
MasterClient 与客户端通信
除了上方初始项目提供的广播自定义事件及转发自定义事件外,您依然可以使用多人对战服务中的自定义属性、自定义事件进行通信。
除此之外,Game
还提供了以下 RxJS 方法方便您对事件进行流处理,进而精简自己的代码及逻辑:
getStream()
方法:获取玩家发送的自定义事件的流,这是一个 RxJS 中的 Observable 对象。接口说明请参考 API 文档。takeFirst()
方法:获取玩家发送的指定条件的从现在开始算的第一条自定义事件的流,返回一个 RxJS 中的 Observable 对象。接口说明请参考 API 文档。
注意,以上两个方法需要您了解 RxJS 才能使用,如果您不了解 RxJS,依然可以使用多人对战服务中的事件方法进行通信。
游戏结束
当所有玩家都离开后,GameManager
会自动帮您销毁当前房间及相关的 MasterClient。此时如果您没有其他的逻辑要做,则不需要关心本节文档。如果您希望自己做一些清理工作,例如保存用户数据等,可以使用 autoDestroy
装饰器,这个装饰器会在所有玩家离开后自动触发 Game
子类中的 destroy()
方法,您可以将相关逻辑写在这个方法中。
import { autoDestroy, Game } from "@leancloud/client-engine";
@autoDestroy()
export default class SampleGame extends Game {
protected destroy() {
super.destroy();
console.log('在这里可以做额外的清理工作');
}
}
负载均衡
Client Engine 会根据整体实例负载的高低自动对实例数量进行调整。
在 Client Engine 中,需要负载均衡的情况有两种:第一种是客户端通过 REST API 发起的请求,第二种是每一个实例运行的 Game
数量的负载。对于客户端通过 REST API 发起的请求,Client Engine 会自动将请求均匀的分配给当前的所有实例,不需要我们再做任何配置工作。对于第二种情况,每个 Game
对象(每局游戏)一般都会持续存在一段时间,为了让每个实例承载的 Game
对象尽可能达到均衡,我们需要额外配置 GameManager
到负载均衡系统中。
这个特性由 SDK 提供的 LoadBalancerFactory
类实现。在 [GameManager 实例化](#GameManager 实例化)中我们可以看到,LoadBalancerFactory
通过绑定 gameManager
生成一个 LoadBalancer
的对象,每一个 Client Engine 实例中都会有这样一个对象。
当 Client Engine 的某个实例接收到来自客户端的 REST API 请求,并调用 gameManager
中的方法时,接收请求的实例中的负载均衡节点 LoadBalancer
会找出集群中承载 Game
数量最小的实例,将指定的 gameManager
的 API 调用转发给该实例的 gameManager
运行并将结果返回。在这里,LoadBalancer
只负责请求的转发,不关心如何处理请求。
API 文档
您可以在 API 文档中找到更多 SDK 的类、方法及属性说明,点击查看 Client Engine SDK API 文档。