メインコンテンツまでスキップ

Unreal Engine Module - Add an all time high score leaderboard - Use the Online Subsystem to show the leaderboards

Last updated on January 13, 2024

Unwrap the Subsystem

In this section, you will learn how to get the leaderboard using AccelByte Online Subsystem (OSS). In the Byte Wars project, there is already a Game Instance Subsystem created namely the LeaderboardSubsystem class. This subsystem contains basic leaderboard-related functionality. In this tutorial, you will use a starter version of that subsystem, so you can implement leaderboard functionalities from scratch.

What's in the Starter Pack

To follow this tutorial, we have prepared a starter subsystem class namely LeaderboardSubsystem_Starter for you. This class consists of the following files.

  • Header file can be found in /Source/AccelByteWars/TutorialModules/Engagement/LeaderboardEssentials/LeaderboardSubsystem_Starter.h.
  • CPP file can be found in /Source/AccelByteWars/TutorialModules/Engagement/LeaderboardEssentials/LeaderboardSubsystem_Starter.cpp.

The LeaderboardSubsystem_Starter class has already several functionalities supplied for you.

  • AccelByte OSS interfaces declaration namely LeaderboardInterface and UserInterface. You will use these interfaces to implement leaderboard-related functionalities later.

    protected:
    FOnlineUserAccelBytePtr UserInterface;
    FOnlineLeaderboardAccelBytePtr LeaderboardInterface;
  • Helper function to get UniqueNetId from a PlayerController. You will need this helper to use the AccelByte OSS interfaces mentioned above.

    FUniqueNetIdPtr ULeaderboardSubsystem_Starter::GetUniqueNetIdFromPlayerController(const APlayerController* PC) const
    {
    if (!ensure(PC))
    {
    return nullptr;
    }

    ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
    if (!ensure(LocalPlayer))
    {
    return nullptr;
    }

    return LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId();
    }
  • Helper function to get LocalUserNum from a PlayerController. You will also need this helper to use the AccelByte OSS interfaces.

    int32 ULeaderboardSubsystem_Starter::GetLocalUserNumFromPlayerController(const APlayerController* PC) const
    {
    if (!PC)
    {
    return INDEX_NONE;
    }

    const ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
    if (!LocalPlayer)
    {
    return INDEX_NONE;
    }

    return LocalPlayer->GetControllerId();
    }

Besides the starter subsystem, we have also prepared some constants, delegates, and other helpers in the /Source/AccelByteWars/TutorialModules/Engagement/LeaderboardEssentials/LeaderboardEssentialsModels.h file. In that file, you can find the following helpers.

  • A helper class namely LeaderboardRank which contains the individual player rank information such as display name, rank, and score. You will need this to display the leaderboard entries later.

    UCLASS()
    class ACCELBYTEWARS_API ULeaderboardRank : public UObject
    {
    GENERATED_BODY()

    public:
    FUniqueNetIdRepl UserId;
    int32 Rank;
    FString DisplayName;
    float Score;
    };
  • Delegates that can be used as callback upon the get leaderboard data process is completed.

    DECLARE_DELEGATE_TwoParams(FOnGetLeaderboardRankingComplete, bool /*bWasSuccessful*/, const TArray<ULeaderboardRank*> /*Rankings*/);

Get leaderboard rankings

