© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
B. BorsGame Backend Developmenthttps://doi.org/10.1007/978-1-4842-8910-5_5

5. Leaderboards

Balint Bors1  
(1)
Munich, Germany
 

This chapter introduces leaderboards and explains how to implement them in games. Leaderboards are important motivators for players. Achieving the highest rank, gathering the most items, or reaching the highest score all reinforce competition and encourage players to stay in the game longer.

Technically, you will implement a data table containing values assigned to players. A typical scenario is when the leaderboard contains the scores gathered by players. The game then shows the highest scores, and the players compete to be the best.

PlayFab provides a simple way to implement this feature, but you can also implement your own custom leaderboards in the cloud with little effort. This chapter shows, through a simple example, this idea and realization.

Components

Looking closer at leaderboards, you can see that they are basically data tables containing values assigned to players, as illustrated in Figure 5-1. A leaderboard also needs some data-manipulating algorithms to aggregate and sort values.

Two leaderboards. First, 5 players' positions, names, and scores. Second, the top scorer with their position and name on the leaderboard.

Figure 5-1

Leaderboard

A leaderboard gathers data from multiple players. You store this data in a central database and share it with players if their client requests it. You need to code both the client and the server side.

You need to implement at least the following functions on the client side:
  • Get leaderboard. The clients want to download the actual leaderboard content and show it to the players. For that, you have to specify which leaderboard you are requesting and how many entries you need. A leaderboard can contain many more entries than the game can show, so such a limitation can restrict the traffic.

  • Update leaderboard. The clients should be able to update the leaderboard with some values, such as the number of coins that specific player has gathered. This update will not affect other players and the values will be assigned to that specific player.

  • Show leaderboard. This is the GUI part of the client, where it shows the downloaded leaderboard entries to the player.

On the server side, you need to implement at least the following functionality:
  • Interfaces and connection to the database. The server side should be able to receive requests from the clients and query or update the database based on that.

  • Requested number of entries. If the client only requests a subset of the entries in the leaderboard, the server should be able to filter them out and provide them to the client.

  • Sorting mechanism. A leaderboard contains values assigned to players. When sending values to the client, you have to sort them according to some predefined criteria. For example, you want to send the client ten entries, starting with the highest score.

  • Implement some aggregation method. You need to implement some method to manipulate the incoming player’s values. As an example, for a high score leaderboard, you don’t want to store all the scores a player achieved, only the highest one. So when a player sends an update with its actual score, you have three options: if no update came from that specific player, add the score as a new entry in the database. If there has been a lower score, overwrite it with the latest result. Otherwise, keep the older value and throw away the latest one.

You can also add functionality, just as PlayFab does, for example a reset frequency, where PlayFab wipes the tables regularly. Also, you can have multiple tables for different leaderboards. In the example here, you’ll see how to implement the core functions.

PlayFab

PlayFab provides an easy and fast way to implement leaderboards. It refers to leaderboards as a type of player statistic. Statistics are any values assigned to different players that are typically unranked. On a leaderboard, you want to rank the entries based on some criteria, such as the highest score or the largest number of collected items.

Note

PlayFab is working on a second version of leaderboard, which is at this moment in private preview. It will extend the classic leaderboard with additional functionality, such as the ability to create child leaderboards.

Prerequisites

Before a player can update the leaderboard, it must be authenticated with PlayFab. You can assume that the DisplayName of the player is available, otherwise you would only have the PlayFabID to show on the leaderboard.

Configure PlayFab

Go to PlayFab’s Game Manager and create a new leaderboard, as shown in Figure 5-2. Select Manually for the reset frequency, which means PlayFab will not periodically wipe the table. For the aggregation method, select Maximum (Always Use the Highest Value), because you want to store the highest value.

A screenshot of My game studio software displays inputs for the new leaderboard statistics properties as statistics name, reset frequency, and aggregation method.

Figure 5-2

Configuring a leaderboard in PlayFab

Another important step is to allow clients to send player statistics to PlayFab. You can configure this in PlayFab’s Game Manager, by clicking the gear wheel, choosing Title Settings ➤ API Features, and checking the Allow Client to Post Player Statistics checkbox. Click Save to save the settings.

Implement the Client

Now that you have a leaderboard in PlayFab, you need to access and manipulate it from your clients. For that, you need a GUI and the logic to use the PlayFab API to update and request the leaderboard.

