Using PlayFab to create a leaderboard in Unity

What is PlayFab

PlayFab is Microsoft's multiplayer solution for live games and offers a few features I use for game jams. I mainly only use the leaderboard portion of PlayFab because it's free for up to 100k unique users which is more than enough for game jam games.

Why I'm using it

When I was working on my game for Boss Rush Jam 2024, I noticed a lot of games had a leaderboard. This seemed like an easy way to add some replayability to your game, so I wanted to add this in future games I work on.

So going into Acerola Jam 0, I knew I wanted to add some type of leaderboard system. I had some experience with PlayFab in the past when I was looking into it for a multiplayer solution. I didn't end up using it because it's fairly pricey or at least it was at the time. But for something like leaderboards or storing simple user data it shouldn't cost anything.

How to set up

  1. Head over to PlayFab and sign up. This should take you to the developer dashboard.

  2. PlayFab sets you up with a starter game. Click it to go to the game dashboard.

  3. If you would like to change the name of the game. Click the cog wheel next to the title in the top left. Edit title info. You can change the name here and save.

  4. Go to this link and download the Unity PlayFab SDK. You can then drag and drop the SDK into your unity project or import it in Unity Assets > Import package > Custom package...

  5. Once the package is done importing, you should notice a PlayFab window show up. You'll need to sign in. Click on the log in button so you can login with the account you already created.

  6. Then it should prompt you to download the PlayFab SDK.

  7. When the SDK is finished installing, the PlayFab window will show that the SDK has been installed and there will be a link to Set my title. Click this and update the information here. You should only need to update the Title ID.

  • The developer secret key should be filled out for you. You should not give this to anyone. If you are going to host this project on a public repo, you'll want to dig through the code to see where your login information and this secret are stored and add it to your git ignore file.
  1. Head over to the developer console.
  2. Back to the game dashboard, click the Leaderboards link on the left sidebar.
  3. Click New leaderboard.
  4. Give the leaderboard a name. You'll use this in the code below. I'll call mine TestLeaderboard for this example. You have some options for resetting the leaderboard. I'm going to leave this as manual, but you can do whatever you want. For the Aggregation method, this depends on what you are going to do with your leaderboard. For me, it will be set to maximum.
  5. We can now head back into Unity.

How to use in Unity

The general flow of how this is going to work is:

  1. When the game starts, the user logs in with LoginWithCustomID. Since I'm doing this for a game jam, no one is going to want to create an account so we'll do it behind the scenes for them. On desktop, we'll use SystemInfo.deviceUniqueIdentifier to create a customId for this user. For Web, we'll create a guid and store it with Playerpref. There are a lot of other authentication methods that you can check out here.

  2. Then the user starts to play the game and when they finish (or maybe some other trigger in your game) we'll send the user's score to PlayFab and then request the leaderboard data.

  3. We'll also prompt the player for a name the first time they submit a score. You may want to add some checks around the name like character limit and the type of characters players can put. Also a profanity filter if you want. I didn't add a filter and was fine checking names as they came in since it was a game jam game.

  4. Then after we have submitted the score and prompted the user, we can show the leaderboard.

  5. I also added a button to show the leaderboard on the main menu.

  6. If there are any errors logging in or using PlayFab there will be fallbacks to skip using PlayFab.

The script

For the actual PlayFab script, I'm only including the PlayFabManager and not anything for how it communicates with the UI. That is up to you, but the idea is that each PlayFab function has two callbacks for success and error. So if we try to log the user in and they get a success then we're good to go, but if there is an error. We would have a flag set up so that we know to try to call any PlayFab functions going forward. Or if we try to call for the leaderboard and there is an error, we skip showing the leaderboard and move on to the next screen or scene.

Let's talk about the script.

  1. I'm making this a singleton and using DontDestroyOnLoad on it. DontDestroyOnLoad means that the gameObject will persist through game scenes. I generally structure my games so that I have a few managers that I create in a Main Menu scene, and then they get moved from scene to scene. This way, we will create the PlayFab Manager once and we can use it anywhere.

  2. We want the user to log in and have an ID. But since this is a game jam game, most players would probably just not do it. So to get around this, we can log the user in with a custom ID. For desktop, this is simple. We can use SystemInfo.deviceUniqueIdentifier to get your computer's custom hardware ID. For Web, we'll need to use PlayerPrefs to store a unique identifier. We'll be making use of Conditional compilation. This will allow us to run code for different builds based on the build type. Below you can see that for Webgl builds we use PlayerPrefs, but for everything else we use SystemInfo.deviceUniqueIdentifier. Note: I have not tried this for mobile. You may need a third solution if you want to also try this for mobile devices.

