Skip to main content

Single-player Matches

Matches started in the single-player mode take place entirely locally, on the player's device. Since one game instance acts as both a server and a client, there is no real server authority. It also means that creation of such match and its result are not reported to external backend servers or included in tournament leaderboards. Single-player matches also don't affect player's high score. At the same time a match in this mode can start almost immediately, with no need to wait for external game server allocation and connection.

Editor

The single-player mode can be used to create matches in deployed games, but it can also be tested in editor. To start a single-player match in editor, select it as your current development mode and enter Play mode with your gameplay scene loaded. This is the easiest way to see if your game works in the single-player mode. For any adjustments that may be necessary to make your game compatible with that mode, see the section on adding support.

Implementation

Starting a match

To start a single-player match, all you have to do is call Elympics.ElympicsLobbyClient.PlaySinglePlayer while in lobby. This will load the gameplay scene and start the match.

Adding support

As long as your gameplay logic follows some simple rules and guidelines, you should be able to use the same code for both regular online matches and local single-player matches.

IsServer vs. IsClient

A common assumption when working with code that should only be executed on client or on server is that in an ElympicsMonoBehaviour component the expression Elympics.IsServer == !Elympics.IsClient will always evaluate to true. In the single-player mode both IElympics.IsServer and IElympics.IsClient are true, which can lead to unexpected behavior in code that was written without keeping that in mind. For example with the following code:

void Shoot()
{
SpawnBullet();
Debug.Log("Bullet spawned.)

if (Elympics.IsServer)
return;

PlayBulletSoud();
SpawnBulletSFX();
}

sound and special effects will not be present in a single-player match. To fix that kind of issue simply replace Elympics.IsServer with !Elympics.IsClient:

void Shoot()
{
SpawnBullet();
Debug.Log("Bullet spawned.)

if (!Elympics.IsClient)
return;

PlayBulletSoud();
SpawnBulletSFX();
}

Callbacks

In the single-player mode relevant callbacks from both the IServerHandlerGuid and the IClientHandlerGuid interface are called. In a standard playthrough, you can expect to receive the following callbacks:

  • IServerHandlerGuid
    • OnServerInit
    • OnPlayerConnected
  • IClientHandlerGuid
    • OnClientsOnServerInit
    • OnConnected
    • OnAuthenticated
    • OnMatchJoined
    • OnMatchEnded
    • OnDisconnectedByServer

Keep in mind that IClientHandlerGuid.OnStandaloneClientInit will not be called in the single-player mode. You can use that fact to avoid double initialization of some systems which could happen in your code when it receives callbacks for both server and client.

RPCs

Remote Procedure Calls invoked in the single-player mode in any direction (player to server or server to player) are executed instantly. This allows you to maintain a normal flow of game logic, but it could also cause some actions to become duplicated, for example if you used an RPC to synchronize state between client and server. Assume the following code is placed in an ElympicsMonoBehaviour component.

public GameObject target;

public void ElympicsUpdate()
{
if (Elympics.IsClient && Input.GetKeyDown(KeyCode.Space))
{
DestroyLocally();
DestroyOnServer();
}
}

[ElympicsRpc(ElympicsRpcDirection.PlayerToServer)]
void DestroyOnServer() => DestroyLocally();

void DestroyLocally()
{
target.Destroy();
target = null;
}

This code will work in regular matches, but in the single-player mode it will end up calling DestroyLocally twice, which will lead to NullReferenceException. In cases like this, you have to make sure that the instance of the game which is executing this code is not a server:

public GameObject target;

public void ElympicsUpdate()
{
if (Elympics.IsClient && Input.GetKeyDown(KeyCode.Space))
{
DestroyLocally();

if (!Elympics.IsServer)
DestroyOnServer();
}
}

[ElympicsRpc(ElympicsRpcDirection.PlayerToServer)]
void DestroyOnServer() => DestroyLocally();

void DestroyLocally()
{
target.Destroy();
target = null;
}