Skip to main content

Multiplayer

Easel makes multiplayer games effortless. Developers can code as if there is only one state of the game, making multiplayer games as easy to code as singleplayer games. Easel takes care of all state synchronization and even performs prediction and rollback netcode without any additional effort from the developer. Additionally, no server management is required as all games are deployed on Easel's platform using Easel's servers.

Maximum players

To enable multiple players to join a game, the maxHumanPlayers parameter should be set in your game fn. If maxHumanPlayers is greater than 1, then the game will be a multiplayer game. That means, as long as the game has not been locked (by calling LockGame), other players who are searching for a game with the exact same set of parameters may be added to the same game. Note that the server has an internal limit on how many maximum human players can be in a game at once, and so it may not achieve the requested maxHumanPlayers if it is too high. Currently, the server limit is 16, but this may increase over time.

pub game fn Main(maxHumanPlayers=4) {
// this is now a multiplayer game with a maximum of 4 players
}

See Entrypoints for more information on declaring a game function.

Locking the game

Some games have the concept of a lobby, where players can join and leave before the game starts. In Easel, there is no separate lobby. Instead, the game begins in an "unlocked" mode where players can freely join and leave. Then, at some point, perhaps based pressing a "start" button once a minimum number of players have joined, the game becomes "locked", no new players can join, and the game can begin.

Call the LockGame function to lock the game. Once the game is locked, players cannot join or leave. This should be used when the game is about to start. For example, if your game is a racing game, call LockGame just before the race starts.

While LockGame stops new players from joining, the CommenceGame function marks the beginning of the game. Because in most games both of these occur at the same time, calling CommenceGame will first lock the game if it hasn't been already (equivalent to calling LockGame) before starting the game.

Leaving players

When a player leaves, whether their player entity gets despawned or not depends on whether the game is locked. If the game is not locked, the player entity will be despawned when they leave. If the game is locked, the player entity will never be despawned, even if they leave. Instead, their IsPresent property will change to false. This is useful because sometimes the player entity owns other entities which are essential for the remaining players to be able to complete the game. It may be a good idea to watch the IsPresent property using a with block and make a bot take over control of the player entity when the player leaves so that the game can be completed.

The only change due to locking is whether the player entity is despawned or not. Signals like await BeforePlayerLeave still do fire just the same after the game is locked.

Instantaneous effect

LockGame applies instantly, even though it may take a fraction of a second for the server to get the message and stop adding players. Any players who are added within this fraction of a second will see the locked game but will not join it. As soon as their client detects the situation they will be reassigned to a new game.

This means your game can rely on no more players being joiniing the instant LockGame is called. This means it is safe to call LockGame and immediately begin the game.

Catching up

When a player joins a game, their client will need to replay all inputs that have been made so far. An "Entering the game..." message will be shown to the player while they are catching up. This can take a while if the game has been running for a long time.

One way to improve this is to enable a feature called snapshotting in network.toml. This will cause the game to capture a snapshot of the game state at regular intervals. This snapshot can be sent to new players when they join, allowing them to catch up much faster. This feature increases the CPU and bandwidth usage of the client, so it is disabled by default. To learn how to enable it, see Snapshotting.

Otherwise, during the phase before LockGame when players are still joining, it is best to avoid complicated calculations that may slow down joining the game for new players. For example, wait until your game begins before adding bot enemies to the game.

Rollback netcode

The speed of light is not as fast as we would like.

In a simple world, a game would be able to wait until all inputs are received before simulating the next tick. However, when players are far away from each other, there is a delay between when the player sends an action and when another receives it. This delay is called latency, and it can make games feel unresponsive. Imaging pressing the jump button and your character not jumping immediately. Many techniques exist to try to make games feel responsive even when there is latency. Easel uses rollback netcode.

Rollback netcode allows the game to present a predicted game state, before all inputs have been received. Your character can be shown to jump immediately, without needing to wait for all inputs to arrive. Later on when all inputs finally arrive, the game will correct itself. It does this by rolling back the game to the point where the missing inputs occurred, and then re-simulating the game with the complete set of inputs.

It turns out that, in practice, the game can predict the future quite accurately. This is because while many games are simulated at 60 ticks per second, players only really provide input a few times a second. This makes rollback netcode a good fit for many games.

Determinism

One of the greatest barriers to achieving rollback netcode is that a game must be deterministic. That means, given the same sequence of inputs, the game must always produce the same series of states. If the developer fails to make even a single part of the game deterministic, the game will desynchronize, and so it is easy to make a mistake when implementing rollback netcode. However, because Easel games are written in the Easel programming language, Easel controls the execution of your game and ensures all game logic is executed in a deterministic way, without any additional effort from the developer.

Rubberbanding

Easel uses rubberbanding to smooth out the differences between the predicted game state and the actual game state. Instead of sudden jumps which may be jarring, characters will smoothly move to their correct positions. These corrections sometimes are so subtle they become unnoticeable.

To ensure Easel can rubberband all graphics in your game, make sure to assign an Entity with a Body, rather than a Vector, to the pos parameter when using Spark, Streak or Swoop. This way the graphics can track the position of the entity which may be smoothed out by Easel.