#if UNITY_WEBGL
    if (!PlayerPrefs.HasKey("UniqueIdentifier"))
    {
        PlayerPrefs.SetString("UniqueIdentifier", Guid.NewGuid().ToString());
    }

    deviceId = PlayerPrefs.GetString("UniqueIdentifier");
#else
    deviceId = SystemInfo.deviceUniqueIdentifier;
#endif
  1. You'll need to have PlayFabClientAPI imported and ready to go. We'll be making use of the PlayFabClientAPI functions: LoginWithCustomID, UpdatePlayerStatistics, UpdateUserTitleDisplayName and GetLeaderboard. Each of these functions takes in a few parameters including success and error callbacks. We'll be setting up these functions to talk to our UI to display things like leaderboards or errors.

At this point you're probably good to check out the script. Nothing in here is really that complicated.

The main flow is that when we load the script into our main menu we'll try to log them in. If they're a new user we'll create the account and they won't have a username. So at some point we'll need to prompt them to update it. For me, I ask the user to name themselves when they go to see the leaderboard for the first time in the gameplay. We can update the user statistics whenever, probably at the end of a level. Then we can call for the leaderboard data. On successful leaderboard data retrieval, we can loop the data and generate a string to be shown. At any point if we have an error, we can then have functions setup to handle this.

Speaking of leaderboards, PlayFab unfortunately is very limited with its database. There is no sorting and you can only use integers. At first, I thought this was a deal breaker because I wanted to do a time-based leaderboard, so I will need to sort and have floats. Unity time always caps at 5 decimal places so we can use this later on to solve this problem.

Here is some of the code in the leaderboard function for converting the float to a negative integer:

float newF = (float)item.StatValue;
newF = newF * -1;
newF = newF / 100000;
var newTime = System.TimeSpan.FromSeconds(newF);

playerScores += newTime.ToString("mm\\:ss\\:fff") + "<br>";

Since we can't use floats in the database, let's multiply the time by 100000 so we get an integer from the float. We also can't sort and I want the lowest time to be the best time, but we can multiply the integer by -1. This makes all the entries negative, so now the lowest time is the "best". When we return the leaderboard we can then reverse all of this to get our true times and parse them.

If you do a standard points system you don't have to worry about this though, but I wanted to show that even though PlayFab is limited it can still be used.

using System.Collections.Generic;
using UnityEngine;
using PlayFab;
using PlayFab.ClientModels;
using System;

public class PlayFabManager : MonoBehaviour
{
    public static PlayFabManager Instance { get; private set; }

    private LoginResult loginResult = null;
    private string userName = null;
    private int playerScore;

    private bool alreadyLoggedIn = false;
    private bool failed = false;
    private bool canSubmitName = false;

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private void Start()
    {
        Login();
    }

    private void Login()
    {
        if (failed)
        {
            Debug.LogWarning("PlayFab has failed!");
            return;
        }

        if (alreadyLoggedIn)
        {
            return;
        }

        try
        {
            if (loginResult == null)
            {
                string deviceId = "";

#if UNITY_WEBGL
                if (!PlayerPrefs.HasKey("UniqueIdentifier"))
                {
                    PlayerPrefs.SetString("UniqueIdentifier", Guid.NewGuid().ToString());
                }

                deviceId = PlayerPrefs.GetString("UniqueIdentifier");
#else
                deviceId = SystemInfo.deviceUniqueIdentifier;
#endif
                var request = new LoginWithCustomIDRequest
                {
                    CustomId = deviceId,
                    CreateAccount = true,
                    InfoRequestParameters = new GetPlayerCombinedInfoRequestParams
                    {
                        GetPlayerProfile = true,
                    }
                };

                PlayFabClientAPI.LoginWithCustomID(request, OnLoginSuccess, OnLoginError);
            }
        }
        catch (Exception err)
        {
            failed = true;
            Debug.LogWarning("Failed to login: " + err);
        }
    }