Create a Fancy Leaderboard

You can skip this part and implement your own version of leaderboard, as this is very game specific. For this scenario, let’s create a simple leaderboard.
  1. 1.

    Create a Canvas (right-click the Hierarchy window and choose UI ➤ Canvas).

     
  2. 2.

    Under the canvas, create an empty leaderboard game object. Set the Width to 300 and the Height to 200. Add an Image component to it, with some nice color. Also add a Shadow with Effect Distance 5 and -5, as well as a Vertical Layout Group component.

     
  3. 3.

    Under the Leaderboard, add a new empty game object called Header. Set the Width to 300 and the Height to 30, and add a Horizontal Layout Group component to it.

     
  4. 4.

    Under Header, add three empty game objects: PositionText, PlayerText, and ScoreText. Add a Text component to each with the related name in the Text field. Set the Font Size to 16 and the Alignment to Center.

     
  5. 5.

    Duplicate (right-click the Hierarchy window and choose Duplicate) the Header game object, rename the resulted game object to Row, and create a prefab from it (by dragging-and-dropping into the Prefab folder in the Project window). You can now remove the row from the Hierarchy window, as you will generate the rows dynamically.

     

Now you have a basic leaderboard; you just need to fill it with data from the backend.

Updating the Leaderboard

In Unity Editor, create a new C# script called PlayFabLeaderboard.cs. Add this script to the PlayFab game object. By calling the PlayFab API, you can simply update the leaderboard.
using System.Collections.Generic;
using UnityEngine;
using PlayFab;
using PlayFab.ClientModels;
public class PlayFabLeaderboard : MonoBehaviour
{
    public void UpdatePlayFabLeaderboard()
    {
        PlayFabClientAPI.UpdatePlayerStatistics(
            new UpdatePlayerStatisticsRequest
            {
                Statistics = new List<StatisticUpdate>
                {
                    new StatisticUpdate
                    {
                        StatisticName = "HighScore",
                        Value = UnityEngine.Random.Range(0,100)
                    }
                }
            },
            (UpdatePlayerStatisticsResult result) =>
            {
                Debug.Log("Leaderboard updated.");
            },
            (PlayFabError error) =>
            {
                Debug.LogError(error.GenerateErrorReport());
            });
    }
}

You will test this as soon as you implement a trigger button on the control panel. But first, let’s implement the leaderboard reading method.

Getting the Leaderboard

Let’s add the GetPlayFabLeaderboard() method to the PlayFabLeaderboard script, while keeping the UpdatePlayFabLeaderboard() method.

You need to call the PlayFab API with the name of the leaderboard (in PlayFab’s terminology every leaderboard is a statistic), and the number of values you want to receive. You want to fill the leaderboard on the canvas with the result of the API call.

Don’t forget to drag-and-drop the Leaderboard game object and the Row prefab into the PlayFabLeaderboard component in the Inspector. In this way, the GetPlayFabLeaderboard method can instantiate for each entry in the leaderboard a new row under the Leaderboard game object.
using UnityEngine.UI;
public class PlayFabLeaderboard : MonoBehaviour
{
    public Transform leaderboard;
    public GameObject leaderboardRow;
    GameObject[] leaderboardEntries;
    public GameObject canvas;
    public void GetPlayFabLeaderboard()
    {
        PlayFabClientAPI.GetLeaderboard(
            new GetLeaderboardRequest
            {
                StatisticName = "HighScore",
                StartPosition = 0,
                MaxResultsCount = 6
            },
            (GetLeaderboardResult result) =>
            {
                canvas.SetActive(true);
                leaderboardEntries = new GameObject[result.Leaderboard.Count];
                for (int i = 0; i < result.Leaderboard.Count; i++)
                {
                    leaderboardEntries[i] = Instantiate(leaderboardRow, leaderboard);
                    Text[] texts = leaderboardEntries[i].GetComponentsInChildren<Text>();
                    texts[0].text = result.Leaderboard[i].Position.ToString();
                    texts[1].text = result.Leaderboard[i].DisplayName;
                    texts[2].text = result.Leaderboard[i].StatValue.ToString();
                }
            },
            (PlayFabError error) =>
            {
                Debug.LogError(error.GenerateErrorReport());
            });
    }
}
Finally, implement a closing method, where you destroy all the row game objects, delete the values from the array, and turn off the leaderboard canvas. Add this method (while keeping the existing ones) to the PlayFabLeaderboard script as well:
using System;
public class PlayFabLeaderboard : MonoBehaviour
{
public void ClosePlayFabLeaderboard()
    {
        for (int i = 0; i < leaderboardEntries.Length; i++)
        {
            Destroy(leaderboardEntries[i]);
        }
        Array.Clear(leaderboardEntries, 0, leaderboardEntries.Length);
        canvas.SetActive(false);
    }
}