In this section, you will implement functionality to get leaderboard rankings.

  1. Open the LeaderboardSubsystem_Starter class header file and declare the following function.

    public:
    void GetRankings(const APlayerController* PC, const FString& LeaderboardCode, const int32 ResultLimit, const FOnGetLeaderboardRankingComplete& OnComplete = FOnGetLeaderboardRankingComplete());
  2. Next, declare a callback function to handle when the get leaderboard rankings process is completed.

    protected:
    void OnGetRankingsComplete(bool bWasSuccessful, const int32 LocalUserNum, const FOnlineLeaderboardReadRef LeaderboardObj, const FOnGetLeaderboardRankingComplete OnComplete);
  3. Now, let's define the functions above. Open the LeaderboardSubsystem_Starter class CPP file and define the GetRankings() function first. This function will send a request to get leaderboard rankings around the defined range. In this case, we want to get the leaderboard from rank zero to a certain rank limit. Once completed, it will call the OnGetRankingsComplete() function to handle the callback.

    void ULeaderboardSubsystem_Starter::GetRankings(const APlayerController* PC, const FString& LeaderboardCode, const int32 ResultLimit, const FOnGetLeaderboardRankingComplete& OnComplete)
    {
    if (!ensure(LeaderboardInterface.IsValid()) || !ensure(UserInterface.IsValid()))
    {
    UE_LOG_LEADERBOARD_ESSENTIALS(Warning, TEXT("Cannot get leaderboard rankings. Leaderboard Interface or User Interface is not valid."));
    return;
    }

    if (!ensure(PC))
    {
    UE_LOG_LEADERBOARD_ESSENTIALS(Warning, TEXT("Cannot get leaderboard rankings. PlayerController is null."));
    return;
    }

    const int32 LocalUserNum = GetLocalUserNumFromPlayerController(PC);

    FOnlineLeaderboardReadRef LeaderboardObj = MakeShared<FOnlineLeaderboardRead, ESPMode::ThreadSafe>();
    LeaderboardObj->LeaderboardName = FName(LeaderboardCode);

    // Get the leaderboard within the range of 0 to ResultLimit.
    OnLeaderboardReadCompleteDelegateHandle = LeaderboardInterface->AddOnLeaderboardReadCompleteDelegate_Handle(FOnLeaderboardReadCompleteDelegate::CreateUObject(this, &ThisClass::OnGetRankingsComplete, LocalUserNum, LeaderboardObj, OnComplete));
    LeaderboardInterface->ReadLeaderboardsAroundRank(0, ResultLimit, LeaderboardObj);
    }
  4. Next, define the OnGetRankingsComplete() function. When the get leaderboard rankings request completed, it will return a list of leaderboard members' user ID and their points (in this case is score). To get each member's user information (e.g. display name), you need to query them. Once the user information is queried, this function will return the list of leaderboard rankings to the assigned callback.

    void ULeaderboardSubsystem_Starter::OnGetRankingsComplete(bool bWasSuccessful, const int32 LocalUserNum, const FOnlineLeaderboardReadRef LeaderboardObj, const FOnGetLeaderboardRankingComplete OnComplete)
    {
    ensure(UserInterface);
    ensure(LeaderboardInterface);

    LeaderboardInterface->ClearOnLeaderboardReadCompleteDelegate_Handle(OnLeaderboardReadCompleteDelegateHandle);

    if (!bWasSuccessful)
    {
    UE_LOG_LEADERBOARD_ESSENTIALS(Warning, TEXT("Failed to get leaderboard rankings with code: %s"), *LeaderboardObj->LeaderboardName.ToString());
    OnComplete.ExecuteIfBound(false, TArray<ULeaderboardRank*>());
    return;
    }

    // Collect leaderboard members' player id.
    TPartyMemberArray LeaderboardMembers;
    for (const FOnlineStatsRow& Row : LeaderboardObj->Rows)
    {
    if (Row.PlayerId.IsValid())
    {
    LeaderboardMembers.Add(Row.PlayerId->AsShared());
    }
    }

    // Query leaderboard members' user information.
    OnQueryUserInfoCompleteDelegateHandle = UserInterface->AddOnQueryUserInfoCompleteDelegate_Handle(
    LocalUserNum,
    FOnQueryUserInfoCompleteDelegate::CreateWeakLambda(this, [this, LeaderboardObj, OnComplete](int32 LocalUserNum, bool bWasSuccessful, const TArray<FUniqueNetIdRef>& UserIds, const FString& ErrorStr)
    {
    if (!ensure(UserInterface))
    {
    UE_LOG_LEADERBOARD_ESSENTIALS(Warning, TEXT("Cannot get leaderboard. User Interface is not valid."));
    return;
    }
    UserInterface->ClearOnQueryUserInfoCompleteDelegate_Handle(LocalUserNum, OnQueryUserInfoCompleteDelegateHandle);

    if (!bWasSuccessful)
    {
    UE_LOG_LEADERBOARD_ESSENTIALS(Warning, TEXT("Failed to get leaderboard with code: %s. Error: %s"), *LeaderboardObj->LeaderboardName.ToString(), *ErrorStr);
    OnComplete.ExecuteIfBound(false, TArray<ULeaderboardRank*>());
    return;
    }

    UE_LOG_LEADERBOARD_ESSENTIALS(Warning, TEXT("Success to get leaderboard rankings with code: %s"), *LeaderboardObj->LeaderboardName.ToString());

    // Return leaderboard information along with its members' user info.
    TArray<ULeaderboardRank*> Rankings;
    for (const FOnlineStatsRow& Row : LeaderboardObj->Rows)
    {
    if (!Row.PlayerId.IsValid())
    {
    continue;
    }

    // Get the member's display name.
    const TSharedPtr<FOnlineUser> LeaderboardMember = UserInterface->GetUserInfo(LocalUserNum, Row.PlayerId->AsShared().Get());
    const FString DisplayName = !LeaderboardMember->GetDisplayName().IsEmpty() ?
    LeaderboardMember->GetDisplayName() :
    FText::Format(DEFAULT_LEADERBOARD_DISPLAY_NAME, FText::FromString(Row.NickName.Left(5))).ToString();

    // Get the member's stat value.
    float Score = 0;
    if (Row.Columns.Contains(FName("AllTime_Point")))
    {
    // The stat key is "AllTime_Point" if it was retrieved from FOnlineLeaderboardAccelByte::ReadLeaderboardsAroundRank().
    Row.Columns[FName("AllTime_Point")].GetValue(Score);
    }
    else if (Row.Columns.Contains(FName("Point")))
    {
    // The stat key is "Point" if it was retrieved from FOnlineLeaderboardAccelByte::ReadLeaderboards()
    Row.Columns[FName("Point")].GetValue(Score);
    }

    // Add a new ranking object.
    ULeaderboardRank* NewRanking = NewObject<ULeaderboardRank>();
    NewRanking->UserId = Row.PlayerId;
    NewRanking->Rank = Row.Rank;
    NewRanking->DisplayName = DisplayName;
    NewRanking->Score = Score;
    Rankings.Add(NewRanking);
    }

    OnComplete.ExecuteIfBound(true, Rankings);
    }
    ));

    UserInterface->QueryUserInfo(LocalUserNum, LeaderboardMembers);
    }
  5. Congratulations! Your get leaderboard rankings functionality is completed.

