Introduction

There are a variety of different Leaderboards types and configurations available from GameSparks. However, not all of these options will be available on other platforms. This topic will cover basic Leaderboard transitions involving setting up a leaderboard, posting scores, returning Leaderboard data and player specific Leaderboard data.

This topic wont cover partitioned Leaderboards. As partitioned Leaderboards are widely used by GameSparks developers, these are covered in their own topic here [link to partitioned leaderboards topic]. It will also not cover all of the functionality of GameSparks leaderboards like social notifications, high scores, top X, etc, as these features are not available to all other platforms. Where available, we will cover some of these topics.

Beamable

With GameSparks, new Leaderboards are set up through the portal, however, when using Beamable, new Leaderboards are created using the Unity editor plugin. Once you have logged into Beamable through the Unity plugin you will need to go to the Beamable Toolbox which can be opened by going to the menu bar -> Windows -> Beamable -> Open Toolbox.

You can see a guide here on how to get setup with the Beamable plugin.

From here you need to click on the Content option to open up the Content Window.

Leaderboards Setup

From the content window you can select Leaderboard from the dropdown menu and then Create Leaderboards. You will see a new Leaderboard created in the content list.

You can rename this Leaderboard by right clicking or double-clicking on it.

Important

There is one more step to allow us to write to the Leaderboard from C#. You will need to check on the content object for your new Leaderboard and click the check-box “Write-Self”. This will allow you to post to the Leaderboard using C#.

To publish your changes to your backend you can click on the Publish button on the Content Manager tab. Once the content is published you will get a message saying the content has been successfully published.

If the Leaberboard was published successfully you will be able to see it by logging into the portal. To open your portal from the editor, click on the Toolbox tab and you’ll see the Open Portal button on the right hand side.

In your portal you should see your new Leaderboard under the Leaderboards tab. You are now ready to start posting scores.

Posting Scores

Next we will cover how to post to the Leaderboard we just created using the C# API. As we are working in Unity, the first thing you need to do is create a new script and attach it to a new GameObject.

In order to access the Arena Leaderboard you will need to add a LeaderboardRef variable to the top of the script. This will be a private serialized field.

using UnityEngine;
using Beamable.Common.Leaderboards;

public class LBManager : MonoBehaviour
{
   [SerializeField]
   private LeaderboardRef _leaderboardRef = null;

When you let the script compile and go back into the editor you can see this field now appears as a dropdown on your GameObject.

Select your Leaderboard from this list and head back into your script so we can start posting to your Leaderboard.

Note - You technically don't need this LeaderboardRef object as you already know the name of your Leaderboard. You could just hard-code that into the API or add another parameter for the Id you can pass into the PostScore function we are about to create.

We are going to need one more new variable which will be our reference back to the Beamable API. We will set this in the Start() function and create a new function for posting scores to the Leaderboard. All this function needs is the score we want to post because we can get the Id of the Leaderboard from that LeaderboardRef object we just set up.

public class LBManager : MonoBehaviour
{
   [SerializeField]
   private LeaderboardRef _leaderboardRef = null;
   private IBeamableAPI beamableAPI = null;

   // Start is called before the first frame update
   async void Start()
   {
       beamableAPI = await Beamable.API.Instance;
   }

