Unity client guide

The official Unity client handles all communication in realtime with the server. It implements all features in the server and is compatible with Unity 5.4+. To work with our Unity client you'll need to install and setup Unity engine.

Download

The client is available on the Unity Asset store and also on GitHub releases. You can download "Nakama.unitypackage" which contains all source code and DLL dependencies required in the client code.

For upgrades you can see changes and enhancements in the CHANGELOG before you update to newer versions.

Help and contribute

The Unity client is open source on GitHub. Please report issues and contribute code to help us improve it.

Install and setup

When you've downloaded the "Nakama.unitypackage" file you should drag or import it into your Unity editor project to install it. In the editor create a new C# script via the Assets menu with "Assets > Create > C# Script" and create an INClient.

The client object is used to execute all logic against the server.

using Nakama;
using System.Collections;
using UnityEngine;

public class NakamaSessionManager : MonoBehaviour {
  void Start() {
    INClient client = new NClient.Builder("defaultkey")
        .Host("127.0.0.1")
        .Port(7350)
        .SSL(false)
        .Build();
  }

  void Update() {
  }
}

We use the builder pattern with many classes in the Unity client. Most classes have a ".Default()" method to construct an object with default values.

Note

By default the client uses connection settings "127.0.0.1" and 7350 to connect to a local Nakama server.

// Quickly setup a client for a local server.
INClient client = NClient.Default("defaultkey");

Unity uses an entity component system (ECS) which makes it simple to share the client across game objects. Have a read of Controlling GameObjects Using Components for examples on how to share a C# object across your game objects.

Authenticate

With a client object you can authenticate against the server. You can register or login a user with one of the authenticate options.

To authenticate you should follow our recommended pattern in your client code:

   1. Build an instance of the client.

var client = NClient.Default("defaultkey");

   2. Write a callback which will be used to connect to the server.

Action<INSession> sessionHandler = delegate(INSession session) {
  Debug.LogFormat("Session: '{0}'.", session.Token);
  client.Connect(session, (bool done) => {
    Debug.Log("Session connected.");
    // Store session for quick reconnects.
    PlayerPrefs.SetString("nk.session", session.Token);
  });
};

   3. Restore a session for quick reconnects.

var sessionString = PlayerPrefs.GetString("nk.session");
if (!string.IsNullOrEmpty(sessionString)) {
  INSession session = NSession.Restore(sessionString);
  if (!session.HasExpired(DateTime.UtcNow)) {
    sessionHandler(session);
  }
}

   4. Login or register a user.

Tip

It's good practice to cache a device identifier when it's used to authenticate because they can change with device OS updates.

Action<INError> errorHandler = delegate(INError err) {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
};

// See if we have a cached id in PlayerPrefs.
var id = PlayerPrefs.GetString("nk.id");
if (string.IsNullOrEmpty(id)) {
  // We'll use device ID for the user. See other authentication options.
  id = SystemInfo.deviceUniqueIdentifier;
  // Store the identifier for next game start.
  PlayerPrefs.SetString("nk.id", id);
}

var message = NAuthenticateMessage.Device(id);
_client.Login(message, sessionHandler, (INError err) => {
  if (err.Code == ErrorCode.UserNotFound) {
    _client.Register(message, sessionHandler, errorHandler);
  } else {
    ErrorHandler(err);
  }
});

In the code above we use NAuthenticateMessage.Device(id) but for other authentication options have a look at the code examples.

A full example class with all code above is here.

Send messages

When a user has been authenticated a session is used to connect with the server. You can then send messages for all the different features in the server.

This could be to add friends, join groups and chat, or submit scores in leaderboards, and matchmake into a multiplayer match. You can also execute remote code on the server via RPC.

The server also provides a storage engine to keep save games and other records owned by users. We'll use storage to introduce how messages are sent.

string json = "{\"jsonkey\":\"jsonvalue\"}";

var message = new NStorageWriteMessage.Builder()
    .Write("someBucket", "someCollection", "myRecord", storageValue)
    .Build();
client.Send(message, (INResultSet<INStorageKey> list) => {
  Debug.Log("Successfully wrote record.");
}, (INError error) => {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
});

Have a look at other sections of documentation for more code examples.

Handle events

The client has callbacks which are called on various events received from the server.

client.OnError = (INError err) => {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
};

client.OnDisconnect = (INDisconnectEvent evt) => {
  Debug.Log("Disconnected from server.");
  Debug.LogFormat("Reason '{0}'", evt.Reason);
}

Some events only need to be implemented for the features you want to use.

Callbacks Description
OnDisconnect Handles an event for when the client is disconnected from the server.
OnError Receives events about server errors.
OnMatchData Handles realtime match messages.
OnMatchmakeMatched Receives events when the matchmaker has found match participants.
OnMatchPresence When in a realtime match receives events for when users join or leave.
OnNotification Receives live in-app notifications sent from the server.
OnTopicMessage Receives realtime chat messages sent by other users.
OnTopicPresence Similar to "OnMatchPresence" it handles join and leave events but within chat.

Main thread dispatch

The client runs all callbacks on a socket thread separate to the Unity main thread.

Unity engine does not let code which executes on another thread call one of it's APIs because of how it manages code execution within the game loop. This can causes errors which look something like "<SomeMethod> can only be called from the main thread".