Get player leaderboard ranking

Previously, you have implemented to get leaderboard rankings within some range. In this section, you will implement functionality to get the local player's leaderboard rank, so you can display it later if the player is not included in a certain leaderboard rankings range.

  1. Open the LeaderboardSubsystem_Starter class header file and declare the following function.

    public:
    void GetPlayerRanking(const APlayerController* PC, const FString& LeaderboardCode, const FOnGetLeaderboardRankingComplete& OnComplete = FOnGetLeaderboardRankingComplete());
  2. Now, open the LeaderboardSubsystem_Starter class CPP file and define the function above. This function will send a request to get a certain player's leaderboard rank. The callback will be handled by the OnGetRankingsComplete() function too.

    void ULeaderboardSubsystem_Starter::GetPlayerRanking(const APlayerController* PC, const FString& LeaderboardCode, const FOnGetLeaderboardRankingComplete& OnComplete)
    {
    if (!ensure(LeaderboardInterface.IsValid()) || !ensure(UserInterface.IsValid()))
    {
    UE_LOG_LEADERBOARD_ESSENTIALS(Warning, TEXT("Cannot get player leaderboard ranking. Leaderboard Interface or User Interface is not valid."));
    return;
    }

    if (!ensure(PC))
    {
    UE_LOG_LEADERBOARD_ESSENTIALS(Warning, TEXT("Cannot get player leaderboard ranking. PlayerController is null."));
    return;
    }

    const FUniqueNetIdPtr PlayerNetId = GetUniqueNetIdFromPlayerController(PC);
    if (!ensure(PlayerNetId.IsValid()))
    {
    UE_LOG_LEADERBOARD_ESSENTIALS(Warning, TEXT("Cannot get player leaderboard ranking. Player's UniqueNetId is not valid."));
    return;
    }

    const int32 LocalUserNum = GetLocalUserNumFromPlayerController(PC);

    FOnlineLeaderboardReadRef LeaderboardObj = MakeShared<FOnlineLeaderboardRead, ESPMode::ThreadSafe>();
    LeaderboardObj->LeaderboardName = FName(LeaderboardCode);

    // Get the player's leaderboard ranking.
    OnLeaderboardReadCompleteDelegateHandle = LeaderboardInterface->AddOnLeaderboardReadCompleteDelegate_Handle(FOnLeaderboardReadCompleteDelegate::CreateUObject(this, &ThisClass::OnGetRankingsComplete, LocalUserNum, LeaderboardObj, OnComplete));
    LeaderboardInterface->ReadLeaderboards(TPartyMemberArray{ PlayerNetId->AsShared() }, LeaderboardObj);
    }
  3. Congratulations! Your get player's leaderboard rank functionality is completed.

Resources