Client Engine Developer Guide - Node.js
Please read Client Engine Quick Start - Node.js and Your First Client Engine Game to get an initial idea of how to develop a game using a starter project. This document will build on the initial project to explain the Client Engine SDK in depth.
The Client Engine starter project relies on the Client Engine SDK, which is a wrapper around the Multiplayer SDK to help you write server-side game logic. You can install the dependencies by following the quickstart guide.
Components
The SDK provides the following components:
Game
: Responsible for the specific logic of the game in the room. Client Engine maintains a number of game rooms, each of which is a Game instance, i.e., each Game instance corresponds to a unique Play Room and MasterClient. Logic in the game rooms is controlled by the code in the Game, so the logic of the game in the room must be inherited from this class.GameManager
: Responsible for creating, managing and distributing specific Game objects. The management and destruction of Game are handled by the SDK, you don't need to write additional code.
GameManager
GameManager instantiation
GameManager
will help you to create, manage and destroy Game automatically, so you need to instantiate GameManager
when your project starts. The sample code is shown below:
Customising GameManager
First of all, you need to customise a Class inherited from GameManager
, such as SampleGameManager
in the sample code:
import { Game, GameManager, ICreateGameOptions } from "@leancloud/client-engine";
export default class SampleGameManager<T extends Game> extends GameManager<T> {
}
Custom Methods in GameManager
One of the core uses of the client engine is to create a Game and return the roomName to the client, so in the SampleGameManager
class we need to write a method to create a Game
for the web API to use. Like quick start and create new game in the example project. Here's the sample code we'll use to create a new game:
import { Game, GameManager, ICreateGameOptions } from "@leancloud/client-engine";
export default class SampleGameManager<T extends Game> extends GameManager<T> {
/**
* Creates a new game.
* @param playerId The player ID of the reservation.
* @param options Some configuration items that can be specified when creating a new game.
* @return The room name of the game being created.
*/
public async createGameAndGetName(playerId: string, options?: ICreateGameOptions) {
const game = await this.createGame(playerId, options);
return game.room.name;
}
}
After writing the custom method, later we also need to configure load balancing for the method here. It should be noted that by the requirement of load balancing system, public
methods in GameManager
subclass must have their parameters and return values be string
, number
, boolean
, null
, Object
, or Array
. In the above code, we can see that the createGame()
method of GameManager
returns a Game
, which does not meet the requirement of load balancing, so we encapsulate it into our own method createGameAndGetName()
here.
Creating GameManager Subclass Objects
Next, create a subclass of GameManager
. When creating SampleGameManager
, you need to pass Custom Game in the first parameter. Here we use Sample Demo of the guessing game 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,
);
Setting up load balancing
The GameManager
needs to be configured for load balancing to ensure that the Game
objects created by the GameManager
are distributed as evenly as possible to each Client Engine instance. See below for detailed documentation on load balancing. Here we will start by explaining how to configure it.
Here we will create a load balancing object and bind the above gameManager
to it:
import { ICreateGameOptions,LoadBalancerFactory } from "@leancloud/client-engine";
// Create the object responsible for load balancing. Don't change it; just copy and paste it when you use it.
const loadBalancerFactory = new LoadBalancerFactory({
poolId: `${APP_ID.slice(0, 5)}-${process.env.LEANCLOUD_APP_ENV || "development"}`,
redisUrl: process.env.REDIS_URL__CLIENT_ENGINE,
});
// Configure load balancing with reception and our custom method makeReservation.
const loadBalancer = loadBalancerFactory.bind(gameManager, ["createGameAndGetName"]);
In the bind()
method of loadBalancerFactory
, the first parameter is the gameManager
and the second parameter is an array containing the names of the methods that need load balancing, like ["createGameAndGetName"]
.
At this point, the configuration of the gameManager
is complete, and you can call the relevant method at your own defined Web API like this: gameManager.createGameAndGetName()
.
Creating a Room
In the section GameManager Instantiation, we used createGame()
of GameManager
in a subclass to create a room.
createGame()
accepts the following parameters:
- playerId: userId of the client initiating the request in the Multiplayer service.
- createGameOptions (optional): create the room with the specified conditions.
- roomName (optional): create the room with the specified roomName. For example, if you need to play with your friends, you can use this interface to create a room and then share the roomName with your friends. If you don't care about roomName, you can leave it out.
- roomOptions (optional): with this parameter, the client can set
customRoomProperties
,customRoomPropertyKeysForLobby
, andvisible
when requesting the Client Engine to create a room. Please refer to Create Room for the description of these three parameters. - seatCount (optional): when creating a room, specify how many players are needed for this game. This value needs to be between
minSeatCount
andmaxSeatCount
of setting the number of players in a room, otherwise the Client Engine will refuse to create the room. If not specified,defaultSeatCount
is used.
For example, to create a new room with matching conditions, call createGame()
like this:
// You can get the playerId and createGameOptions from the request sent by the client.
const props = {
level: 2,
};
const roomOptions = {
customRoomPropertyKeysForLobby: ['level'],
customRoomProperties: props,
};
const createGameOptions = {
roomOptions
};
gameManager.createGame(playerId, createGameOptions);
In your first Client Engine game, reception.ts
writes two custom methods for "QuickStart" and "Create a new game" using createGame() and invokes relevant logic with the web APIs
/reservationand
/gamein
index.ts`. If you don't need any customization, you can just use the interfaces in the sample demo with the above parameters.
Getting currently available rooms
GameManager provides a getAvailableGames()
method to retrieve the list of available games in the Client Engine instance where the GameManager object resides. Here, available means that the room still has empty seats. The sample code is as follows.
var games = gameManager.getAvailableGames();
Note that this method does not fetch all available rooms in the Multiplayer service but only the available rooms in the current Client Engine instance. For Client Engine multi-instance load balancing, please refer to Load Balancing.
Matching
The GameManager
does not provide a matching mechanism for the time being. If a client only needs to join a room randomly, please refer to the Quick Start
implementation in sample project. This implementation looks for available rooms or creates rooms in the least loaded instance, and eventually returns to the client the name of a room that can be joined.
If you wish to implement conditional matching, you can implement it like this:
- The client requests conditional match from the multiplayer service, and if there is a room available, the join-success event is triggered.
- If there is no spare room in the Multiplayer service, the client will receive the "join room failed" event. In this event, if the error code is 4301, then request the Client Engine to create a room.
- The Client Engine receives the request, creates the room and returns the roomName to the client. The logic of this part can use the
/game
entry in sample project. - The client gets the roomName returned by the Client Engine, joins the room, and waits for others to join.
The sample code for this process in the client is as follows (not Client Engine):
The client first makes a conditional request to the Multiplayer service to join the room:
const matchProps = {level: 2};
play.joinRandomRoom({matchProperties: matchProps});
If the multiplayer service has a new room available at this time, you will automatically be added to the new room and the join-room-success event will be triggered.
play.on(Event.ROOM_JOINED, () => {
// TODO can do things like jumping to other scenes
});
If no room is available, the join-room-failed event will be triggered. In this event, the error code 4301 means there's no room available. Now we can request the Client Engine to create a new room, get the roomName of the new room, and join the new room:
// Requesting Client Engine to create a room after failing to join a room
play.on(Event.ROOM_JOIN_FAILED, (error) => {
if (error.code === 4301) {
// Setting up to create rooms with matching properties
const props = {level: 2};
const options = {customRoomPropertyKeysForLobby: ['level']};
// The `/game` interface implemented in the Client Engine is called over HTTP.
const { roomName } = await (await fetch(
`${CLIENT_ENGINE_SERVER}/game`,
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
playerId: play.userId,
options
})
}
)).json();
// Join the room
return play.joinRoom(roomName);
} else {
console.log(error);
}
});
Game
Game lifecycle
- Create:
Game
is managed byGameManager
in the SDK, which creates aGame
as appropriate when it receives a request to create a room. - Run: After creation, control of the
Game
is transferred from theGameManager
in the SDK to theGame
itself. From this moment on, players will join the game room one after another. - Destruction: Once all players have left the room, the game is over, and the
Game
hands control back to theGameManager
, which does the final cleanup, including disconnecting and destroying the masterClient for the room, deleting theGame
from the list of games it manages, and so on.
General Properties of Game
The Game
class provides the following properties to simplify the implementation of common requirements in implementing game logic. You can easily get the following properties in your own class inheriting Game
:
room
property: the room that the game corresponds to, which is an instance of Room in the Play SDK.masterClient
property: the masterClient for the game, which is an instance of Play in the Play SDK.players
property: a list of players that do not contain a masterClient. Note that if you get the list of room members via theplayerList
property of a Play SDK Room instance, it includes the masterClient.
General Methods of Game
The Game class encapsulates the following methods on top of the Multiplayer SDK, which allows MasterClient to send custom events more conveniently:
broadcast()
method: broadcast custom event to all players. Please refer to Broadcast Custom Events for example code.forwardToTheRests()
method: forward the custom events sent by one player to other players. Please refer to Forwarding Custom Events for example code.
Implementing Your Own Game
To implement your own in-room game logic, you need to create a class that inherits from Game
to write your own game logic. The sample method is as follows:
import { Game } from "@leancloud/client-engine";
export default class SampleGame extends Game {
constructor(room: Room, masterClient: Play) {
super(room, masterClient);
}
}
Setting the number of players in a room
The number of players here refers to the number of players excluding the MasterClient, and according to the limitations of the Multiplayer service, the maximum number of players cannot exceed 9.
In Game
, you need to specify defaultSeatCount
static attribute as the default number of players, and the Client Engine will request the multiplayer service to create a room according to this value. For example, if you need 3 players to play Landlord, you can set it like this:
export default class SampleGame extends Game {
public static defaultSeatCount = 3; // Maximum 9
}
If your game requires a certain number of players, in addition to setting defaultSeatCount
, you need to use the minSeatCount
static attribute to limit the minimum number of players and the maxSeatCount
static attribute to set the maximum number of players. For example, Triple Triad requires a minimum of 2 players and a maximum of 8 players to play, but the default is 5 players, so you can set it like this:
export default class SampleGame extends Game {
public static minSeatCount = 2;
public static maxSeatCount = 8; // Maximum 9
public static defaultSeatCount = 5;
}
In the Create Room interface, you can dynamically override defaultSeatCount
with the seatCount
parameter in the client request.
You can optionally configure the room full event to fire when the room reaches seatCount
; if your client does not specify seatCount
, the defaultSeatCount
value will be used for the room full event.
Join Room Event
When a client successfully joins a room, the MasterClient in the Client Engine will receive the new player join event. If you need to listen to this event, you can write the code to listen to it in the constructor()
method in your custom Game
.
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('Someone\'s coming');
});
}
}
Room Full Event
When the number of people in a room satisfies the room full logic of set the number of players in the room, the watchRoomFull
decorator lets you receive the AutomaticGameEvent.ROOM_FULL
event thrown by the Game, where you can write the appropriate game logic, such as closing the room, broadcasting the start of the game to the Client:
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);
// Listen for the ROOM_FULL event and call the `start() method` when it receives it.
this.once(AutomaticGameEvent.ROOM_FULL, this.start);
}
protected start = async () => {
// Write the logic for when your room is full here.
// Mark the room as no longer available
this.masterClient.setRoomOpened(false);
// Broadcast the game start event to the client
this.broadcast(Event.GameStart);
}
}
Broadcasting custom events
In Room Full Event, Game
broadcasts the start of the game to all members of the room:
enum Event {
GameStart = 15,
};
this.broadcast(Event.GameStart);
You can also broadcast events with some data:
enum Event {
GameStart = 15,
};
const gameData = {someGameData};
this.broadcast(Event.GameStart, gameData);
At this point the client's receive custom event method will be triggered, and if it finds out it's a game-start
event, the client can show the start of the matchmaking on the UI.
Forwarding custom events
MasterClient can forward events from one client to other clients, and process data while doing so:
enum Event {
SomeEvent = 15,
};
this.forwardToTheRests(event, (eventData) => {
// Preparing data to be forwarded
const actUserId = event.senderId;
const result = {actUserId};
return result;
// Event.SomeEvent is the ID of the custom event, or the ID of the original event if omitted.
}, Event.SomeEvent)
In this code, the event
parameter is the original event sent by some client, and eventData
is the data of the original event, which you can manipulate when forwarding the event to other clients, e.g., to erase or add some information. After MasterClient sends the event, the client's [Receive Custom Event](/sdk/ multiplayer/guide/js/#ReceiveCustomEvent) is triggered.
Communications between the MasterClient and clients
In addition to the Broadcast Custom Events and Forward Custom Events provided in the initial project above, you can still use Custom Attributes and Custom Events in the Multiplayer service for communication.
In addition, Game
provides the following RxJS methods to stream events and streamline your code and logic.
getStream()
method: get the stream of custom events sent by the player, which is an Observable object in RxJS. Please refer to API Documentation for interface description.takeFirst()
method: get the stream of the first custom event sent by the player with the specified condition counting from now. Returns an Observable object in RxJS. Please refer to the API documentation for the interface description.
Note that the above two methods require you to know RxJS in order to use them. If you don't know RxJS, you can still use the event methods for communication.
Game Over
When all players have left, GameManager
will automatically destroy the current room and the associated MasterClient for you; at this point, if you have no other logic to do, you don't need to concern yourself with this section of the documentation. If you want to do some cleanup yourself, such as saving user data, you can use the autoDestroy
decorator, which will automatically trigger the destroy()
method in the Game
subclass when all the players have left, and you can write the relevant logic in this method.
import { autoDestroy, Game } from "@leancloud/client-engine";
@autoDestroy()
export default class SampleGame extends Game {
protected destroy() {
super.destroy();
console.log('Extra cleaning can be done here');
}
}
Load Balancing
Client Engine automatically adjusts the number of instances based on the overall instance load.
In Client Engine, there are two types of load balancing: the first is for requests initiated by clients through the REST API, and the second is for the load of the number of Games
running on each instance. For requests initiated by clients via the REST API, the Client Engine automatically distributes the requests evenly among all current instances, without requiring any configuration work. For the second scenario, each Game
object (per game) usually exists for a certain period of time, and in order to make the Game
objects carried by each instance as balanced as possible, we need to additionally configure the GameManager
into the load balancing system.
This feature is implemented by the LoadBalancerFactory
class provided by the SDK. As we can see in GameManager Instantiation, LoadBalancerFactory
generates a LoadBalancer
object by binding gameManager
, which is present in every Client Engine instance.
When an instance of the Client Engine receives a REST API request from a client and calls a method in the gameManager
, the load-balanced node LoadBalancer
in the instance that receives the request finds the instance with the smallest number of Games
in the cluster, forwards the specified gameManager
API call to the gameManager
of that instance to run, and returns the result. In this case, the LoadBalancer
is only responsible for forwarding the request and does not care about how the request is handled.
API Documentation
You can find more descriptions of the SDK's classes, methods, and properties in the API documentation. Click to view Client Engine SDK API documentation.