Client #

The Unity client is available from Heroic Labs. Contact us to request access.

The Hiro.unitypackage contains all source code and DLL dependencies required in the client code.

After downloading the file:

  • Drag or import it into your Unity project
  • Set the editor Api Compatibility Level setting to .NET Standard 2.1 (from the Edit -> Project Settings -> Player -> Other Settings -> Configuration menu).
API Compatibility Level
While Hiro is developed against .NET Standard 2.1, the .NET Framework option will work if you need or would like to use it instead.

Hiro deterministic startup #

A key philosophy of Hiro is its opinionated and structured method for bootstrapping your game using a deterministic startup. This is achieved by inheriting from the HiroCoordinator and sequentially configuring and adding the required game Systems.

Hiro provides several different Systems out-of-the-box, such as the NakamaSystem (which handles the connection to Nakama and keeps a reference to the Client, Session and Socket objects) and the EconomySystem (which handles all aspects of in-game economy including both soft and hard currency purchases).

Configuration #

  1. In the Scene Hierarchy, create a new Game Object and call it GameCoordinator.

  2. Attach a new C# script to the object and call it GameCoordinator.

  3. Inherit from HiroCoordinator and create a function to handle player authentication, telling Hiro how you want to authorize users against Nakama. This example creates an authorizer that uses Device Authentication:

 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
public class GameCoordinator : HiroCoordinator
{
    private const string PlayerPrefsAuthToken = "nakama.AuthToken";
    private const string PlayerPrefsRefreshToken = "nakama.RefreshToken";
    private const string PlayerPrefsDeviceId = "nakama.DeviceId";

    private static NakamaSystem.AuthorizerFunc NakamaAuthorizerFunc(INetworkMonitor monitor)
    {
        return async client =>
        {
            var authToken = PlayerPrefs.GetString(PlayerPrefsAuthToken);
            var refreshToken = PlayerPrefs.GetString(PlayerPrefsRefreshToken);
            var session = Session.Restore(authToken, refreshToken);

            // Add an hour, so we check whether the token is within an hour of expiration to refresh it.
            var expiredDate = DateTime.UtcNow.AddHours(1);
            if (session != null && (!monitor.Online || !session.HasRefreshExpired(expiredDate)))
            {
                return session;
            }

            var deviceId = PlayerPrefs.GetString(PlayerPrefsDeviceId, SystemInfo.deviceUniqueIdentifier);
            session = await client.AuthenticateDeviceAsync(deviceId);
            PlayerPrefs.SetString(PlayerPrefsAuthToken, session.AuthToken);
            PlayerPrefs.SetString(PlayerPrefsRefreshToken, session.RefreshToken);
            PlayerPrefs.SetString(PlayerPrefsDeviceId, deviceId);

            if (session.Created)
            {
                Debug.LogFormat("New user account '{0}' created.", session.UserId);
            }

            return session;
        };
    }
}
  1. Override the CreateSystemsAsync method of the HiroCoordinator to configure our initial setup. The first System you need to initialize is the NakamaSystem. This System is responsible for handling connections and authorization with the Nakama server and maintaining the Client, Session and Socket objects:
 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
public class GameCoordinator : HiroCoordinator
{
    protected override Task<Systems> CreateSystemsAsync()
    {
        // Nakama server address.
        const string scheme = "http";
        const string host = "127.0.0.1";
        const int port = 7350;
        const string serverKey = "defaultkey";

        var logger = new Hiro.Unity.Logger();

        // Set up network connectivity probes.
        var nakamaProbe = new NakamaClientNetworkProbe(TimeSpan.FromSeconds(60));
        var monitor = new NetworkMonitor(InternetReachabilityNetworkProbe.Default, nakamaProbe);
        monitor.ConnectivityChanged += (_, args) =>
        {
            Instance.Logger.InfoFormat($"Network is online: {args.Online}");
        };
        var nakamaSystem = new NakamaSystem(logger, scheme, host, port, serverKey, NakamaAuthorizerFunc(monitor), nakamaProbe);

        nakamaSystem.Client.ReceivedSessionUpdated += session =>
        {
            PlayerPrefs.SetString(PlayerPrefsAuthToken, session.AuthToken);
            PlayerPrefs.SetString(PlayerPrefsRefreshToken, session.RefreshToken);
        };

        // Create our systems container
        var systems = new Systems("HiroSystemsContainer", monitor, logger);
        systems.Add(nakamaSystem);

        // Further system initialization here...

        return Task.FromResult(systems);
    }
}

Using Systems #

Accessing a registered system can be done from within any MonoBehaviour by using the GetSystem extension method defined within Hiro:

1
2
3
4
5
var nakamaSystem = this.GetSystem<NakamaSystem>();
if (nakamaSystem == null)
{
    Debug.LogError("NakamaSystem not initialized");
}

Refreshing a System #

To ensure the system has the latest data from Nakama, call the RefreshAsync function. This same refresh logic applies to any system that caches state locally.

1
await nakamaSystem.RefreshAsync();

The NakamaSystem provides some additional quality of life properties and functions, such as providing direct access to the IApiAccount via the Account property or retrieving a storage object via the ReadStorageObjectAsync function.

1
2
var account = nakamaSystem.Account;
Debug.Log(account.User.Username);
1
2
var account = nakamaSystem.Account;
var storageObject = await nakamaSystem.ReadStorageObjectAsync("<Collection>", "<Key>", nakamaSystem.UserId);