Extending the Control Panel

You need to extend the control panel and trigger the earlier described functions to see if everything is working fine:
public class ControlPanel : MonoBehaviour
{
    public const int PLAYFAB_GETLEADERBOARD = 5;
    void OnGUI()
    {
        if (selection == PLAYFAB_GETLEADERBOARD)
            GUILayout.Window(0, new Rect(0, 0, 300, 0), GetPlayFabLeaderboard, "PlayFab Leaderboard");
    }
    void OptionsWindow(int windowID)
    {
        if (GUILayout.Button("Get PlayFab Leaderboard"))
        {
            playFab.GetComponent<PlayFabLeaderboard>().GetPlayFabLeaderboard();
            selection = PLAYFAB_GETLEADERBOARD;
        }
    }
    void GetPlayFabLeaderboard(int windowID)
    {
        if (GUILayout.Button("Update PlayFab Leaderboard"))
            playFab.GetComponent<PlayFabLeaderboard>().UpdatePlayFabLeaderboard();
        if (GUILayout.Button("Cancel"))
        {
            playFab.GetComponent<PlayFabLeaderboard>().ClosePlayFabLeaderboard();
            selection = ROOTMENU;
        }
    }
}

With that, you see how easy to implement a leaderboard with the help of PlayFab. With very little effort, you can improve the player engagement and level up your game with this nice feature. Next, you see how to implement the same functionality using Azure.

Azure

You can implement a basic leaderboard using pure cloud services simply. As illustrated in Figure 5-3, you will only implement a GET and a POST HTTP request.

A chart of the azure leaderboard includes players, A P I management services, azure function applications, and database.

Figure 5-3

Leaderboard on Azure

You can use the same basic architecture, where the player calls a serverless function through the API Management service, which in turn updates or queries the database with the actual data.

Note that the implementation is very similar to the one involving matchmaking in Chapter 4. Refer to that section for a more detailed explanation.

Prerequisites

Similarly to PlayFab, this implementation also requires authenticated players. Otherwise, the client would not know the current player, and so the server cannot assign any values to it.

You will reuse the following shared components: API Management service, Azure Function App, and Azure Database for PostgreSQL.

Note

We defined the shared resources in Chapter 5. You will reuse those resources in this and the following chapters.

Initialize Terraform with New Resources

You’ll add a new module to the existing Terraform scripts. Create a new folder called leaderboard and refer it from the main.tf file:
module "leaderboard" {
  source                     = "./leaderboard"
  api_management_name        = module.shared.api_management_name
  shared_resource_group_name = module.shared.shared_resource_group_name
  app_service_plan_id        = module.shared.app_service_plan_id
  storage_account_name       = module.shared.storage_account_name
  storage_account_access_key = module.shared.storage_account_access_key
}

Pass parameters of the shared components, such as the name of the API Management service to the leaderboard module. You will set these in variable blocks for each resource later. When you add a new module to Terraform, you have to reinitialize it with terraform init.

In the newly created leaderboard folder, create a new resource group for the leaderboard-specific resources. Create a new file called resourcegroup.tf and add the following code:
resource "azurerm_resource_group" "leaderboard" {
  name     = "leaderboard-resources"
  location = "West Europe"
}

Leaderboard Azure Functions

Create a new function app called leaderboard-azure-functions, which reuses your shared App Service Plan and Storage Account. You can create a new file called functionapp.tf in the leaderboard folder and add the following code:
variable "app_service_plan_id" {
  description = "AppService Plan ID"
}
variable "storage_account_name" {
  description = "Storage account name"
}
variable "storage_account_access_key" {
  description = "Storage account access key"
}
resource "azurerm_function_app" "leaderboard" {
  name                       = "leaderboard-azure-functions"
  location                   = azurerm_resource_group.leaderboard.location
  resource_group_name        = azurerm_resource_group.leaderboard.name
  app_service_plan_id        = var.app_service_plan_id
  storage_account_name       = var.storage_account_name
  storage_account_access_key = var.storage_account_access_key
  version                    = "~4"
}
data "azurerm_function_app_host_keys" "leaderboard" {
  name                = azurerm_function_app.leaderboard.name
  resource_group_name = azurerm_resource_group.leaderboard.name
}

