Skip to content

Best practices & recommendations

Work-in-progress

This documentation page is not complete

This section provides general recommendations and best practices to keep your codebase healthy and readable for your team. They are all optional, but if followed are going to improve code readability and cleanliness.

  • Keep your room classes as small as possible, delegating game-specific functionality to other composable structures.
  • Keep your synchronizeable data structures as small as possible
    • Ideally, each class extending Schema should only have field definitions.
    • Custom getters and setters methods can be implemented, as long as you don't have game logic in them.
  • Your game logic should be handled by other structures, such as:

Unit Testing

TODO: we need to provide a @colyseus/testing package to allow easily mocking the Room class and triggering its lifecycle events, as well as creating dummy clients.

Design Patterns

The Command Pattern

Why?

  • Models (@colyseus/schema) should contain mostly data, without heavy game logic.
  • Rooms should have as little code as possible, and forward actions to other structures

The command pattern has several advantages, such as:

  • It decouples the classes that invoke the operation from the object that knows how to execute the operation.
  • It allows you to create a sequence of commands by providing a queue system.
  • Implementing extensions to add a new command is easy and can be done without changing the existing code.
  • Have strict control over how and when commands are invoked.
  • Improves code readability and the possibility of unit testing.

Usage

Installation

npm install --save @colyseus/command

Initialize the dispatcher in your room implementation:

import { Room } from "colyseus";
import { Dispatcher } from "@colyseus/command";

import { OnJoinCommand } from "./OnJoinCommand";

class MyRoom extends Room<YourState> {
  dispatcher = new Dispatcher(this);

  onCreate() {
    this.setState(new YourState());
  }

  onJoin(client, options) {
    this.dispatcher.dispatch(new OnJoinCommand(), {
        sessionId: client.sessionId
    });
  }

  onDispose() {
    this.dispatcher.stop();
  }
}
const colyseus = require("colyseus");
const command = require("@colyseus/command");

const { OnJoinCommand } = require("./OnJoinCommand");

class MyRoom extends colyseus.Room {

  onCreate() {
    this.dispatcher = new command.Dispatcher(this);
    this.setState(new YourState());
  }

  onJoin(client, options) {
    this.dispatcher.dispatch(new OnJoinCommand(), {
        sessionId: client.sessionId
    });
  }

  onDispose() {
    this.dispatcher.stop();
  }
}

How a command implementation looks like:

// OnJoinCommand.ts
import { Command } from "@colyseus/command";

export class OnJoinCommand extends Command<YourState, {
    sessionId: string
}> {

  execute({ sessionId }) {
    this.state.players[sessionId] = new Player();
  }

}
// OnJoinCommand.js
const command = require("@colyseus/command");

exports.OnJoinCommand = class OnJoinCommand extends command.Command {

  execute({ sessionId }) {
    this.state.players[sessionId] = new Player();
  }

}

See more

Entity-Component System (ECS)

We currently do not have an official ECS (Entity-Component System), although we've seen members of the community implement their own solutions.

Back to top