How I Created an Online Multiplayer Game Using Colyseus
Tags: gamedev, node, colyseus
Published: 2024-03-24 09:37:44

# **How I Created an Online Multiplayer Game Using Colyseus**

## The Challenge

Let's delve into the world of Trivia Games, where players engage in competitive quizzes, testing their knowledge. This article aims to explore the process of creating a browser-based multiplayer trivia game using Colyseus.

### Designing the Game Logic

Before diving into the code, I began by sketching flowcharts to visualize the game's logic, such as player room allocation and question distribution. 

**Picture here**

### The Concept of (Game) Rooms

![[Ryan Doede](https://www.behance.net/gallery/64200385/Isometric-Rooms-(CGI)/modules/376280015)](https://mir-s3-cdn-cf.behance.net/project_modules/fs/dacb2964200385.5acacbccc27d9.jpg)

[Ryan Doede](https://www.behance.net/gallery/64200385/Isometric-Rooms-(CGI)/modules/376280015)

The concept of rooms is not a new one, and it's not limited to multiplayer games. Many online experiences, such as video conference meetings or group chats, follow a room-based pattern. In the context of our game, rooms provide a virtual space for players to join and play with their friends.

Rooms is a paradigm that can helps isolate each game session into its own room.

Initially, a player creates a room and then invites the other players to join.

### The First Prototype: Exploring Ably

In the initial stages of development, I opted to use Ably as the communication platform. Ably offers a generous free plan (6 million messages per month) and has the ability to minimize latency by players joining from different parts of the world. In a traditional server-client model, players would connect to a common server, potentially leading to latency issues. However, Ably's infrastructure can help mitigate this problem.

Ably follows the Pub-Sub (Publish-Subscribe) pattern, where messages are published to a channel and subscribers receive those messages. This pattern allows for real-time communication, making it suitable for multiplayer game development.

![The Publish-Subscribe pattern](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1ghts4ur9tuyw28l8kjp.png)

The Publish-Subscribe pattern

### Designing the prototype

But going with this route means that I will have to implement the creation of 

1. rooms, managing authentication
2. synchronization, state sharing (when an event is emitted the server process it and mutates the state, all clients need to get the latest state)
3. client room creating/joining
4. messaging format e.g. when a player respond to a question a message like the following needs to be sent `{"event_name":"player_answer_question", "data":{"player_response":1,"player_id":"foo"}}`
5. threading (!) to handle multiple rooms at the same time

But nevertheless, I continued anyway to explore this approach.

I had also designed some flow charts of different scenarios (Room creation, game flow etc):


![Scenario Room Creation ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t2obws14f0ny29v8zoz8.png)

Thankfully, Ably provides an elegant Dashboard, and a developer menu that I can see all the messages and join a channel (room).


![Ably Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ftpd4csjy7mtmt56npzr.png)

For the prototype I used Python.

### The Prototype Code

The code snippet you see here is a prototype for creating and joining rooms in a multiplayer game. It's a starting point for testing the concept, even though it might not be the most elegant solution.

In this prototype, we create new rooms and spawn instances to manage them. When a message is received, we check if it's a 'wait' message, and if so, we introduce a delay. This represents a basic form of game logic where timing is essential.

However, this approach requires writing a considerable amount of boilerplate code for even the simplest message exchange. Additionally, defining a common message format is necessary to ensure smooth communication between players and the server.

This reminded me of a quote:

> If you aren't using a framework, you are building one
> 

So by choosing to use this approach, I would have to write code to handle the basic stuff.

I have also have created some diagrams for this but I 

## Discarding the prototype

I tried searching a different solution because using the former path was not viable, I was reinventing the wheel, and I was sure somebody had already solved the problem of a real-time multiplayer game server, so I searched for a solution.

So I begin searching for a framework that could help me in my particular problem.

My requirements for the framework that I was searching were:

1. Have a built-in way to handle room creations
2. Support JavaScript (for the client side) by having a native client
3. Have a solid documentation
4. Easy event emitting and handling
5. Be FOSS

### Colyseus

The framework I stubble upon is called Colyseus, and I'd like to take a moment to elaborate on what makes Colyseus so good for my case. It is an open-source, actively developed, highly versatile framework designed to facilitate the development of multiplayer games and real-time applications.

It provides a robust and efficient server-side infrastructure for handling the complexities of real-time interactions, making it an excellent choice for game developers, as well as anyone building applications that require synchronized and real-time experiences.

In my personal experience, even with minimal prior exposure to Node.js, I found it surprisingly accessible to grasp the fundamentals of Colyseus. The clear documentation, along with the provided examples, enabled me to quickly understand the core concepts and principles of this framework. 

![Colyseus Playground](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmu2x7pt17a1a114we6l.png)

It also provides a debugging interface a “[Playground](https://docs.colyseus.io/tools/playground/)” that can show all the rooms that have been created, the state of each room, the players that have joined those room, and many more helpful features.

Another alternative to Colyseus is [Namaka](https://heroiclabs.com/nakama/) by Heroic Labs, but it requires a dedicated server (SSH-able) that can run a Docker container but Colyseus on the other hand only requires Node.js. 

## The architecture

Having a solid understanding from early of data flows, the game flow, and the architecture can speed up decision-making thought the development

Colyseus follows the [authoritative server](https://www.gabrielgambetta.com/client-server-game-architecture.html) paradigm, ensuring a singular truth for streamlined development and prevention of cheating. In essence, authoritative implies that the server maintains and disseminates the game state to clients. Clients merely need to present the state in a *visually appealing* interface and emit events, for example when the user respond to a question an event is emitted and the server handles it.

### State

In our Quiz, the [game state](https://docs.colyseus.io/state/) encompasses player scores, the ongoing question, the remaining time, the available 4 responses and each player's response details, such as when and what they answered.

Colyseus provides an elegant method for defining this state, employing a schema that accommodates both straightforward data types like numbers and strings, as well as more intricate structures such as arrays and sets.

I have defined the following schema for the Quiz: 

```tsx
// Import necessary classes from the Colyseus schema library
import { Schema, type, ArraySchema, MapSchema } from "@colyseus/schema";

// Define the Player class representing the state of an individual player in the quiz
export class Player extends Schema {
  // Session ID uniquely identifying the player's session
  @type("string") sessionId: string;

  // Player's username
  @type("string") username: string;

  // Number of lives the player has, initialized to 3 by default
  @type("number") lives: number = 3;

  // Player's score, initialized to 0 by default
  @type("number") score: number = 0;

  // Answer chosen by the player
  @type("string") player_answer: string;

  // Time when the player provided an answer
  @type("number") player_answer_time: number;

  // Counter for the number of consecutive correct answers
  @type("number") streak_correct: number = 0;
}

// Define the MyRoomState class representing the overall state of the quiz room
export class MyRoomState extends Schema {
  // Boolean indicating whether the game has started
  @type("boolean") gameHasStarted: boolean = false;

  // Boolean indicating whether the game is over
  @type("boolean") gameOver: boolean = false;

  // Category of the current question
  @type("string") questionCategory: string = "";

  // Text of the current question
  @type("string") question: string = "";

  // Array of possible answers to the current question
  @type([ "string" ]) answers = new ArraySchema<string>();

  // Correct answer to the current question
  @type("string") correctAnswer: string = "";

  // Map of players, where keys are player session IDs and values are instances of the Player class
  @type({ map: Player }) players = new MapSchema<Player>();

  // Time remaining for the current question
  @type("number") timer: number;
}
```

A visual representation of the player's and room state.

![Class Diagram of Player's & Room State](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z2abetxzedb5d3ruhcq6.png)

## The game flow

![Game Flow Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n9bdqgusc41n9fwdut6f.png)


1. **Player Joining the Room**:
    - When a player joins the game room, their `sessionId` and `username` are assigned.
    - This player object is then added to the `players` map in the `MyRoomState` class.
2. **Starting the Game**:
    - Players can trigger the start of the game by sending a "start_game" message.
    - Upon starting the game, the room broadcasts a "players_get_ready" message to all clients.
    - The game checks if it has not already started (`gameHasStarted` is `false`), and then locks the room to prevent new players from joining.
3. **Game Loop**:
    - The game loop continues until the game ends (`gameOver` becomes `true`).
    - During each iteration of the loop, the game follows these steps:
        - Generates a random question using the `QuestionsAPI`.
        - Updates the game state with the new question, answers, and other relevant information.
        - Starts a timer for players to answer the question.
        - Listens for player answers and records them.
        - If all players have answered or the timer runs out, the game proceeds to calculate scores.
        - Checks if at least one player is still alive (has lives remaining). If not, sets `gameOver` to `true`.
4. **Calculating Scores**:
    - The game calculates scores based on player answers and time taken.
    - If a player answers correctly, their score increases based on the time taken to answer.
    - If a player answers incorrectly, they lose a life.
    - Additionally, streaks of correct answers can grant extra lives.
5. **Ending the Game**:
    - The game ends if no player is alive or if a specific condition is met.
    - Once the game is over, the room unlocks, allowing players to leave or join other rooms.

In summary, the game flow involves players joining the room, starting the game, iterating through the game loop to handle questions, answers, and scoring, and finally ending the game when certain conditions are met. Throughout this process, the game state (`MyRoomState`) is continuously updated to reflect the current state of the game.

## Deployment

I opted for utilizing the free service provided by [render.com](http://render.com/) to host both the Node.js Server (Colyseus) and the Vue.js web application (frontend).

It's worth noting that the resources offered in the free tier of the Node.js may go into a dormant (spindown) state due to inactivity, resulting in potential delays of 50 seconds or more for requests.

You can access the game [here](https://quizto.onrender.com/), but please be aware that there might be a slight delay when creating a room. This delay is caused by the potential spin-down of the Node.js instance due to inactivity.