Creating Custom Systems #

Along with facilitating the implementation of powerful Nakama-based features into your game, Hiro can also be used to help you structure your own game systems and logic.

This page demonstrate two different custom systems to showcase how you can use this functionality: the first example makes use of an existing Hiro system while the second is not tied to existing systems at all.

With existing Hiro systems #

The first example will set the player’s initial stats if this is their first time installing the game.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public struct PlayerStats
{
    public int Health;
    public int Attack;
    public int Strength;
    public int Defense;
}

public class FirstTimePlayerSystem : IInitializeSystem, IUserSystem
{
  public string Name => nameof(FirstTimePlayerSystem);
  public bool IsInitialized { get; private set; }

  private NakamaSystem _nakamaSystem;

  public FirstTimePlayerSystem(NakamaSystem nakamaSystem)
  {
    _nakamaSystem = nakamaSystem;
  }

  public async Task InitializeAsync()
  {
    const string collection = "Stats";
    var playerInitObject = await _nakamaSystem.ReadStorageObjectAsync(collection, _nakamaSystem.UserId, _nakamaSystem.UserId);

    if (playerInitObject == null)
    {
      var initialStats = new PlayerStats
      {
        Health = 10,
        Attack = 1,
        Strength = 1,
        Defense = 1
      };

      var writeObject = new WriteStorageObject
      {
        Collection = collection,
        Key = _nakamaSystem.UserId,
        PermissionRead = 2,
        PermissionWrite = 1
      };

      var acks = await _nakamaSystem.WriteStorageObjectsAsync(new [] { writeObject });
    }

    IsInitialized = true;
  }
}

Without existing Hiro systems #

The second example makes use of Hiro’s deterministic startup to ensure that it runs after another system has been initialized and then bootstraps the gameplay logic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class GameBootstrapSystem : IInitializeSystem
{
  public string Name => nameof(FirstTimePlayerSystem);
  public bool IsInitialized { get; private set; }

  private NakamaSystem _nakamaSystem;

  public GameBootstrapSystem(NakamaSystem nakamaSystem)
  {
    _nakamaSystem = nakamaSystem;
  }

  public async Task InitializeAsync()
  {
    if (!_nakamaSystem.IsInitialized)
    {
      return Task.FromException(new InvalidOperationException("NakamaSystem is not initialized"));
    }

    GameManager.Instance.Initialize(_nakamaSystem);
    GameManager.Instance.StartGame();
  }
}

Handling failures during initialization #

While the order of initialization is deterministic, it is important to ensure that systems you are dependent on have initialized successfully.

In the above scenario:

  1. The IsInitialized property of the NakamaSystem is checked to ensure that the system was fully initialized before continuing.
  2. If not, the InitializeAsync call returns with an Exception wrapped in a Task.
  3. This is then handled by the Systems object which will call the system’s InitializeFailedAsync method.

You can handle this two ways:

  1. If the initialization failure of this system is critical to your game, you can re-throw the exception from within InitializeFailedAsync.

This causes the main Systems object to halt the initialization flow, stopping any further systems from being initialized.

1
2
3
4
5
6
7
public async Task InitializeFailedAsync(ILogger logger, Exception e)
{
  logger.Error($"GameBootstrapSystem failed to initialize, halting further initialization.\n{e.Message}");

  // Re-throw the exception, causing the Systems object to halt further initialization
  return Task.FromException(e);
}
  1. You can choose to allow initialization of further systems to continue by returning a completed task.
1
2
3
4
5
6
7
public async Task InitializeFailedAsync(ILogger logger, Exception e)
{
  logger.Error($"GameBootstrapSystem failed to initialize, continuing initialization of other systems.\n{e.Message}");

  // Return a completed task to allow further systems to be initialized
  return Task.CompletedTask;
}