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.
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
Let's explore the most important files in more detail.
package.json
There are a four dependencies needed to run our server:
-
@colyseus/schema
: @colyseus/schema (opens in a new tab) is used to create schemas and manage our multiplayer room state. -
@oogg/game-server
: This library provides abstraction over the PartyKit (opens in a new tab) library making it easier to create a multiplayer room for oncyber. -
@oogg/rapier3d-compat-cfworker
: For the physics engine, we use Rapier (opens in a new tab). To make it work with Cloudflare Workers, we use this library. -
three
: This is our own version of the three.js (opens in a new tab) library. It's a fork of the official three.js (opens in a new tab) library with some modifications and new features.
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 thestartGame(countdown)
here to notify clients that the game will start incountdown
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 invokestartGame
. -
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 aftercountdown
seconds. And notify clients, taking into account the nework latency for each player. This is usually called in theonRequestStart
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).