    private void UpdatePlayerScore(int _score)
    {
        if (failed)
        {
            Debug.LogWarning("PlayFab has failed!");
            return;
        }

        try
        {
            var request = new UpdatePlayerStatisticsRequest
            {
                Statistics = new List<StatisticUpdate> {
                    new StatisticUpdate
                    {
                        StatisticName = "TestLeaderboard",
                        Value = playerScore
                    }
                }
            };

            PlayFabClientAPI.UpdatePlayerStatistics(request, OnUpdatePlayerScoreSuccess, OnUpdatePlayerScoreError);
        }
        catch (Exception err)
        {
            failed = true;
            Debug.LogWarning("Failed to update score: " + err);
        }
    }

    public void SetUserName(string _name)
    {
        if (failed)
        {
            Debug.LogWarning("PlayFab has failed!");
            return;
        }
        if (canSubmitName)
        {
            canSubmitName = false;

            try
            {
                var request = new UpdateUserTitleDisplayNameRequest
                {
                    DisplayName = _name,
                };

                PlayFabClientAPI.UpdateUserTitleDisplayName(request, OnDisplayNameUpdateSuccess, OnDisplayNameUpdateError);
            }
            catch (Exception err)
            {
                failed = true;
                Debug.LogWarning("Failed to set username: " + err);
            }
        }
    }

    public void GetLeaderboard()
    {
        if (failed)
        {
            Debug.LogWarning("PlayFab has failed!");
            return;
        }

        try
        {
            var request = new GetLeaderboardRequest
            {
                StatisticName = "TestLeaderboard",
                StartPosition = 0,
                MaxResultsCount = 10
            };

            PlayFabClientAPI.GetLeaderboard(request, OnLeaderboardSuccess, OnLeaderboardError);
        }
        catch (Exception err)
        {
            failed = true;
            Debug.LogWarning("Failed to get leaderboard: " + err);
        }
    }

    private void OnLoginSuccess(LoginResult _result)
    {
        alreadyLoggedIn = true;
        loginResult = _result;

        if (_result.InfoResultPayload.PlayerProfile != null)
        {
            userName = _result.InfoResultPayload.PlayerProfile.DisplayName;
        }
    }

    private void OnLoginError(PlayFabError error)
    {
        failed = true;
        Debug.LogError("PlayFab error: " + error);

        // If we fail at the login attempt, we could maybe try again or set up a system that removes all logged in features from the UI.
    }

    private void OnUpdatePlayerScoreSuccess(UpdatePlayerStatisticsResult _result)
    {
        Debug.Log("Leaderboard sent success: " + _result);

        // Once we have updated the score, you can then do something with your UI or maybe move the user to a new scene.
    }

    private void OnUpdatePlayerScoreError(PlayFabError error)
    {
        failed = true;
        Debug.LogError("PlayFab error: " + error);

        // If we fail here, you can show an error in the UI or maybe move the user to the next screen.
    }

    private void OnLeaderboardSuccess(GetLeaderboardResult _results)
    {
        string leaderboardName = "LeaderboardName";
        string playerScores = "";
        string playerNames = "";
        string yourScore = "";

        foreach (var item in _results.Leaderboard)
        {
            playerNames += item.DisplayName + "<br>";

            float newF = (float)item.StatValue;
            newF = newF * -1;
            newF = newF / 100000;
            var newTime = System.TimeSpan.FromSeconds(newF);

            playerScores += newTime.ToString("mm\\:ss\\:fff") + "<br>";
        }

        var yourTime = System.TimeSpan.FromSeconds(GameComms.GameState_GetRawGameTime());
        yourScore = "Time:  " + yourTime.ToString("mm\\:ss\\:fff");

        // Send the leaderboard name, player names, player scores and your current score to your UI to display. In this case, I have three Text Mesh Pro text areas set up to display the leaderboard with these.
    }

    private void OnLeaderboardError(PlayFabError error)
    {
        failed = true;
        Debug.LogError("PlayFab error: " + error);

        // If we fail here, you can show an error in the UI or maybe move the user to the next screen.
    }

    private void OnDisplayNameUpdateSuccess(UpdateUserTitleDisplayNameResult _result)
    {
        userName = _result.DisplayName;
        UpdatePlayerScore(playerScore);
    }

    private void OnDisplayNameUpdateError(PlayFabError error)
    {
        canSubmitName = true;
        Debug.LogError("PlayFab error: " + error);

        // On name submit error, show an error so the user knows that their name was rejected. The user can then retry. This isn't a hard error like the rest so we don't flip the boolean to true here.
    }

    private void OnDestroy()
    {
        Debug.Log("PlayFabManager is being destoryed.");
    }
}

Thanks for getting this far. Hopefully this helped you add a leaderboard to your game.