Creating a Leaderboard API

Update the API Management service with a new Leaderboard API and add the two new operations (GET and POST) to it. These point to the respective Function App functions, and they require authentication tokens through the Azure AD. Create a new file called apim.tf and add the following code to it:
variable "api_management_name" {
    description = "Name of the API Management service"
}
variable "shared_resource_group_name" {
  description = "Shared resource group name"
}
resource "azurerm_api_management_api" "leaderboard" {
  name                  = "leaderboard-api"
  resource_group_name   = var.shared_resource_group_name
  api_management_name   = var.api_management_name
  revision              = "1"
  path                  = "leaderboard"
  display_name          = "Leaderboard API"
  protocols             = ["https"]
  subscription_required = false
}
resource "azurerm_api_management_api_operation" "leaderboard-post" {
  operation_id        = "leaderboard-post"
  api_name            = azurerm_api_management_api.leaderboard.name
  api_management_name = var.api_management_name
  resource_group_name = var.shared_resource_group_name
  display_name        = "POST Leaderboard"
  method              = "POST"
  url_template        = "/Leaderboard"
}
resource "azurerm_api_management_api_operation" "leaderboard-get" {
  operation_id        = "leaderboard-get-ticket"
  api_name            = azurerm_api_management_api.leaderboard.name
  api_management_name = var.api_management_name
  resource_group_name = var.shared_resource_group_name
  display_name        = "GET Leaderboard"
  method              = "GET"
  url_template        = "/Leaderboard"
}
resource "azurerm_api_management_backend" "leaderboard" {
  name                = "leaderboard-azure-functions-backend"
  resource_group_name = var.shared_resource_group_name
  api_management_name = var.api_management_name
  protocol            = "http"
  url                 = "https://${azurerm_function_app.leaderboard.default_hostname}/api/"
  resource_id         = "https://management.azure.com/${azurerm_function_app.leaderboard.id}"
  credentials {
    header = {
      x-functions-key = "${data.azurerm_function_app_host_keys.leaderboard.default_function_key}"
    }
  }
}
resource "azurerm_api_management_api_policy" "leaderboard" {
  api_name            = azurerm_api_management_api.leaderboard.name
  api_management_name = azurerm_api_management_api.leaderboard.api_management_name
  resource_group_name = azurerm_api_management_api.leaderboard.resource_group_name
  xml_content = <<XML
<policies>
  <inbound>
    <set-backend-service id="apim-policy" backend-id="leaderboard-azure-functions-backend" />
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
            <openid-config url="https://gamebackend2022.b2clogin.com/gamebackend2022.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_SignInSignUp" />
            <audiences>
                <audience>e4d1e07d-81ff-45b8-b849-91bef09b5bbd</audience>
            </audiences>
            <issuers>
           <issuer>https://gamebackend2022.b2clogin.com/fe46bdde-805c-4a42-83c6-871e0f4278e7/v2.0/</issuer>
            </issuers>
        </validate-jwt>
  </inbound>
</policies>
XML
  depends_on = [
        azurerm_api_management_backend.leaderboard
  ]
}

Note that the openid-config URL, audience, and the issuer should represent the actual values from your setup.

At this point, you can execute the Terraform script with terraform apply and verify that it runs without any issues.

Configure and Extend the Database

You have to do some configuration steps for the database. There are the same as in the previous chapter. You disable secure transport and add firewall rules so that you can access the database from your local machine and from Azure Functions.
az postgres flexible-server parameter set --name require_secure_transport --value off --resource-group shared-resources --server-name sharedpostgresqlserver
az postgres flexible-server firewall-rule create --resource-group shared-resources --name sharedpostgresqlserver --rule-name myclientaccess --start-ip-address <your local IP>
az postgres flexible-server firewall-rule create --resource-group shared-resources --name sharedpostgresqlserver --rule-name AllAzureServices --start-ip-address 0.0.0.0
You need to add a new table for the leaderboard to your existing shared PostgreSQL database. Create a file, for example createtable.sql, and add the following:
CREATE TABLE "Leaderboard" (
"PlayerID" varchar(150) NOT NULL PRIMARY KEY,
"Value" INT NOT NULL,
"DisplayName" varchar(150))
Execute it with Azure CLI:
az postgres flexible-server execute --admin-user sharedadmin --admin-password SharedPassword99 --name sharedpostgresqlserver --database-name shared-postgresql-database --file-path createtable.sql
Finally, note the connection string:
az postgres flexible-server show-connection-string -s sharedpostgresqlserver -u sharedadmin -p SharedPassword99 -d shared-postgresql-database

