Skip to main content
Version: v3

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, and visible 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 and maxSeatCount 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/gameinindex.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:

  1. The client requests conditional match from the multiplayer service, and if there is a room available, the join-success event is triggered.
  2. 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.
  3. 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.
  4. 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

  1. Create: Game is managed by GameManager in the SDK, which creates a Game as appropriate when it receives a request to create a room.
  2. Run: After creation, control of the Game is transferred from the GameManager in the SDK to the Game itself. From this moment on, players will join the game room one after another.
  3. Destruction: Once all players have left the room, the game is over, and the Game hands control back to the GameManager, which does the final cleanup, including disconnecting and destroying the masterClient for the room, deleting the Game 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 the playerList 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.