Skip to content

Room API (Server-side)

Considering that you already set up your server, now it's time to register room handlers and start accepting connections from your users.

You'll define room handlers creating classes that extend from 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 () { }
}
const colyseus = require('colyseus');

export class MyRoom extends colyseus.Room {
    // When room is initialized
    onCreate (options) { }

    // Authorize client based on provided options before WebSocket handshake is complete
    onAuth (client, options, request) { }

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

These methods correspond to the room lifecycle.

onCreate (options)

Is called once when the room is initialized. You may specify custom initialization options when registering the room handler.

Tip

The options will contain the merged values you specified on Server#define() + the options provided by client.joinOrCreate() or client.create()

onAuth (client, options, request)

The onAuth() method will be executed before onJoin(). It can be used to verify authenticity of a client joining the room.

  • If onAuth() returns a truthy value, onJoin() is going to be called with the returned value as the third argument.
  • If onAuth() returns a falsy value, the client is immediatelly rejected, causing the matchmaking function call from the client-side to fail.
  • You may also throw a ServerError to expose a custom error to be handled in the client-side.

If left non-implemented, it always returns true - allowing any client to connect.

Getting player's IP address

You can use the request variable to retrieve the user's IP address, http headers, and more. E.g.: request.headers['x-forwarded-for'] || request.connection.remoteAddress

Implementations examples

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");
    }
  }
}
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");
     }
  }
}
import { Room } from "colyseus";

class MyRoom extends Room {
  onAuth (client, options, request): Promise<any> {
    /**
     * 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"));
        }
      });
    });
  }
}

Client-side examples

From the client-side, you may call the matchmaking method (join, joinOrCreate, etc) with a token from some authentication service of your choice (i. e. Facebook):

client.joinOrCreate("world", {
  accessToken: yourFacebookAccessToken

}).then((room) => {
  // success

}).catch((err) => {
  // handle error...
  err.code // 400
  err.message // "bad access token"
});
try {
  var room = await client.JoinOrCreate<YourStateClass>("world", new {
    accessToken = yourFacebookAccessToken
  });
  // success

} catch (err) {
  // handle error...
  err.code // 400
  err.message // "bad access token"
}
client:join_or_create("world", {
  accessToken = yourFacebookAccessToken

}, function(err, room)
  if err then
    -- handle error...
    err.code -- 400
    err.message -- "bad access token"
    return
  end

  -- success
end)
client.joinOrCreate("world", {
  accessToken: yourFacebookAccessToken

}, YourStateClass, function (err, room) {
  if (err != null) {
    // handle error...
    err.code // 400
    err.message // "bad access token"
    return;
  }

  // success
})
client.joinOrCreate("world", {
  { "accessToken", yourFacebookAccessToken }

}, [=](MatchMakeError *err, Room<YourStateClass>* room) {
  if (err != "") {
    // handle error...
    err.code // 400
    err.message // "bad access token"
    return;
  }

  // success
});

onJoin (client, options, auth?)

Parameters:

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

onLeave(client, consented) {
    if (this.state.players.has(client.sessionId)) {
        this.state.players.delete(client.sessionId);
    }
}
async onLeave(client, consented) {
    const player = this.state.players.get(client.sessionId);
    await persistUserOnDatabase(player);
}

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 to true (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());
  }
}

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 either string or number.

Callback for specific type of message

onCreate () {
    this.onMessage("action", (client, message) => {
        console.log(client.sessionId, "sent 'action' message: ", message);
    });
}

Callback for ALL messages

You can register a single callback to handle all 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);
    });
}

setState (object)

Set the new room state instance. See State Handling for more details on the state object. It's highly recommended to use the new Schema Serializer to handle your state.

Warning

Do not call this method for updates in the room state. The binary patch algorithm is re-set every time you call it.

Tip

You usually will call this method only once during onCreate() in your room handler.

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().

// server-side
this.setMetadata({ friendlyFire: true });

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 && 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: a Client instance not to send the message to
  • afterNextPatch: 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.

If seconds is provided, the reconnection is going to be cancelled after the provided amout of 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];
  }
}

Alternatively, you may not provide the amount of seconds to automatically reject the reconnection, and reject it yourself using your own logic.

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

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(). You need to make sure roomId is unique.

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 Web-Socket Client.

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 50ms (20fps)

autoDispose: boolean

Automatically dispose the room when last client disconnect. 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() or unlock().

clock: ClockTimer

A ClockTimer instance, used for timing events.

presence: Presence

The presence instance. Check Presence API for more details.