   /// <summary>
   /// Posts the given score to the leaderboard
   /// </summary>
   /// <param name="score">The player's current score</param>
   async void PostScore(double score, Dictionary<string, object> stats)
   {
       // Post the score using the LeaderboardService //
       await beamableAPI.LeaderboardService.SetScore(_leaderboardRef.Id, score, stats);
       Debug.Log($"Score Posted {score}");
   }
  }

It's a pretty simple API as you can see. You could add your own logging here but to confirm your score was posted you can always open your portal and click on the Leaderboards tab. You should see one score on your Arena Leaderboard.

The SetScore function updates the player’s score as you can see from this example however there is also a IncrementScore() function which will increment the score by the set amount instead of updating it. This may be useful in some cases.

Supplemental Data

Now that we have seen how we can post scores to our new Leaderboard, let's cover another GameSparks feature, Supplemental Data.

This feature is also available for Beamable and allows you to post additional information along with your score. An example of this might be your AvatarId, or the Id of the car you were using in a racing game, or other stats for your player which would be useful to have when pulling down the Leaderboard for other players to view.

This can also be added to the SetScore() function as a C# Dictionary object. What we are doing here is essentially creating JSON data which we can append to the score.

This is also very simple to add to our PostScore() function.

/// <summary>
/// Posts the given score to the leaderboard
/// </summary>
/// <param name="score">The player's current score</param>
/// <param name="stats">Supplemental data</param>
async void PostScore(double score, Dictionary<string, object> stats)
{
   // Post the score using the LeaderboardService //
   await beamableAPI.LeaderboardService.SetScore(_leaderboardRef.Id, score, stats);
   Debug.Log($"Score Posted {score}");
}

And you can create objects for this API using the existing C# Dictionary API.

Dictionary<string, object> stats = new Dictionary<string, object>();
stats.Add("avatarId", 12);
stats.Add("displayName", "DifficultyDr");
stats.Add("level", 2);
stats.Add("kills", 212);
stats.Add("elo", 2107);
PostScore(3.0d, stats);

After you post your test to the leaderboard, you will see your stats appear as a JSON string.

Returning Leaderboard Data

The next thing to cover in this topic is to be able to fetch the Leaderboard data we just posted. This is also a very simple API which lets us get a range of Leaderboard data between two ranks, for example the Top10 Leaderboard data would be between 0 and 9.

/// <summary>
/// Returns leaderboard entry data between the given ranks
/// </summary>
/// <param name="fromRank">To first rank to include in the response.</param>
/// <param name="toRank"> The last rank to include in the response</param>
async void GetLeaderboardData(int fromRank, int toRank)
{
   LeaderBoardView leaderboadView = await beamableAPI.LeaderboardService.GetBoard(_leaderboardRef.Id, fromRank, toRank);
   foreach (RankEntry entry in leaderboadView.rankings)
   {
       Debug.Log("PlayerId: "+entry.gt);
       Debug.Log("Rank: "+entry.rank);
       Debug.Log("Score: "+entry.score);
       foreach (RankEntryStat stat in entry.stats)
       {
           Debug.Log("Stat: " + stat.name);
       }
   }
}

So that you can see what parameters you are dealing with in the response we’ve added logs which you should be able to see in your own console.

Note - Since we are using asynchronous functions here, our GetLeaderboardData function would ideally return this data and not just print it to the console as you want to do something useful with that data like draw your Leaderboard UI. We will show another example of this flow in the next section.

Returning Player Leaderboard Entries

There are also common cases where you don't want to know the full list of Leaderboard data, you just need to know your own player’s Leaderboard rank or entry data. We can use the LeaderboardService API for this as well.

/// <summary>
/// Returns a single entry for the given playerId
/// </summary>
/// <param name="playerId">The player's Id beamableAPI.User.id</param>
/// <returns>RankEntry object containing the player's leaderboard entry details
async Task<RankEntry> GetPlayerLeaderboardEntry(long playerId)
{
   Debug.Log($"Fetching LB Entry for {playerId}");
   RankEntry entry = await beamableAPI.LeaderboardService.GetUser(_leaderboardRef.Id, playerId);
   return entry;
}

You can see from this function it is actually set up to get any player’s Leaderboard entry, not just the current player, so we need to pass that in.

long playerId = beamableAPI.User.id;
var entry = await GetPlayerLeaderboardEntry(playerId);

And that's it! You can check if the entry is returning valid data using the same debug logs as we showed for getting leaderboard data.

Around-Me Leaderboard Request

Beamable does not have an out-of-the-box alternative to GameSparks AroundMeLeaderboardRequest. However, given the two examples we showed here for getting the player’s current rank, and a range of entries you can see it would be a simple case of using a range +/- the player’s current rank in order to replicate the AroundMeLeaderboardRequest.

AccelByte

In comparison to GameSparks, Accelbyte provides a basic set of Leaderboard features, but they have daily, weekly leaderboards out-of-the-box, as well as timed Leaderboards for events or seasons.

The biggest difference between the two platforms is that AccelByte does not have features for providing social friends, team ids, team types with their Leaderboards.

There is also no API which would allow you to add supplemental data to their Leaderboards for posting additional player data along with scores, for example avatarId, level, displayName, etc. Leaderboards Setup In Gamesparks, we start by creating Leaderboard configuration like shortCode, name, description, high-score notifications and a few others. Similarly, we have to add a new Leaderboard configuration in AccelByte through either admin portal. You can also do this through their REST API, but we won't cover that in this topic.

In order to create a new Leaderboard we have to add a new Stat before creating the Leaderboard. We then associate that Stat with the Leaderboard we will create in a moment.

The Stat is similar to Running Totals in GameSparks. GameSparks Leaderboards consume data from a Running Total while AccelByte takes data from their statistics service. The Running Total in Gamesparks could be an event/attribute that you have created. In AccelByte, the statistics service is called by the game client to update stats (i.e. post a score) which in turn publishes that stat to the Leaderboard and updates it.

Creating New Stats

Under the Statistics category, you can find Configurations options. Here, you can add New Configuration.

As you can see, you can specify whether to increment or decrement, set min and max values, etc. The Set By field allows you to specify who has permission to set the value (Server or client if you only allow the server to post scores).

There are other fields you can see from the image, but we are only concerned with those mentioned above for this topic.

Important

When creating a new Stat for use with a player Leaderboard you need to set the Set As Global option to false. Global stats are not player-specific, so they are not suitable for Leaderboards.

Creating a new Leaderboard

We can create a Leaderboard easily in the portal as shown below.

Once you click Create Leaderboard option above, a small window will popup to configure the Leaderboard with all the required fields.

In contrast to the Leaderboards in Gamesparks, you can specify the date this Leaderboard is going to become active. You can also set a reset time for other types of Leaderboards like Daily, Weekly, Monthly and Seasonal Leaderboards.

AccelByte’s resetting Leaderboards are split into their own Leaderboards which means you will have a separate version for the main (non-resetting Leaderboard) and another for the daily, weekly, etc.

Note that Leaderboard will not be active until its start time and it will consider the latest Stat values once they are started.

Posting Stats/Scores

In AccelByte we don't post scores directly to the Leaderboard API. Instead we use the statistics service API to increment that Stat which is tied to the Leaderboard.

If the player does not have a Stat set up, the statistics service API will automatically create one for them when they call the IncrementUserStatItems() function.

abStatistic = AccelBytePlugin.GetStatistic();
abStatistic.IncrementUserStatItems(statItemOperationResult, OnIncrementUserStatItems);

This will initiate a player Stat with the score given, or based on the default configuration. Next time the score is posted, it will start incrementing the desired value.

We can see the Leaderboard data in the admin portal by going to the Leaderboard category and clicking on the Action Menu to select the View option.

After following the above steps, we can see the leaderboard data as shown below with Rank, Display Name, Username, points.

Updating Stats/Scores You can update the above player Stat using the below API call.

abStatistic.UpdateUserStatItems(statItemOperationResult, OnUpdateUserStatItem);

This will overwrite the Stat value which was previously stored and triggers the Leaderboard to be updated.

Returning Leaderboard Data

Using Gamesparks we can retrieve Leaderboard based on multiple options using the LeaderboardDataRequest. However, with AccelByte, we can only get Leaderboard entry data with very basic details like offset and limit parameters.

Below is an example of how you can return the Top 10 entries of a given Leaderboard.

abLeaderboard.GetRankings(leaderboardCode, LeaderboardTimeFrame.ALL_TIME, 0, 9, OnGetTopTenRanking);

The ALL_TIME enum added to the function above indicates that we want to request these entries from the ALL_TIME Leaderboards, which would be the equivalent of the basic GameSparks non-resetting Leaderboards. The other options available are MONTH, SEASON and TODAY.

The GetRankings function would return the data from which each player leaderboard data can be retrieved.

Returning Player Leaderboard Entry

We can retrieve the player Leaderboard data similar to Gamesparks using below code. In contrast to the GameSparks LeaderboardsEntriesRequest, getting player entries with AccelByte is very simple. We only have to supply the userId, along with the leaderboardCode.

abLeaderboard.GetUserRanking(AccelByteManager.Instance.AuthLogic.GetUserData().userId, leaderboardCode ,OnGetMyRanking);

The GetUserRanking function would return the data from which certain player leaderboard data can be retrieved.

Nakama

Leaderboards in Nakama have many of the same functionality as you would expect in GameSparks, however there is still some customization needed to cover all of the GameSparks Leaderboard features.

Luckily, Nakama runtime environment has a very flexible set of APIs so you will see throughout this topic that practically all of the GameSparks Leaderboard features can be rebuilt using Nakama.

Check out Nakama’s full documentation on leaderboards here for more details. We will cover a lot of those examples in this topic.

Porting Leaderboards

Before starting this process it is important to know that unlike GameSparks and many other alternative platforms, leaderboards in Nakama are not set up from the portal, though you can view them from there as we will see later. Instead, you need to set up a runtime server and have those leaderboards created when your Nakama instance starts.

This will be done in the InitModule function of the main.ts script. You can see more details here on how to set your server up and use the InitModule function.

The InitModule function runs only once when your server boots. You can think of it like the GameSparks OnPublished script which you may be familiar with and runs only when a new snapshot is published to the live environment.

You can also import your existing GameSparks Leaderboard configurations and use those to create new Nakama leaderboards if you would like. This is what we will be doing in this topic and there is a guide on how to do that in our topic on Cloud-Code here

GameSparks Leaderboard Config

Before we start writing any code let us take a look at what a Leaderboard configuration object looks like in GameSparks.

{
    "@id": "/~leaderboards/DAILY_RESET",
    "description": "DAILY_RESET",
    "highScoreNotifications": true,
    "name": "DAILY_RESET",
    "propertySet": null,
    "segmentData": [],
    "shortCode": "DAILY_RESET",
    "snapshotFrequency": "DAILY",
    "socialNotifications": false,
    "team": null,
    "topNNotifications": false,
    "topNThreshold": null,
    "updateFrequency": "REALTIME",
    "~fields": [
      {
        "@id": "/~leaderboards/DAILY_RESET/~fields/score-all",
        "calcType": "MAX",
        "collector": {
          "@ref": "/~runningTotals/POST_SCORE/~collectors/score-all"
        },
        "filterType": "*",
        "filterValue": null,
        "sort": "DESC"
      }
    ]
  }

As you can see there are a few things here we will want to transition. In this topic we will cover, resetting, sort-order, aggregation, filter values and notifications.

The “~fields” array is going to be important because this is where we use the sort-order and aggregation (calcType) for the new Nakama leaderboards. Basic Nakama leaderboards only allow you to aggregate by a single value however there are some examples of how to customize this in our topic on Partitioned Leaderboards here

We will need to parse the calcType and sort fields into a format that is acceptable for Nakama leaderboards. We will also need to do this with the snapshotFrequency field to replicate our resetting leaderboards.

Resetting Leaderboards

Our code for setting the reset frequency of our leaderboards is going to need to convert the GameSparks reset type (“DAILY”, “WEEKLY”, “MONTHLY”) to a cron-string format.

This is very simple and we’ll see an example of this below.

/**
* Returns the reset schedule string based on the GS config detail for the given LB
* @param {string} gsConfigResetFrequency
* @returns {string}
*/
function parseGSLeaderboardReset(gsConfigResetFrequency: string) : string | null {
   switch(gsConfigResetFrequency){
       case "DAILY" :
           return "0 0 * * *";
       case "WEEKLY" :
           return "0 0 * * 0";
       case "MONTHLY" :
           return "0 0 1 * *";
       default : // case for handling 'NEVER'
           return null;
   }
}

As you can see from the example above, this also opens up the possibility of configuring your own custom Leaderboard reset durations in future if you need to.

Aggregation Types

This is the calcType field in the Leaderboard config JSON.

In Nakama this is called the ‘operator’. Nakama does not offer all the options that GameSparks does for aggregation but it does have the basics. We use the nkRuntime.Operator for this in our Nakama runtime environment code.

The options for aggregation type are:

● Operator.INCREMENTAL = SUM ● Operator.BEST = MAX ● Operator.SET = default

If you want to replicate the GameSparks “MIN” aggregation type you can use the “BEST” operator value on a Leaderboard which has a descending sort order. This basically flips the Leaderboard around so that the MAX/BEST aggregation type is running on the lowest score.

/**
* Returns the corresponding Nakama LB operator based on the GS leaderboard field config
* @param {any} gsLBFieldDetails - The field in the GS LB config that will become the score value
* @returns {nkruntime.Operator}
*/
function parseGSLeaderboardAggregation(gsLBFieldDetails: any) : nkruntime.Operator {
   switch(gsLBFieldDetails.calcType){
       case "SUM" :
           return nkruntime.Operator.INCREMENTAL;
       case "MAX" :
           return nkruntime.Operator.BEST;
       case "MIN" :
           // reverse the LB order to get a min LB with the 'BEST' operator
           return nkruntime.Operator.BEST;
       case "LAST" :
           return nkruntime.Operator.SET;
       default :
           // for any cases that arent covered just return the normal 'SET' operator
           return nkruntime.Operator.SET;
   }
}

Sort Order

This is simple as there are only two orders we can choose from, ascending or descending. However, Nakama is using an enumerator for this value so we need to parse that for use with the new Nakama Leaderboards.

/**
* Returns the corresponding Nakama Sort-Order Enum based on the GS leaderboard field config
* @param {any} gsLBFieldDetails - The field in the GS LB config that will become the score value
* @returns {nkruntime.SortOrder}
*/
function parseGSLeaderboardOrder(gsLBFieldDetails: any) : nkruntime.SortOrder {
   return  (gsLBFieldDetails.sort === "DESC")? nkruntime.SortOrder.DESCENDING : nkruntime.SortOrder.ASCENDING;
}

Filter Values

Leaderboards in Nakama can only work with a single score value so any filter values you have assigned to your Leaderboards can only work on one score value. We will need to parse these values as Nakama does not come with this filtering feature out-of-the-box so we will see in the following section how we can use this to create our own custom filtering.

/**
* Created a JSON object about of the current filters in the GS LB config
* @param {any} gsLBFieldDetails - The field in the GS LB config that will become the score value
* @returns {any}
*/
function parseGSLeaderboardFilters(gsLBFieldDetails: any) : any | null {
   // check if there are any filter values set //
   if(gsLBFieldDetails.filterType && gsLBFieldDetails.filterValue){
       return {
           filterType: gsLBFieldDetails.filterType,
           filterValue: gsLBFieldDetails.filterValue
       }
   }
   return null;
}

Now we can start creating our new Nakama Leaderboards.

/**
* Creates new Nakama LB configurations when
* @param logger
* @param nk
*/
function admin_createLeaderboards(logger: nkruntime.Logger, nk: nkruntime.Nakama) {
   logger.info("Creating Nakama LBs from GS Configs...");
   // Get all LB configs //
   const adminUserId   = "00000000-0000-0000-0000-000000000000";
   const collName      = "GSData_Leaderboard";
   let lbConfigCurs: nkruntime.StorageObjectList = nk.storageList(adminUserId, collName);
   logger.info(`Found ${lbConfigCurs.objects?.length} Leaderboard Configs...`);
   // iterate through each LB config //
   if(lbConfigCurs.objects && lbConfigCurs.objects.length > 0){
       lbConfigCurs.objects.forEach(configObj => {
           let lbConfig: any = configObj.value;
           logger.info(`Creating [${lbConfig.shortCode}]`);
           try {
               // setup params for the LB //
               let id : string = lbConfig.shortCode;
               let authoritative : boolean = false;
               // get the sort order of the GS LB //
               let sortOrder =  parseGSLeaderboardOrder(lbConfig['~fields'][0]);
               // get the aggregation type, called the 'operator' in Nakama //
               let operator = parseGSLeaderboardAggregation(lbConfig['~fields'][0]);
               // get the reset frequency and convert it to Nakama reset date //
               let reset = parseGSLeaderboardReset(lbConfig.snapshotFrequency);
               // add any other config or information you might need based on the GS config //
               var metaData: any = {
                   description:        lbConfig.description,
                   name:               lbConfig.name,
                   segmentData:        lbConfig.segmentData,
                   socialNotifications:lbConfig.socialNotifications,
                   topNNotifications:  lbConfig.topNNotifications,
                   topNThreshold:      lbConfig.topNThreshold,
                   // leaderboard score value filtering //
                   filterValue:        parseGSLeaderboardFilters(lbConfig['~fields'][0])
               }
               logger.info(`Id:${id}, auth:${authoritative}, SortOrder:${sortOrder}, Operator:${operator}, Reset:${reset}, MetaData [${JSON.stringify(metaData)}]`);
               nk.leaderboardCreate(id, authoritative, sortOrder, operator, reset, metaData);
               logger.info(`Created [${lbConfig.shortCode}] ...`);
           } catch(error) {
               logger.info(`Error creating LB ${lbConfig.shortCode} ...`);
               logger.error(error.message);
           }
       });   
   }
   else{
       logger.warn("No Leaderboard Configs Found...");
   }
   logger.info("LBs Created...");
}

Before we finish we need to add this function to the InitModule of the main.ts script.

// Server Boot Utils //
admin_createLeaderboards(logger, nk);

Reminder - Remember to add file references to the tsconfig.json file and rebuild and redeploy the server before testing.

Testing

If you have your terminal open when you reboot the server you will see the new logs coming in showing the new leaderboards being created.

You can also check in the Nakama Developer Console to see your leaderboards and the current records. To check your leaderboards click on the “Leaderboards” option on the left-hand side menu or the portal.

If you click on one of your leaderboards you will be able to see the configuration details. You can also view records from here, though you don't have any records in your Leaderboard at this point so we’ll come back to that.

Posting Scores

Now that we have some leaderboards configured let us take a look at how players can post scores to these leaderboards.

/// <summary>
/// Post a score to the given LB for the current player
/// </summary>
/// <param name="session"></param>
/// <param name="lbID"></param>
/// <param name="score"></param>
private async void PostScore(ISession session, string lbID, int score)
{
   IApiLeaderboardRecord postScoreResp = await nakamaClient.WriteLeaderboardRecordAsync(session, lbID, score);
   Debug.Log($"Current Rank [{postScoreResp.Rank}]");
}

If you test out this code you should get the player’s current rank in your Unity console.

We can also check the ranks from the Nakama Developer Console as mentioned before.

Custom RPC

The example above is really basic score submission but what if we wanted to do something more complex?

For this we will need a custom RPC. We will put this RPC function in the same script as the Leaderboard creation code. We are also going to create a class so that we can group all the Leaderboard functionality together.

Let's take a look at a simple example first. All we are going to do is have the RPC post a score on behalf of the player who called it. This is essentially the same as the method we called in Unity above.

class GSLeaderboards {
   /**
    * Posts a score to the leaderboard with the given ID
    * @param logger
    * @param nk
    * @param leaderboardId {string}
    * @param score {number}
    * @returns {string}
    */
   static postScore(leaderboardId: string, score: number, userId: string, userName: string, logger: nkruntime.Logger, nk: nkruntime.Nakama): string {
       logger.info(`Posting Score [${score}] to ${leaderboardId}`);
       // >> post & return rank << //
       let currRecord : nkruntime.LeaderboardRecord = nk.leaderboardRecordWrite(leaderboardId, userId, userName, score);
       logger.info(JSON.stringify(currRecord));
       logger.info(`Current Rank: ${currRecord.rank}`);
       return JSON.stringify({
           rank: currRecord.rank
       });
   }
}

And our RPC function is going to look as follows.

/**
* Posts a score to the leaderboard with the given ID
* @param context
* @param logger
* @param nk
* @param payloadString
*/
function rpcPostScoreCustom(context: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payloadString: string): string {
   logger.info("Data In: " + payloadString);
   let payloadData = JSON.parse(payloadString);
   // grab the request details //
   let lbId    =  payloadData.lbId;
   let score   =  payloadData.score;
   return GSLeaderboards.postScore(lbId, score, context.userId, context.username, logger, nk);
}

And finally we need to add this RPC call to the InitModule function of the main.ts script.

initializer.registerRpc("postScoreCustom", rpcPostScoreCustom);

Reminder - Remember to rebuild and redeploy the server before testing.

Unity Example

Now all we need to do is call this custom function from Unity and let it post scores for our players.

Calling an RPC function to Nakama from Unity is very simple. We just need to know the name of the RPC function and we need to give the RPC function a custom JSON payload.

/// <summary>
/// Post a score to the given LB for the current player
/// </summary>
/// <param name="session"></param>
/// <param name="lbID"></param>
/// <param name="score"></param>
private async void PostScoreCustom(ISession session, string lbId, int score)
{
   string rpcId = "postScoreCustom";
   Dictionary<string, object> requestPayload = new Dictionary<string, object>()
   {
       { "lbId" , lbId },
       { "score" , score }
   };
   var payload = Nakama.TinyJson.JsonWriter.ToJson(requestPayload);
   IApiRpc responseData = await nakamaClient.RpcAsync(session, rpcId, payload);
   Debug.Log(responseData.Payload);
}

If you run this code you will see the rank being returned in the Unity console.

Filter Values

Now that we have a custom Leaderboard posting RPC function lets do something useful with it. The simplest thing we can do is reproduce the GameSparks Leaderboard value filtering.

To do this we can get the Leaderboard config data from the Nakama Storage Engine and then validate the score being posted against the filter value.

You can check what the filter values are for a given Leaderboard by going to the Storage option in the Developer Console.

There are two methods we need to add to our GSLeaderboards class in order to facilitate this. First we are going to have to create a method which will load the config for the leaderboard with the given ID we are posting to. We are going to make this static so we can use it like the Spark.getLeaderboards() function from GameSparks.

/**
 * Returns the leaderboard config for the given shortCode
 * @param {string} lbId
 * @returns {object} lbConfig
 */
static getLeaderboardConfig(lbId: string, nk: nkruntime.Nakama, logger: nkruntime.Logger) : any {
   // first we need to load the Virtual Good config //
   let lbConfigCursor: nkruntime.StorageObject[] = nk.storageRead([{
          collection: "GSData_Leaderboard",
          key: lbId,
          userId: "00000000-0000-0000-0000-000000000000"
   }]);
   if (lbConfigCursor.length > 0) {
         return lbConfigCursor[0].value;
   }
   else{
         // handle errors //
   }
}

Next we will create a private function which we can use to evaluate if the score passess the filter.

/**
* Evaluates if the score being posted passes the filter on the LB
* @param score {number}
* @param filterType {string}
* @param filterValue {number}
* @return {boolean}
*/
private static evaluateScore(score: number, filterType: string, filterValue: number) : boolean {
   // check if there is any filter value at all //
   if(isNaN(filterValue)){
       return true;
   }
   switch(filterType){
       case ">" :
           return (score > filterValue);
       // other cases here for //
       // >=, <=, <, = //
       default :
           return false;
   }
}

And now we can add this check just before where we posted our score to the Leaderboard in the postScore function.

// Check if the LB has any filter values and validate the score being posted //
let lbConfig: any = GSLeaderboards.getLeaderboardConfig(leaderboardId, nk, logger);
// for this example we assume that the score field is the first field in the array //
// you may have to create a function which can get the right filter values for the field used for score //
let filterType = lbConfig["~fields"][0].filterType;
let filterValue = parseInt(lbConfig["~fields"][0].filterValue);
logger.info(`Evaluating Score [${score}], filterType ${filterType}, filterValue ${filterValue}`);
// check if the score value passed the filter //
if(!this.evaluateScore(score, filterType, filterValue)){
   // return an error //
   logger.warn("Validation check failed...");
   throw JSON.stringify({
         "code": 123,
         "error": "validation-failed"
   });
}

You can see below where it fits into the function.

As we are throwing an error if the filter check does not pass we can test this from Unity to see the error log from the console.

Reminder - Remember to rebuild and redeploy the server before testing.

There is a bit more work to do here to get all your filters working for all your Leaderboard fields but this should give you enough context to continue porting your code and to get things working in Nakama as you expect them to in GameSparks.

Leaderboard Notifications

Let us take a look at how you can handle Leaderboard notifications in Nakama. Out-of-the-box, Nakama does not send notifications when scores are posted to the Leaderboard so we will have to put in some custom code to achieve this.

To keep things simple, we will look at the high score notification feature of GameSparks, but the same code can be adapted for TopN or social notifications (see more details about Nakama’s Friends feature here relating to social Leaderboards).

There are two things to note about this process.

