Guides
Multiplayer

Multiplayer (Alpha)

Introduction

By default, every new game created with our engine starts in multiplayer mode. You can disable this feature by setting the enabled property to false in the multiplayer section of the config file in the boilerplate code.

export const config = {
    // ...
    multiplayer: {
        enabled: false,
        // ...
    },
    // ...
}

For those looking to host their multiplayer server, our engine offers easy customization through the url property in the config file.

export const config = {
    // ...
    multiplayer: {
        enabled: true,
        // ...
        url: "http://your-custom-server-url.com",
    },
    // ...
}

In the first part of this tutorial we will explore what is currently offered with our default server, and in the second part we will explore how to create and deploy your own server.

Default server

The current default behavior of multiplayer when creating a new game, is simple players state synchronization. This means that when a player joins a space, they will be able to see other players in the space and their movements in real time.

After a new game is created, it'll be provided with boilerplate code that includes a Multiplayer file.

Alt text

After opening that file, you'll notice that it is importing a GameClient object and using it in the connect function.

The most important part of this file is the GameClient object. This object is responsible for connecting the player to the multiplayer room - for more info on GameClient check here (opens in a new tab).

this.room = GameClient.join({
    host: config.multiplayer.url, // in case no url is provided in the config, it'll use the default server
})

The join method will return a GameRoom object - for more info on GameRoom, check here (opens in a new tab). This object will allow us to interact with the multiplayer room.

Apart from the player state synchronization that is offered by our default server, you can also broadcast and send messages to other players in the room.

Example of boardcasting a message to all players

Multiplayer.room.send({
    type: "broadcast", // required
    payload: {
        hello: "world",
    },
    exclude: [], // optional: array of player sessionIds to exclude from the boardcast
})

Example of sending a message a specific player

// we store the players list in a variable
const players = Object.values(Multiplayer.room.state.players.toJSON())
 
let playerSessionId = null
 
// we loop through the players list to find a player that is not us
players.forEach((_player: any) => {
    if (_player.sessionId !== Multiplayer.me.sessionId) {
        playerSessionId = _player.sessionId
        return
    }
})
 
// if we found another player, we send him a message
if (playerSessionId) {
    Multiplayer.room.send({
        type: "send", // required
        playerId: playerSessionId, // required
        payload: {
            hello: "world from player",
        },
    })
}

Custom server

If you want to customize the multiplayer behavior beyond player synching - like making a game with some authoritative logic - you'll need to create your own multiplayer server and deploy it. In the next sections, we'll show you how to do that.

Setting up the server

Before we can update the config file to use our own custom server, we need to first run our server. To do this, we'll use our server library, which is a wrapper around the PartyKit (opens in a new tab) library. This library will allow us to create a multiplayer server with minimal effort.

For a quickstart, you can head out to this github repository and clone it to your local machine.

git clone https://github.com/oncyberio/oo-game-server-starter

This will create a new folder called oo-game-server-starter in your current directory. You can then navigate to this folder and install the dependencies by running the following commands:

cd oo-game-server-starter
npm install

Once the dependencies are installed, you can start the server by running the following command:

npm run dev

This will start the server on port 1999. Now that the server is running, we can update the config file to use our server.

Connecting our server to our game client

We'll explain later how the server works, but for now, let's just update the config file to use our server.

export const config = {
    // ...
    multiplayer: {
        enabled: true,
        // ...
        url: "http://localhost:1999",
    },
    // ...
}

Now, when we run our game, it'll connect to our server instead of the default server.

You can test this by opening two browser windows, one being private. Then, open the game in both windows - you can get the game link by pressing preview in studio. You should see that the player in the first window is replicated in the second window and any movement in the first window is replicated in the second window and vice versa.

Deploying the server

Currently, our server is running locally. To make it available to the public, we need to deploy it.

To deploy the server, simply run the following command:

npm run deploy

Then follow the instructions in the console. After the deployment is complete, an URL to your server will be provided. you can use that URL in the config file to connect to your server.

{
    // ...
    multiplayer: {
        enabled: true,
        url: "https://your-custom-server-url.com",
    },
    // ...
}

Now you can share your game with your friends and you will be able to play together.

Understanding our Server Architecture

Now that we have our server running and we know how to connect to it, let's take a look at how it works.

    • room.ts
    • server.ts
  • .gitignore
  • README.md
  • package-lock.json
  • package.json
  • partykit.json
  • tsconfig.json
  • Let's explore the most important files in more detail.

    package.json

    There are a four dependencies needed to run our server:

    partykit.json

    This file is used to configure the PartyKit (opens in a new tab) library. You can read more about it here (opens in a new tab).

    room.ts

    In this file we define our multiplayer room behavior. You can customize the room behavior by setting room properties and implementing some callback methods

    export class MyRoom extends GameRoom<RoomState> {
     
      // Properties
     
      tickRate = 30
      // ...
     
      // Callback methods
     
      onPreload() { ... }
     
      onJoin(player) { ... }
     
      onMessage(message, player) { ... }
     
      // ...
    }

    Let's look on how to customize room class

    Properties
    • tickRate : defines how often the state is synchronized with the clients

    • state : room state

    • maxPlayers : max number of players that can join the room

    • simulatedLatency : in milliseconds, used to simulate network latency when using local dev server

    • readonly status : Game loop status, can be "idle" or "running".

    Lifecycle methods
    • onPreload() : is called before any client connection is made. Use this to define room initialization that needs to run only once.

    • onJoin(player): called when a player has beed added to the room. Use this method to initialize the player state like spawn position ...

    • onLeave(player) : called when a player has left the room.

    • onRequestStart : called when the room host request a game start; the default implementation. You can call the startGame(countdown) here to notify clients that the game will start in countdown seconds.

    • onMessage(message, player) : When a message is received from a client script.

    • onUpdate(dt) : called on each tick of the game loop. This is only invoked after you invoke startGame.

    • onDispose : called when the room is disposed. The room is disposed when all players has left.

    Room methods
    • broadcast (msg, except?) : Sends a message to all players, You can exclude some in the seconds argument (player ids)

    • send (msg, playerId) : Sends a message to a single client

    • startGame(countdown) : Starts the game after countdown seconds. And notify clients, taking into account the nework latency for each player. This is usually called in the onRequestStart handler method.

    • stopGame() : Stops the game loop, and notifies all the client. Call this when the game ends by reaching the max time, or reaching a win/lose condition.

    types folder

    The room state is defined in src/types folder; The default template includes a preset for players and a game timer.

    You can extend the template by adding properties to the relevent class. Every property must be annotated with a @type(...). This is needed so that the room server can efficiently serialize the state over the network. Currently we use @colyseus/schema package for state definition and serialization. See https://docs.colyseus.io/state/schema/ (opens in a new tab) for more information.

    To add player specific attributes, add the relevent properties to the Player class.

    To add general game attributes, add the relevent properties to the RoomState class.

    The state is synchronized regularly for all client scripts; You can customize the rate in src/room.ts by setting the tickRate property;

    It's important to not delete the current state properties defined in this template (like players ...); they're used internally by the multiplayer package

    Future plans

    Currently multiplayer is in its early stages, and we're working on improving it. Here are some of the features we're currently working on:

    • In studio server side scripting: We're working on adding support for server side scripting in studio. This will allow you to add server side logic without having to deploy your own server or working externally from the studio.

    • Support for more providers: Currently we only support PartyKit (opens in a new tab), but we're working on adding support for other providers like Colyseus (opens in a new tab).