Leaderboards: Example of Implementation¶
Displaying the GOG GALAXY Leaderboards¶
User Experience¶
The user opens the main menu and clicks the Leaderboards button. A new window is opened, leaderboard entries are downloaded and displayed on the screen. The user can change the displayed leaderboard at any point by selecting a different leaderboard from a drop-down menu. He/she can also exit the menu at any point, even when leaderboards are still being downloaded.
Solution¶
This is handled by the LeaderboardsController script attached to the LeaderboardsScreen GameObject in the MainMenu scene. The script itself can be found in Assets/Scripts/UI/MainMenu/LeaderboardsController.cs.
Variables¶
Variable | Description |
---|---|
displayedLeaderboard |
Reference to a dropdown GameObject in the scene |
globalToggle |
Reference to a toggle GameObject in the scene, used to mark if the user wants to display Global leaderboards, or just the entries around the user (players in the current user’s vicinity on the leaderboard) |
message |
Reference to a text GameObject in the scene, used to display a message to inform the user that the leaderboard entries could not be retrieved |
waitingCircularArrow |
Reference to an image GameObject, displayed when the user is waiting for the leaderboard entries to be retrieved |
entriesContainer |
Reference to a GameObject, which will be used as a parent to all leaderboard entries |
entryPrefab |
Prefab used to create leaderboard entries |
entriesList |
List that contains all currently displayed entries (to be easily disposed of later) |
Methods and Usage¶
LeaderboardsController.OnEnable¶
When the LeaderboardScreen menu is enabled, we automatically request the leaderboards for the default settings by calling RequestLeaderboard(bool refresh)
.
LeaderboardsController.RequestLeaderboard¶
public void RequestLeaderboard(bool refresh)
{
if (GameObject.Find("GalaxyManager") == null || !GalaxyManager.Instance.GalaxyFullyInitialized)
{
DisplayMessage("Galaxy not initialized");
}
else
{
message.gameObject.SetActive(false);
waitingCircularArrow.SetActive(true);
if (refresh)
{
DisposeLeaderboard();
}
string leaderboardKey;
GalaxyManager.Instance.leaderboardNames.TryGetValue(displayedLeaderboard.captionText.text, out leaderboardKey);
if (globalToggle.isOn)
{
GalaxyManager.Instance.Leaderboards.RequestLeaderboardEntriesGlobal(leaderboardKey, 0, 100);
}
else
{
GalaxyManager.Instance.Leaderboards.RequestLeaderboardEntriesAroundUser(leaderboardKey, 50, 49, GalaxyManager.Instance.MyGalaxyID);
}
}
}
-
First, this method checks if GalaxyManager is enabled and working. If it is, we activate the waitingCircularArrow GameObject. It will be displayed while entries are being retrieved.
-
Next, we check if we need to dispose of the currently displayed entries by examining the bool
refresh
from the method parameters. -
Finally, if the
globalToggle
is on, we request global leaderboard entries. Otherwise, we only request entries around the user.
LeaderboardEntriesRetrieveListener.OnLeaderboardEntriesRetrieveSuccess Callback¶
The callbacks from the above methods are handled by the LeaderboardEntriesRetrieveListener in Leaderboards class of GalaxyManager (Assets/Scripts/GalaxyManager/Features/Leaderboards.cs), described in the previous article:
public override void OnLeaderboardEntriesRetrieveSuccess(string leaderboardName, uint entryCount)
{
Debug.Log("Leaderboard \"" + leaderboardName + "\" entries retrieved\nEntry count: " + entryCount);
leaderboardEntries.Clear();
leaderboardEntries.TrimExcess();
for (uint i = 0; i < entryCount; i++)
{
GalaxyID userID = new GalaxyID();
uint rank = 0;
int score = 0;
string username = null;
object[] entryDetails = new object[] { rank, score, username };
GalaxyInstance.Stats().GetRequestedLeaderboardEntry(i, ref rank, ref score, ref userID);
username = GalaxyManager.Instance.Friends.GetFriendPersonaName(userID);
entryDetails[0] = rank;
entryDetails[1] = score;
entryDetails[2] = username;
Debug.Log("Created object #" + i + " | " + rank + " | " + score + " | " + username);
leaderboardEntries.Add(entryDetails);
}
if (GameObject.Find("LeaderboardsScreen")) GameObject.Find("LeaderboardsScreen").GetComponent<LeaderboardsController>().DisplayLeaderboard();
}
This callback is fired when the leaderboard entries are retrieved. We use it to gather all retrieved entries and their details in the Leaderboards.leaderboardEntries
list, and then we call the LeaderboardsController.DisplayLeaderboard()
method.
LeaderboardsController.DisplayLeaderboard¶
Let’s go back to the LeaderboardsController script mentioned before:
public void DisplayLeaderboard()
{
GameObject currentObject;
waitingCircularArrow.SetActive(false);
foreach (object[] entry in GalaxyManager.Instance.Leaderboards.LeaderboardEntries)
{
currentObject = Instantiate(entryPrefab, entriesContainer.transform);
currentObject.transform.GetChild(0).GetComponent<Text>().text = entry[2].ToString();
currentObject.transform.GetChild(1).GetComponent<Text>().text = entry[1].ToString();
entriesList.Add(currentObject);
}
}
This method is called when all entries for a specified leaderboard are retrieved and ready to be displayed.
We start by defining the currentObject
local variable for storing reference to the currently instantiated GameObject. Then we disable the waitingCircularArrow GameObject used to inform the user that leaderboards are still being downloaded. Next, we iterate through all object arrays in the Leaderboards.leaderboardEntries
list with a foreach
loop, inside of which we:
- Instantiate GameObject using entryPrefab as the prefab and entriesContainer as the parent, and assign said GameObject to currentObject.
- Set the value of the first text field in the currently instantiated GameObject to the currently processed user’s name.
- Set the value of the second text field in the currently instantiated GameObject to the currently processed user’s score.
- Add the currently instantiated GameObject to the list of displayed entries —
entriesList
— for easier disposal when needed.
LeaderboardsController.DisposeLeaderboard¶
void DisposeLeaderboard()
{
waitingCircularArrow.SetActive(true);
foreach (GameObject child in entriesList)
{
Destroy(child);
}
entriesList.Clear();
entriesList.TrimExcess();
}
This method is used to dispose of all currently displayed entries when the LeaderboardScreen GameObject is disabled, or when the currently displayed leaderboard is changed.
First, it enables waitingCircularArrow GameObject (so that it can be displayed when the user is waiting for the next leaderboard to be downloaded). Then, it iterates through all GameObjects in the entriesList
list, and destroys them. Finally, it clears and trims entriesList
.
LeaderboardsController.DisplayMessage¶
public void DisplayMessage(string text)
{
message.text = text;
message.gameObject.SetActive(true);
waitingCircularArrow.SetActive(false);
}
This method is used in the LeaderboardEntriesRetrieveListener.OnLeaderboardEntriesRetrieveFailure(string leaderboardName, FailureReason failureReason)
callback, which is a wrapper for the method from the GOG GALAXY SDK to display an appropriate message to inform the user that the leaderboard cannot be downloaded.
It sets the message text to match the one passed as its text parameter, enables the message GameObject, and disables the waitingCircularArrow GameObject.
LeaderboardsController.OnDisable¶
void OnDisable()
{
DisposeLeaderboard();
StopAllCoroutines();
}
On disable, we make sure to call the DisposeLeaderboard()
method so that everything is ready when LeaderboardScreen is opened again, and to stop all co-routines running inside this class.
Setting a New Score in a Leaderboard¶
User Experience¶
A user’s score is updated every time its value surpasses that of the one currently stored in the leaderboard. The process invisible to the user.
Solution¶
This is done directly by calling the Leaderboards.SetLeaderboardScore(string leaderboardName, int score, bool forceUpdate)
method whenever it is required in the game.
Let’s say we want to update the user’s Highest score leaderboard entry each time a single player match is finished. To that end, we have added a few lines to the Local1PlayerGameManager script, which can be found in Assets/Scripts/GameManager.
Methods and Usage¶
Local1PlayerGameManager.Awake¶
if (GameObject.Find("GalaxyManager") && GalaxyManager.Instance.GalaxyFullyInitialized && GalaxyManager.Instance.IsSignedIn())
{
galaxyManagerActive = true;
fouls = GalaxyManager.Instance.StatsAndAchievements.GetStatInt("fouls");
shotsTaken = GalaxyManager.Instance.StatsAndAchievements.GetStatInt("shotsTaken");
}
In the Awake()
method we check if GalaxyManager is active and we set the GameManager.galaxyManagerActive
variable accordingly. We also take this opportunity to update user stats by getting them from the GOG backend for easier use later.
Local1PlayerGameManager.GameEnd¶
public override void GameEnd()
{
if ((shotBallsOnTable.Count == 1 && shotBallsOnTable[0] == BallColorEnum.White) || shotBallsOnTable.Count == 0)
{
endMessage.SetActive(true);
GameFinished = true;
endPlayer1Score.text = "" + playerList[0].Score;
if (galaxyManagerActive)
{
GalaxyManager.Instance.StatsAndAchievements.SetAchievement("winSPRound");
GalaxyManager.Instance.StatsAndAchievements.SetStatFloat("highestScore", playerList[0].Score);
GalaxyManager.Instance.StatsAndAchievements.SetStatFloat("lastScore", playerList[0].Score);
GalaxyManager.Instance.Leaderboards.SetLeaderboardScore("highestScore", playerList[0].Score);
GalaxyManager.Instance.Leaderboards.SetLeaderboardScore("fouls", GalaxyManager.Instance.StatsAndAchievements.GetStatInt("fouls"), true);
GalaxyManager.Instance.Leaderboards.SetLeaderboardScore("shotsTaken", GalaxyManager.Instance.StatsAndAchievements.GetStatInt("shotsTaken"), true);
}
}
}
When the game ends, we first check the value of GameManager.galaxyManagerActive
. If it is set to true
, we set the current user’s leaderboard with the API Key highestScore value to that user’s player score at the end of the game. We also perform a similar operation for two other leaderboards: fouls and shotsTaken).