  1. We can send messages to players with Nakama in a similar way to the GameSparks SparkMessage API.
  2. Since the player posting the score is going to get a response back in the client anyway, we can avoid sending a message directly to them and instead, we can return additional information in the response. This is more efficient than sending an additional message asynchronously.

As there may be cases where you need to send a notification to the player in this postScore method, we will look at how to achieve both these options. Just keep in mind that an additional message might not be efficient if we can just return the data we need in the response.

The first thing we need to do is get the player’s score before they have posted to the Leaderboard so that we can compare it to the score being posted.

You can stick this code just above where you post your score in the postScore method.

// Get player's current score //
let currScore: number;
let isHighScore: boolean = false;
let lbCursor: nkruntime.LeaderboardRecordList = nk.leaderboardRecordsList(leaderboardId, [userId], 1);
if(lbCursor.records && lbCursor.records.length > 0){
   currScore = lbCursor.records[0].score;
   // Now we can compare to see if this is higher than the previous score //
   isHighScore = (currScore < score);
}

Next, we need the current rank of the player so we can add it to the notification we send after we have posted the score.

You are going to model this notification on the GameSparks NewHighScoreMessage. For example.

So our message code will look like this.

// If this is the player's highest score, send a notification //
if(isHighScore){
   let subject = `You just moved up the ${leaderboardId} leaderboard`;
   let content = {
       leaderboardData: {
           userId:     userId,
           score:      score,
           userName:   userName,
       },
       leaderboardName:        leaderboardId,
       leaderboardShortCode:   leaderboardId,
       notification:           true,
       playerId:               userId,
       rankDetails: {
           rank: currRecord.rank,
       },
       summary: `You just moved up the ${leaderboardId} leaderboard`,
   }
   // The number '1' is a unique code for the notification this could be mapped to an enum of something like  your  message shortCode //                                     //6
   nk.notificationSend(userId, subject, content, 1, "00000000-0000-0000-0000-000000000000", true);
}

And then finally, we will return the isHighScore value with the new rank so we can see that in the client.

return JSON.stringify({
   rank: currRecord.rank,
   isHighScore: isHighScore
});

Altogether, your new postScore function will look something like this.

Reminder - Remember to rebuild and redeploy the server before testing.

If we test this in Unity you should see the response from the RPC call showing the isHighScore field, and another log showing the notification has come through.

Important - The above notification flow requires a socket to be opened so that the client can receive asynchronous messages. This is covered in another topic on Achievements here or the Nakama documentation here.

Team Leaderboards

Although Nakama does not have an out-of-the-box Team Leaderboard feature, it is easy to build this feature yourself using the examples above.

The *leaderboardRecordWrite()** method does not require a userId specifically, you can assign any ID you want to that field.

Therefore, with a custom RPC, you can easily recreate a Team Leaderboard and take advantage of all the Nakama Leaderboard APIs.

let currRecord : nkruntime.LeaderboardRecord = nk.leaderboardRecordWrite(leaderboardId, teamId, teamName, score);

Getting Leaderboard Details

Nakama has similar functionality to GameSparks when it comes to returning and listing Leaderboard records. You can do the following.

There are examples in the links above to Nakama’s docs on how to use these requests so we won't go over them again here.