We recommend a simple pattern which can be used to run any code which calls UnityEngine APIs.

   1. Add a queue to your script which manages a client.

Queue<Action> executionQueue = new Queue<Action>(1024);

   2. Add code in your Update method so the queued actions are run.

for (int i = 0, l = executionQueue.Count; i < l; i++) {
  executionQueue.Dequeue()();
}

   3. Enqueue any code which uses a UnityEngine API.

client.Connect(_session, (bool done) => {
  executionQueue.Enqueue(() => {
    Debug.Log("Session connected.");
    // Store session for quick reconnects.
    PlayerPrefs.SetString("nk.session", session.Token); // a UnityEngine API
  });
});

You can see a more advanced version of this pattern in the full example.

Tip

This code pattern is not specific to our client. It's useful for any code which executes on a separate thread with Unity engine.

Managed client

If you don't care about explicit control over which callbacks are dispatched on the Unity main thread you can wrap your code in a helper class which will handle it for you. The "NManagedClient" acts as a proxy for all callbacks.

You must call .ExecuteActions() in Update with the managed client or no callbacks will ever be run.

using Nakama;
using System.Collections;
using UnityEngine;

public class NakamaSessionManager : MonoBehaviour {
  private INClient _client;

  public NakamaSessionManager() {
    var client = NClient.Default("defaultkey");
    _client = new NManagedClient(client);
  }

  private void Update() {
    (_client as NManagedClient).ExecuteActions(); // important!
  }
}

This makes code simpler to reason about but is slightly less performant than if you control exactly which callbacks use UnityEngine APIs and "move them" onto the main thread with an action queue.

Logs and errors

The server and the client can generate logs which are helpful to debug code. To log all messages sent by the client you can enable "Trace" when you build an "INClient".

#if UNITY_EDITOR
INClient client = new NClient.Builder("defaultkey")
    .Trace(true)
    .Build();
#else
INClient client = NClient.Default("defaultkey");
#endif

The #if preprocessor directives is used so trace is only enabled in Unity editor builds. For more complex directives with debug vs release builds have a look at Platform dependent compilation.

Every error in the Unity client implements the "INError" interface. It contains details on the source and content of an error:

Action<INError> errorHandler = delegate(INError error) {
  Debug.LogFormat("Error code {0}", error.Code);
  Debug.LogFormat("Error message {0}", error.Message);
};

Full example

An example class used to manage a session with the Unity client.

using Nakama;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NakamaSessionManager : MonoBehaviour {
  private INClient _client;
  private INSession _session;

  private Queue<IEnumerator> _executionQueue;

  public NakamaSessionManager() {
    _client = NClient.Default("defaultkey");
    _executionQueue = new Queue<IEnumerator>(1024);
  }

  private void Awake() {
    RestoreSessionAndConnect();
    if (_session == null) {
      LoginOrRegister();
    }
  }

  private void RestoreSessionAndConnect() {
    // Lets check if we can restore a cached session.
    var sessionString = PlayerPrefs.GetString("nk.session");
    if (string.IsNullOrEmpty(sessionString)) {
      return; // We have no session to restore.
    }

    var session = NSession.Restore(sessionString);
    if (session.HasExpired(DateTime.UtcNow)) {
      return; // We can't restore an expired session.
    }

    SessionHandler(session);
  }

  private void LoginOrRegister() {
    // See if we have a cached id in PlayerPrefs.
    var id = PlayerPrefs.GetString("nk.id");
    if (string.IsNullOrEmpty(id)) {
      // We'll use device ID for the user. See other authentication options.
      id = SystemInfo.deviceUniqueIdentifier;
      // Store the identifier for next game start.
      PlayerPrefs.SetString("nk.id", id);
    }

    // Use whichever one of the authentication options you want.
    var message = NAuthenticateMessage.Device(id);
    _client.Login(message, SessionHandler, (INError err) => {
      if (err.Code == ErrorCode.UserNotFound) {
        _client.Register(message, SessionHandler, ErrorHandler);
      } else {
        ErrorHandler(err);
      }
    });
  }

  private void SessionHandler(INSession session) {
    _session = session;
    Debug.LogFormat("Session: '{0}'.", session.Token);
    _client.Connect(_session, (bool done) => {
      // We enqueue callbacks which contain code which must be dispatched on
      // the Unity main thread.
      Enqueue(() => {
        Debug.Log("Session connected.");
        // Store session for quick reconnects.
        PlayerPrefs.SetString("nk.session", session.Token);
      });
    });
  }

  private void Update() {
    lock (_executionQueue) {
      for (int i = 0, len = _executionQueue.Count; i < len; i++) {
        StartCoroutine(_executionQueue.Dequeue());
      }
    }
  }

  private void OnApplicationQuit() {
    if (_session != null) {
      _client.Disconnect();
    }
  }

  private void Enqueue(Action action) {
    lock (_executionQueue) {
      _executionQueue.Enqueue(ActionWrapper(action));
      if (_executionQueue.Count > 1024) {
        Debug.LogWarning("Queued actions not consumed fast enough.");
        _client.Disconnect();
      }
    }
  }

  private IEnumerator ActionWrapper(Action action) {
    action();
    yield return null;
  }

  private static void ErrorHandler(INError err) {
    Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
  }
}