30 Apr 2019

Tutorial: Making a Multiplayer Game with Nakama and Unity: Part 2/3

Paweł Stolarczyk
By Paweł Stolarczyk Senior Unity Developer

Authentication

In the previous post, we focused on setting Nakama and all its components up. Here we will cover one of the crucial elements of working with multiplayer – authentication, which will allow us to send messages and receive them from the server.

Session Authentication

Almost every method provided by the Nakama plugin for Unity used to communicate with our server requires the user to start and maintain their session. Before we can start a session, we need to create Nakama. Client object which contains a list of useful methods to send data to our server.

// NakamaSessionManager.cs

/// <summary>
/// Used to establish connection between the client and the server.
/// Contains a list of usefull methods required to communicate with Nakama server.
/// Do not use this directly, use <see cref="Client"/> instead.
/// </summary>
private Client _client;

...

/// <summary>
/// Used to establish connection between the client and the server.
/// Contains a list of usefull methods required to communicate with Nakama server.
/// </summary>
public Client Client
{
    get
    {
        if (_client == null)
        {
            // "defaultkey" should be changed when releasing the app
            // see https://heroiclabs.com/docs/install-configuration/#socket
            _client = new Client("defaultkey", _ipAddress, _port, false);
        }
        return _client;
    }
}

With our Client object ready, we can authenticate our device Nakama supports multiple ways of authentication:

  • by using a unique ID, assigned to every device,
  • by using email and password,
  • doing it with your Facebook, Google, Game Center and/or Steam accounts,
  • custom authentication system.

Because of the structure of our Jolly Rogers demo game, the best suit was to use device ID and Facebook authentication methods.

//NakamaSessionManager.cs

/// <summary>
/// Authenticates a new session using DeviceId. If it's the first time authenticating using
/// this device, new account is created.
/// </summary>
/// <returns>Returns true if every server call was successful.</returns>
private async Task<AuthenticationResponse> AuthenticateDeviceIdAsync()
{
    try
    {
        Session = await Client.AuthenticateDeviceAsync(_deviceId, null, false);
        Debug.Log("Device authenticated with token:" + Session.AuthToken);
        return AuthenticationResponse.Authenticated;
    }
    catch (ApiResponseException e)
    {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            Debug.Log("Couldn't find DeviceId in database, creating new user; message: " + e);
            return await CreateAccountAsync();
        }
        else
        {
            Debug.LogError("An error has occured reaching Nakama server; message: " + e);
            return AuthenticationResponse.Error;
        }
    }
    catch (Exception e)
    {
        Debug.LogError("Counldn't connect to Nakama server; message: " + e);
        return AuthenticationResponse.Error;
    }
}

As you can see, AuthenticateDeviceIdAsync method shown above is an asynchronous task – instead of events and callbacks, Nakama relies on Tasks. Whenever we await an asynchronous function with to resolve within an async method, Unity will create a new thread and wait until the blocking task returns. This way the main thread is not suspended, which allows us to play the game (send and receive input) while our message is being sent to the server, processed and returned.

Communicating via the internet is not always granted, that’s why calling AuthenticateDeviceAsync method might throw an exception. To determine what happened we check the error status code and message – in some cases, the exception wasn’t thrown by poor network signal, but rather by the server which received an invalid data. Here we can check if the received error is HttpStatusCode.NotFound, in which case the server told us that the user tried to authenticate an account that doesn’t exist yet – we need to create it first.

//NakamaSessionManager.cs

/// <summary>
/// Creates new account on Nakama server using local <see cref="_deviceId"/>.
/// </summary>
/// <returns>Returns true if account was successfully created.</returns>
private async Task<AuthenticationResponse> CreateAccountAsync()
{
    try
    {
        Session = await Client.AuthenticateDeviceAsync(_deviceId, null, true);
        return AuthenticationResponse.NewAccountCreated;
        }
    catch (Exception e)
    {
        Debug.LogError("Couldn't create account using DeviceId; message: " + e);
        return AuthenticationResponse.Error;
    }
}

In both cases, we called Client.AuthenticateDeviceAsync – they differ only in the last, third parameter, which, when set to true, tells the server, that if no account is bound to this device’s ID, create a new account and bind sender’s ID to it. We could let the server create an account during the first call, however, this way we can implement custom logic for when the user logs in for the first time (e.g. show avatar selection panel).

Rather than authenticating every time a user opens our game, it is recommended to authenticate on the first program run and after the session expiration. We can save our session auth code to PlayerPrefs and restore it whenever the user logs in.

//NakamaSessionManager.cs