Implement Azure Functions

In this section, you implement the leaderboard logic. You can find more details about implementing Azure Functions in Chapter 4.

Go to your leaderboards folder and run the following command with Azure Core Tools to create your new project:
func init LeaderboardFunctions –-dotnet
Add the needed package references to LeaderboardFunctions.csproj:
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.2" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.0.1" />
    <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.3" />
  </ItemGroup>
You will implement an entity framework to access and manipulate the database. First add the connection string to the local.settings.json file:
{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet",
        "SqlConnectionString" : "Server=sharedpostgresqlserver.postgres.database.azure.com;Database=shared-postgresql-database;Port=5432;User Id=sharedadmin;Password=SharedPassword99;"
    }
}
Create a new LeaderboardContext.cs file:
using Microsoft.EntityFrameworkCore;
namespace LeaderboardFunctions
{
    public class LeaderboardContext : DbContext
    {
        public LeaderboardContext(DbContextOptions<LeaderboardContext> options) : base(options)
        {
        }        public DbSet<LeaderboardEntry> Leaderboard { get; set; }
    }
}
And create a StartUp class, which will start whenever the Function App starts:
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
[assembly: FunctionsStartup(typeof(LeaderboardFunctions.StartUp))]
namespace LeaderboardFunctions
{
    public class StartUp : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            string connectionString = Environment.GetEnvironmentVariable("SqlConnectionString");
            builder.Services.AddDbContext<LeaderboardContext>(options => options.UseNpgsql(connectionString));
        }
    }
}
Create a LeaderboardEntry, which maps to each record in the database:
using System.ComponentModel.DataAnnotations;
namespace LeaderboardFunctions
{
    public class LeaderboardEntry
    {
        [Key]
        public string PlayerID { get; set; }
        public int Value { get; set; }
        public string DisplayName { get; set; }
    }
}
Finally, create the core function, which is triggered by HTTP calls from the client. You implemented two different HTTP methods:
  • HTTP GET. You simply read all the records from the database into a list. Then you sort the list in descending order and send the requested number of entries in the response.

  • HTTP POST. You check if there is a record with the same player ID. If yes, and if its value is smaller than the new value just arrived from the client, you update the record in the database. If the player ID does not yet exist in the database, you simply add to it.

Note that you can implement this in a lot of ways. The current way of implementation is just an example. Create a new file called Leaderboard.cs and add the following code to it:
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Linq;
using System.Collections.Generic;
namespace LeaderboardFunctions
{
    public class Leaderboard
    {
        private readonly LeaderboardContext leaderboardContext;
        public Leaderboard(LeaderboardContext leaderboardContext)
        {
            this.leaderboardContext = leaderboardContext;
        }
        [FunctionName("Leaderboard")]
        public async Task<IActionResult> LeaderboardEntry(
              [HttpTrigger(AuthorizationLevel.Anonymous, "post", "get", Route = null)] HttpRequest req,
              ILogger log)
        {
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            var data = JsonConvert.DeserializeObject<LeaderboardEntry>(requestBody);
            string responseMessage = "";
            if (req.Method.Equals("POST"))
            {
               var leaderboardEntry = new LeaderboardEntry { PlayerID = data.PlayerID, Value = data.Value, DisplayName = data.DisplayName };
               var oldLeaderboardEntry = leaderboardContext.Leaderboard.Find(data.PlayerID);
                if (oldLeaderboardEntry!=null) {
                    log.LogInformation("old: " + oldLeaderboardEntry.Value + " new: " + data.Value );
                    if (oldLeaderboardEntry.Value < data.Value) {
                        leaderboardContext.Entry(oldLeaderboardEntry).State = Microsoft.EntityFrameworkCore.EntityState.Detached;
                        leaderboardContext.Leaderboard.Update(leaderboardEntry);
                    }
                } else {
                    log.LogInformation("add");
                    leaderboardContext.Entry(leaderboardEntry).State = Microsoft.EntityFrameworkCore.EntityState.Detached;
                    leaderboardContext.Leaderboard.Add(leaderboardEntry);
                }
                leaderboardContext.SaveChanges();
            }
           else if (req.Method.Equals("GET"))
            {
                List<LeaderboardEntry> list = new List<LeaderboardEntry>();
                list = leaderboardContext.Leaderboard.ToList();
                list.Sort((s1, s2) => s1.Value.CompareTo(s2.Value));
                list.Reverse();
                int maxResultsCount = int.Parse(req.Query["MaxResultsCount"]);
                if (maxResultsCount > list.Count) maxResultsCount = list.Count;
                list = list.GetRange(0, maxResultsCount);
                string result = JsonConvert.SerializeObject(list);
                responseMessage = result;
            }
            return new OkObjectResult(responseMessage);
        }
    }
}
You can now test if your functions and the database work correctly:
func start
curl -X POST -d "{PlayerID: 'abc', Value: '123'}" http://localhost:7071/api/Leaderboard
curl -X GET http://localhost:7071/api/Leaderboard?MaxResultsCount=6

