Room API (server-side)¶
The Room
class is meant to implement a game session, and/or serve as the communication channel between a group of clients.
- Rooms are created on demand during matchmaking by default
- Room classes must be exposed using
.define()
import http from "http";
import { Room, Client } from "colyseus";
export class MyRoom extends Room {
// (optional) Validate client auth token before joining/creating the room
static async onAuth (token: string, request: http.IncomingMessage) { }
// When room is initialized
onCreate (options: any) { }
// 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 () { }
}
const colyseus = require('colyseus');
export class MyRoom extends colyseus.Room {
// (optional) Validate client auth token before joining/creating the room
static async onAuth (token: Client, request: http.IncomingMessage) { }
// When room is initialized
onCreate (options) { }
// When client successfully join the room
onJoin (client, options, auth) { }
// When a client leaves the room
onLeave (client, consented) { }
// Cleanup callback, called after there are no more clients in the room. (see `autoDispose`)
onDispose () { }
}
Room lifecycle events¶
- The room lifecycle events are called automatically.
- Optional
async
/await
is supported on every lifecycle event.
onCreate (options)
¶
Called once, when the room is created by the matchmaker.
The options
argument is provided by the client upon room creation:
// Client-side - JavaScript SDK
client.joinOrCreate("my_room", {
name: "Jake",
map: "de_dust2"
})
// onCreate() - options are:
// {
// name: "Jake",
// map: "de_dust2"
// }
The server may overwrite options during .define()
for authortity:
// Server-side
gameServer.define("my_room", MyRoom, {
map: "cs_assault"
})
// onCreate() - options are:
// {
// name: "Jake",
// map: "cs_assault"
// }
On this example, the map
option is "cs_assault"
during onCreate()
, and "de_dust2"
during onJoin()
.
onAuth (token, request)
¶
See the Authentication section for more details.
onJoin (client, options, auth?)
¶
Parameters:
client
: Theclient
instance.options
: merged values specified on Server#define() with the options provided the client onclient.join()
auth
: (optional) auth data returned byonAuth
method.
Is called when the client successfully joins the room, after requestJoin
and onAuth
has succeeded.
onLeave (client, consented)
¶
Is called when a client leaves the room. If the disconnection was initiated by the client, the consented
parameter will be true
, otherwise, it will be false
.
You can define this function as async
. See graceful shutdown
onDispose ()
¶
The onDispose()
method is called before the room is destroyed, which happens when:
- there are no more clients left in the room, and
autoDispose
is set totrue
(default) - you manually call
.disconnect()
.
You may define async onDispose()
an asynchronous method in order to persist some data in the database. In fact, this is a great place to persist player's data in the database after a game match ends.
See graceful shutdown.
Example room¶
This example demonstrates an entire room implementing the onCreate
, onJoin
and onMessage
methods.
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());
}
}
const colyseus = require('colyseus');
const schema = require('@colyseus/schema');
// An abstract player object, demonstrating a potential 2D world position
exports.Player = class Player extends schema.Schema {
constructor() {
super();
this.x = 0.11;
this.y = 2.22;
}
}
schema.defineTypes(Player, {
x: "number",
y: "number",
});
// Our custom game state, an ArraySchema of type Player only at the moment
exports.State = class State extends schema.Schema {
constructor() {
super();
this.players = new schema.MapSchema();
}
}
defineTypes(State, {
players: { map: Player }
});
exports.GameRoom = class GameRoom extends colyseus.Room {
// Colyseus will invoke when creating the room instance
onCreate(options) {
// 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, options) {
this.state.players.set(client.sessionId, new Player());
}
}
onBeforePatch ()
¶
The onBeforePatch
lifecycle hook is triggered before state synchronization, at patch rate frequency. (see setPatchRate())
onBeforePatch() {
//
// here you can mutate something in the state just before it is encoded &
// synchronized with all clients
//
}
onCacheRoom (): any
¶
An optional hook to cache external data when devMode
is enabled.
(See restoring data outside the room's state)
onRestoreData (cachedData: any): void
¶
An optional hook to reprocess/restore data which was returned and stored from the previous hook onCacheRoom
when devMode
is enabled.
(See restoring data outside the room's state)
export class MyRoom extends Room<MyRoomState> {
// ...
onRestoreRoom(cachedData: any): void {
console.log("ROOM HAS BEEN RESTORED!", cachedData);
this.state.players.forEach(player => {
player.method(cachedData["foo"]);
});
}
}
Public methods¶
Room handlers have these methods available.
onMessage (type, callback)
¶
Register a callback to process a type of message sent by the client-side.
- The
type
argument can be eitherstring
ornumber
. - You can only define a single callback per message type. (Defining a callback more than once will result in overriding the previous one)
Callback for specific type of message
onCreate () {
this.onMessage("action", (client, message) => {
console.log(client.sessionId, "sent 'action' message: ", message);
});
}
Fallback for ALL messages
You can register a single callback as a fallback to handle other types of messages.
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);
});
}
Use room.send()
from the client-side SDK to send messages
Check out room.send()
section.
setState (object)
¶
Set the synchronizable room state. See State Synchronization and Schema for more details.
Tip
You usually call this method only once during onCreate()
Warning
Do not call .setState()
for every update in the room state. The binary patch algorithm is reset at every call.
setSimulationInterval (callback[, milliseconds=16.6])
¶
(Optional) Set a simulation interval that can change the state of the game. The simulation interval is your game loop. Default simulation interval: 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)
¶
Set frequency the patched state should be sent to all clients. Default is 50ms
(20fps)
setPrivate (bool)
¶
Set the room listing as private (or revert to public, if false
is provided).
Private rooms are not listed in the getAvailableRooms()
method.
setMetadata (metadata)
¶
Set metadata to this room. Each room instance may have metadata attached to it - the only purpose for attaching metadata is to differentiate one room from another when getting the list of available rooms from the client-side, to connect to it by its roomId
, using client.getAvailableRooms()
.
Now that a room has metadata attached to it, the client-side can check which room has friendlyFire
, for example, and connect directly to it via its roomId
:
// client-side
client.getAvailableRooms("battle").then(rooms => {
for (var i=0; i<rooms.length; i++) {
if (room.metadata?.friendlyFire) {
//
// join the room with `friendlyFire` by id:
//
var room = client.join(room.roomId);
return;
}
}
});
setSeatReservationTime (seconds)
¶
Set the number of seconds a room can wait for a client to effectively join the room. You should consider how long your onAuth()
will have to wait for setting a different seat reservation time. The default value is 15 seconds.
You may set the COLYSEUS_SEAT_RESERVATION_TIME
environment variable if you'd like to change the seat reservation time globally.
send (client, message)
¶
Deprecated
this.send()
has been deprecated. Please use client.send()
instead.
broadcast (type, message, options?)
¶
Send a message to all connected clients.
Available options are:
except
: aClient
, or array ofClient
instances not to send the message toafterNextPatch
: waits until next patch to broadcast the message
Broadcast examples¶
Broadcasting a message to all clients:
onCreate() {
this.onMessage("action", (client, message) => {
// broadcast a message to all clients
this.broadcast("action-taken", "an action has been taken!");
});
}
Broadcasting a message to all clients, except the sender.
onCreate() {
this.onMessage("fire", (client, message) => {
// sends "fire" event to every client, except the one who triggered it.
this.broadcast("fire", message, { except: client });
});
}
Broadcasting a message to all clients, only after a change in the state has been applied:
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 });
});
}
Broadcasting a schema-encoded message:
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 ()
¶
Locking the room will remove it from the pool of available rooms for new clients to connect to.
unlock ()
¶
Unlocking the room returns it to the pool of available rooms for new clients to connect to.
allowReconnection (client, seconds)
¶
Allow the specified client to reconnect
into the room. Must be used inside onLeave()
method.
client
: the disconnectingClient
instanceseconds
: number of seconds to wait for client to perform.reconnect()
, or"manual"
, to allow for manual reconnection rejection (see second example)
Return type:
allowReconnection()
returns aDeferred<Client>
instance.- The returned
Deferred
instance is a promise-like structure, you can forcibly reject the reconnection by calling.reject()
on it. (see second example) Deferred
type can forcibly reject the promise by calling.reject()
(see second example)
Example: Rejecting the reconnection after a 20 second timeout.
async onLeave (client: Client, consented: boolean) {
// flag client as inactive for other users
this.state.players.get(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.get(client.sessionId).connected = true;
} catch (e) {
// 20 seconds expired. let's remove the client.
this.state.players.delete(client.sessionId);
}
}
Example: Manually rejecting the reconnection using custom logic.
async onLeave (client: Client, consented: boolean) {
// flag client as inactive for other users
this.state.players.get(client.sessionId).connected = false;
try {
if (consented) {
throw new Error("consented leave");
}
//
// Get reconnection token
// NOTE: do not use `await` here yet
//
const reconnection = this.allowReconnection(client, "manual");
//
// 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);
// now it's time to `await` for the reconnection
await reconnection;
// client returned! let's re-activate it.
this.state.players.get(client.sessionId).connected = true;
} catch (e) {
// reconnection has been rejected. let's remove the client.
this.state.players.delete(client.sessionId);
}
}
disconnect ()
¶
Disconnect all clients, then dispose.
broadcastPatch ()
¶
You may not need this!
This method is called automatically by the framework.
This method will check whether mutations have occurred in the state
, and broadcast them to all connected clients.
If you'd like to have control over when to broadcast patches, you can do this by disabling the default patch interval:
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
¶
A unique, auto-generated, 9-character-long id of the room.
You may replace this.roomId
during onCreate()
.
Using a custom roomId
Check out the guide How-to » Customize room id
roomName: string
¶
The name of the room you provided as first argument for gameServer.define()
.
state: T
¶
The state instance you provided to setState()
.
clients: Client[]
¶
The array of connected clients. See Client instance.
Example: Getting a client by its sessionId
.¶
maxClients: number
¶
Maximum number of clients allowed to connect into the room. When room reaches this limit, it is locked automatically. Unless the room was explicitly locked by you via lock() method, the room will be unlocked as soon as a client disconnects from it.
patchRate: number
¶
Frequency to send the room state to connected clients, in milliseconds. Default is 50
ms (20fps)
autoDispose: boolean
¶
Automatically dispose the room when last client disconnects. Default is true
locked: boolean
(read-only)¶
This property will change on these situations:
- The maximum number of allowed clients has been reached (
maxClients
) - You manually locked, or unlocked the room using
lock()
orunlock()
.
clock: ClockTimer
¶
A ClockTimer
instance, used for
timing events.
presence: Presence
¶
The presence
instance. Check Presence API for more details.
Client¶
The client
instance from the server-side is responsible for the transport layer between the server and the client. It should not be confused with the Client
from the client-side SDK, as they have completely different purposes.
You operate on client
instances from this.clients
, Room#onJoin()
, Room#onLeave()
and Room#onMessage()
.
Properties¶
sessionId: string
¶
Unique id per session.
Note
In the client-side, you can find the sessionId
in the room
instance.
userData: any
¶
Can be used to store custom data about the client's connection. userData
is not synchronized with the client, and should be used only to keep player-specific with its connection.
onJoin(client, options) {
client.userData = { playerNumber: this.clients.length };
}
onLeave(client) {
console.log(client.userData.playerNumber);
}
auth: any
¶
Custom data you return during onAuth()
.
Methods¶
send(type, message)
¶
Send a type of message to the client. Messages are encoded with MsgPack and can hold any JSON-seriazeable data structure.
The type
can be either a string
or a number
.
Sending a message:
//
// sending message with a string type ("powerup")
//
client.send("powerup", { kind: "ammo" });
//
// sending message with a number type (1)
//
client.send(1, { kind: "ammo"});
sendBytes(type, bytes)
¶
Send a raw byte array message to the client.
The type
can be either a string
or a number
.
This is useful if you'd like to manually encode a message, rather than the default encoding (MsgPack).
Sending a message:
//
// sending message with a string type ("powerup")
//
client.sendBytes("powerup", [ 172, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33 ]);
//
// sending message with a number type (1)
//
client.sendBytes(1, [ 172, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33 ]);
leave(code?: number)
¶
Force disconnection of the client
with the room. You may send a custom code
when closing the connection, with values betweeen 4000
and 4999
(see table of WebSocket close codes)
Tip
This will trigger room.onLeave
event on the client-side.
Table of WebSocket close codes¶
Close code (uint16) | Codename | Internal | Customizable | Description |
---|---|---|---|---|
0 - 999 |
Yes | No | Unused | |
1000 |
CLOSE_NORMAL |
No | No | Successful operation / regular socket shutdown |
1001 |
CLOSE_GOING_AWAY |
No | No | Client is leaving (browser tab closing) |
1002 |
CLOSE_PROTOCOL_ERROR |
Yes | No | Endpoint received a malformed frame |
1003 |
CLOSE_UNSUPPORTED |
Yes | No | Endpoint received an unsupported frame (e.g. binary-only endpoint received text frame) |
1004 |
Yes | No | Reserved | |
1005 |
CLOSED_NO_STATUS |
Yes | No | Expected close status, received none |
1006 |
CLOSE_ABNORMAL |
Yes | No | No close code frame has been receieved |
1007 |
Unsupported payload | Yes | No | Endpoint received inconsistent message (e.g. malformed UTF-8) |
1008 |
Policy violation | No | No | Generic code used for situations other than 1003 and 1009 |
1009 |
CLOSE_TOO_LARGE |
No | No | Endpoint won't process large frame |
1010 |
Mandatory extension | No | No | Client wanted an extension which server did not negotiate |
1011 |
Server error | No | No | Internal server error while operating |
1012 |
Service restart | No | No | Server/service is restarting |
1013 |
Try again later | No | No | Temporary server condition forced blocking client's request |
1014 |
Bad gateway | No | No | Server acting as gateway received an invalid response |
1015 |
TLS handshake fail | Yes | No | Transport Layer Security handshake failure |
1016 - 1999 |
Yes | No | Reserved for future use by the WebSocket standard. | |
2000 - 2999 |
Yes | Yes | Reserved for use by WebSocket extensions | |
3000 - 3999 |
No | Yes | Available for use by libraries and frameworks. May not be used by applications. Available for registration at the IANA via first-come, first-serve. | |
4000 - 4999 |
No | Yes | Available for applications |
error(code, message)
¶
Send an error with code and message to the client. The client can handle it on onError