/// <summary>
/// Stores Nakama session authentication token in player prefs
/// </summary>
private void StoreSessionToken()
{
    if (Session == null)
    {
        Debug.LogWarning("Session is null; cannot store in player prefs");
    }
    else
    {
        PlayerPrefs.SetString("nakama.authToken", Session.AuthToken);
    }
}

The default session duration is set to 60 seconds – this might suit applications, in which we value security. However, the game industry doesn’t always require that much security, that’s why some studios extend their session durations to several weeks, months or even years. We can extend it by using a YAML config file, which should be included in your bound volumes folder, specified in the chapter Quickstart.

Facebook Authentication

Nakama plugin for Unity doesn’t come with Facebook SDK, or any other external API. That’s why we have to download the Facebook plugin for Unity ourselves. Before we can authenticate by using a Facebook account, we need to walk the user through the Facebook connection process

//NakamaSessionManager.cs

/// <summary>
/// Initializes Facebook connection.
/// </summary>
/// <param name="handler">Invoked after Facebook authorisation.</param>
public void ConnectFacebook(Action<FacebookResponse> handler)
{
    if (FB.IsInitialized == false)
    {
        FB.Init(() => InitializeFacebook(handler));
    }
    else
    {
        InitializeFacebook(handler);
    }
}

/// <summary>
/// Invoked by <see cref="FB.Init(InitDelegate, HideUnityDelegate, string)"/> callback.
/// Tries to log in using Facebook account and authenticates user with Nakama server.
/// </summary>
/// <param name="handler">Invoked after Facebook authorisation.</param>
private void InitializeFacebook(Action<FacebookResponse> handler)
{
    FB.ActivateApp();

    List<string> permissions = new List<string>();
    permissions.Add("public_profile");

    FB.LogInWithReadPermissions(permissions, async result =>
    {
        FacebookResponse response = await ConnectFacebookAsync(result);
        handler?.Invoke(response);
    });
}
/// <summary>
/// Connects Facebook to currently logged in Nakama account.
/// </summary>
private async Task<FacebookResponse> ConnectFacebookAsync(ILoginResult result)
{
    FacebookResponse response = await LinkFacebookAsync(result);
    if (response != FacebookResponse.Linked)
    {
        return response;
    }
    . . .
}

Because Facebook SDK is an external plugin, we have to handle multiple cases where the authentication process might fail, e.g. when the user cancels the Facebook login, connection to Facebook failed or a given Facebook token is already assigned to another account.

//NakamaSessionManager.cs

/// <summary>
/// Tries to authenticate this user using Facebook account. If used facebook account hasn't been found in Nakama
/// database, creates new Nakama user account and asks user if they want to transfer their progress, otherwise
/// connects to account linked with supplied Facebook account.
/// </summary>
private async Task<FacebookResponse> LinkFacebookAsync(ILoginResult result = null)
{
    if (FB.IsLoggedIn == true)
    {
        string token = AccessToken.CurrentAccessToken.TokenString;
        try
        {
            await Client.LinkFacebookAsync(Session, token, true);
            return FacebookResponse.Linked;
        }
        catch (ApiResponseException e)
        {
            if (e.StatusCode == System.Net.HttpStatusCode.Conflict)
            {
                return FacebookResponse.Conflict;
            }
            else
            {
                Debug.LogWarning("An error has occured reaching Nakama server; message: " + e);
                return FacebookResponse.Error;
            }
        }
        catch (Exception e)
        {
            Debug.LogWarning("An error has occured while connection with Facebook; message: " + e);
            return FacebookResponse.Error;
        }
    }
    else
    {
        if (result == null)
        {
            Debug.Log("Facebook not logged in. Call ConnectFacebook first");
            return FacebookResponse.NotInitialized;
        }
        else if (result.Cancelled == true)
        {
            Debug.Log("Facebook login canceled");
            return FacebookResponse.Cancelled;
        }
        else if (string.IsNullOrWhiteSpace(result.Error) == false)
        {
            Debug.Log("Facebook login failed with error: " + result.Error);
            return FacebookResponse.Error;
        }
        else
        {
            Debug.Log("Facebook login failed with no error message");
            return FacebookResponse.Error;
        }
    }
}

The yellow line shows the code provided by Nakama plugin, other parts are used to determine what happened in case of an exception.

Summary

Authenticated users can communicate with the server for as long as their session doesn’t expire. For easy access, we can store the authentication token in the PlayerPrefs and restore it upon logging in. The next post will cover some of the features Nakama has to offer for our server, how to implement them, as well as some caveats regarding these features and how to overcome them.

 

Paweł Stolarczyk
By Paweł Stolarczyk Senior Unity Developer
SIRBart

Call The Knights!

    Table of contents