Publishing Azure Functions

Now that the functions work fine locally, you need to publish them to Azure. Set the connection string in the local settings. Let’s upload it to Azure as well:
az functionapp config appsettings set -n leaderboard-azure-functions -g leaderboard-resources --settings SqlConnectionString="Server=sharedpostgresqlserver.postgres.database.azure.com;Database=shared-postgresql-database;Port=5432;User Id=sharedadmin;Password=SharedPassword99;"
And then publish the functions:
func azure functionapp publish leaderboard-azure-functions
You can now test the function from the Azure Portal or by using cURL:
az functionapp function show --function-name Leaderboard --name shared-azure-functions --resource-group shared-resources --query "invokeUrlTemplate" --output tsv
curl -X POST -d "{PlayerID: 'abc', Value: '123'}" https://leaderboard-azure-functions.azurewebsites.net/api/leaderboard
curl -X GET https://leaderboard-azure-functions.azurewebsites.net/api/leaderboard?MaxResultsCount=6

Implement the Client in Unity

Similarly to PlayFab, you’ll implement three functions for requesting, updating, and closing the leaderboard.

You will use simple HTTP calls. In the header, you always have to include the authorization token, otherwise the API Management service will refuse the request.

During the Azure login process, you stored the displayName and playerID values in the AzureSettings component. You can submit these in the update function with some random value to the leaderboard.

Create a new script called AzureLeaderboard.cs and add the UpdateAzureLeaderboard function to it:
using UnityEngine;
using System.Net;
using System.IO;
using System.Text;
public class AzureLeaderboard : MonoBehaviour
{
    public Transform leaderboard;
    public GameObject leaderboardRow;
    public GameObject canvas;
    GameObject azure;
    private void Start()
    {
        azure = GameObject.Find("Azure");
    }
    public void UpdateAzureLeaderboard()
    {
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://shared-apim.azure-api.net/leaderboard/Leaderboard");
        request.Method = "POST";
        request.Headers.Add("Authorization", "Bearer " + GetComponent<AzureSettings>().token);
        string displayName = azure.GetComponent<AzureSettings>().displayName;
        string playerID = azure.GetComponent<AzureSettings>().playerID;
        byte[] data = Encoding.ASCII.GetBytes("{PlayerID: '" + playerID + "', Value: '" + UnityEngine.Random.Range(0, 100) + "', DisplayName: '" + displayName + "'}");
        request.ContentType = "application/json";
        request.ContentLength = data.Length;
        Stream requestStream = request.GetRequestStream();
        requestStream.Write(data, 0, data.Length);
        requestStream.Close();
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        StreamReader reader = new StreamReader(response.GetResponseStream());
        string result = reader.ReadToEnd();
        Debug.Log("Update finished. " + result);
    }
}
When requesting the leaderboard, you convert the received JSON into a list and display the values for the player. Let’s implement the GetAzureLeaderboard method:
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEngine.UI;
public class AzureLeaderboard : MonoBehaviour
{
    public Transform leaderboard;
    public GameObject leaderboardRow;
    GameObject[] leaderboardEntries;
    public GameObject canvas;
    GameObject azure;
    private void Start()
    {
        azure = GameObject.Find("Azure");
    }
    public void GetAzureLeaderboard()
    {
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://shared-apim.azure-api.net/leaderboard/Leaderboard?MaxResultsCount=6");
        request.Method = "GET";
        request.Headers.Add("Authorization", "Bearer " + GetComponent<AzureSettings>().token);
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        StreamReader reader = new StreamReader(response.GetResponseStream());
        string result = reader.ReadToEnd();
        List<LeaderboardEntry> list = JsonConvert.DeserializeObject<List<LeaderboardEntry>>(result);
        canvas.SetActive(true);
        leaderboardEntries = new GameObject[list.Count];
        for (int i = 0; i < list.Count; i++)
        {
            leaderboardEntries[i] = Instantiate(leaderboardRow, leaderboard);
            Text[] texts = leaderboardEntries[i].GetComponentsInChildren<Text>();
            texts[0].text = i.ToString();
            texts[1].text = list[i].DisplayName;
            texts[2].text = list[i].Value.ToString();
        }
    }
}
public class LeaderboardEntry
{
    public string PlayerID { get; set; }
    public int Value { get; set; }
    public string DisplayName { get; set; }
}
Finally, implement a CloseAzureLeaderboard method to clean up the array and hide the leaderboard panel:
using System;
public class AzureLeaderboard : MonoBehaviour
{
    public void CloseAzureLeaderboard()
    {
        for (int i = 0; i < leaderboardEntries.Length; i++)
        {
            Destroy(leaderboardEntries[i]);
        }
        Array.Clear(leaderboardEntries, 0, leaderboardEntries.Length);
        canvas.SetActive(false);
    }
}

With that, you have all the functions you need to communicate with the backend. In Unity, you can add the AzureLeaderboard component to the Azure game object in the Inspector. Then, drag-and-drop the Canvas, Leaderboard, and Row from the hierarchy to the AzureLeaderboard component.

Extending the Control Panel

Finally, you should also implement new buttons on the control panel to call these functions. Extend the ControlPanel script with the following code:
public class ControlPanel : MonoBehaviour
{
    public const int AZURE_GETLEADERBOARD = 6;
    void OnGUI()
    {
        if (selection == AZURE_GETLEADERBOARD)
            GUILayout.Window(0, new Rect(0, 0, 300, 0), GetAzureLeaderboard, "Azure Leaderboard");
    }
    void OptionsWindow(int windowID)
    {
        if (GUILayout.Button("Get Azure Leaderboard"))
        {
            azure.GetComponent<AzureLeaderboard>().GetAzureLeaderboard();
            selection = AZURE_GETLEADERBOARD;
        }
    }
    void GetAzureLeaderboard(int windowID)
    {
        if (GUILayout.Button("Update Azure Leaderboard"))
            azure.GetComponent<AzureLeaderboard>().UpdateAzureLeaderboard();
        if (GUILayout.Button("Cancel"))
        {
            azure.GetComponent<AzureLeaderboard>().CloseAzureLeaderboard();
            selection = ROOTMENU;
        }
    }
}

To test this, log in through Azure, then update the leaderboard with values. You now have a working Azure-based leaderboard for your game.

Summary

In this chapter, we reviewed the leaderboard functionality and implemented it in PlayFab and Azure. You can see how much easier it is to use PlayFab and its API to implement this feature. However, it’s also not difficult to implement basic leaderboards in Azure. You can use the same or similar building blocks and techniques as in the matchmaking solution. Leaderboards are important features and are great motivators for players to continue playing your game. They are easy to implement, but they add lots of additional value.

Review Questions

  1. 1.

    What is the benefit adding a leaderboard to a game?

     
  2. 2.

    Explain a typical leaderboard. How is the data stored?

     
  3. 3.

    Which functions do you need to implement for the client?

     
  4. 4.

    Which functionality should the server side cover?

     
  5. 5.

    How do you configure PlayFab to use a leaderboard?

     
  6. 6.

    Which PlayFab API calls are used to get and update leaderboards?

     
  7. 7.

    What is a prerequisite for a player to update a leaderboard?

     
  8. 8.

    Which building blocks do you need in Azure to implement leaderboards?

     
  9. 9.

    Why do you compare incoming and stored values in a leaderboard?

     
  10. 10.

    Why do you sort values in the leaderboard?

     
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset