Skip to main content

Use the OSS to show periodic leaderboards - Weekly leaderboard - (Unreal Engine module)

Last updated on October 24, 2024

Unwrap the Subsystem

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

What's in the Starter Pack

A starter subsystem class has been prepared for you named PeriodicBoardSubsystem_Starter. This class is available in the Resources section and consists of the following files:

  • Header file: /Source/AccelByteWars/TutorialModules/Engagement/PeriodicLeaderboard/PeriodicBoardSubsystem_Starter.h
  • CPP file: /Source/AccelByteWars/TutorialModules/Engagement/PeriodicLeaderboard/PeriodicBoardSubsystem_Starter.cpp

The PeriodicBoardSubsystem_Starter class has several functionalities provided.

  • AGS OSS interfaces declarations named LeaderboardInterface and UserInterface. You will use these interfaces to implement leaderboard-related functionalities later.

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

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

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

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

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

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

    return LocalPlayer->GetControllerId();
    }

You will use the same constants, delegates, and other helpers from All time leaderboard, which are stored in the /Source/AccelByteWars/TutorialModules/Engagement/LeaderboardEssentials/LeaderboardEssentialsModels.h file. In that file, you can find the following helpers:

  • A helper class named LeaderboardRank, which contains the individual player leaderboard 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;

    void Init(const FUniqueNetIdRepl InUserId, const int32 InRank, const FString InDisplayName, const float InScore)
    {
    UserId = InUserId;
    Rank = InRank;
    DisplayName = InDisplayName;
    Score = InScore;
    }
    };
  • Delegates that can be used as a callback when the get leaderboard data process completes.

    DECLARE_DELEGATE_TwoParams(FOnGetLeaderboardRankingComplete, bool /*bWasSuccessful*/, const TArray<ULeaderboardRank*> /*Rankings*/);
  • Delegates that can be used as a callback when the get leaderboard cycle ID process completes.

    // ...
    DECLARE_DELEGATE_TwoParams(FOnGetLeaderboardsCycleIdComplete, bool /*bWasSuccesful*/, const FString& CycleId);

Implement get periodic leaderboard rankings

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

  1. Open the PeriodicBoardSubsystem_Starter class Header file and declare the following function:

    public:
    // ...
    void GetPeriodicRankings(const APlayerController* PC, const FString& LeaderboardCode, const FString& CycleId, const int32 ResultLimit, const FOnGetLeaderboardRankingComplete& OnComplete = FOnGetLeaderboardRankingComplete());
  2. Declare a callback function to handle when the get periodic leaderboard rankings process completes.

    protected:
    // ...
    void OnGetPeriodicRankingsComplete(bool bWasSuccessful, const int32 LocalUserNum, const FOnlineLeaderboardReadRef LeaderboardObj, const FOnGetLeaderboardRankingComplete OnComplete);
  3. Declare a callback function to query user information from the leaderboard list.

    protected:
    // ...
    void OnQueryUserInfoComplete(
    const FOnlineError& Error,
    const TArray<TSharedPtr<FUserOnlineAccountAccelByte>>& UsersInfo,
    const int32 LocalUserNum,
    const FOnlineLeaderboardReadRef LeaderboardObj,
    const FOnGetLeaderboardRankingComplete OnComplete);
  4. Define the functions above. Open the PeriodicBoardSubsystem_Starter class CPP file and define the GetPeriodicRankings() function first. This function will send a request to get leaderboard rankings around the defined range and periodic cycle. Once completed, it will call the OnGetPeriodicRankingsComplete() function to handle the callback.

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

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

    const int32 LocalUserNum = GetLocalUserNumFromPlayerController(PC);

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

    // Get the periodic leaderboard within the range of 0 to ResultLimit.
    OnLeaderboardReadCompleteDelegateHandle = LeaderboardInterface->AddOnLeaderboardReadCompleteDelegate_Handle(FOnLeaderboardReadCompleteDelegate::CreateUObject(this, &ThisClass::OnGetPeriodicRankingsComplete, LocalUserNum, LeaderboardObj, OnComplete));
    LeaderboardInterface->ReadLeaderboardCycleAroundRank(0, ResultLimit, CycleId, LeaderboardObj);
    }
  5. Define the OnGetPeriodicRankingsComplete() and the OnQueryUserInfoComplete() function. When the get periodic leaderboard rankings request completes, it will return a list of leaderboard members' user IDs and their score points. 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 periodic leaderboard rankings to the assigned callback.

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

    LeaderboardInterface->ClearOnLeaderboardReadCompleteDelegate_Handle(OnLeaderboardReadCompleteDelegateHandle);

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

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

    // Query periodic leaderboard members' user information.
    if (UStartupSubsystem* StartupSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<UStartupSubsystem>())
    {
    StartupSubsystem->QueryUserInfo(
    LocalUserNum,
    LeaderboardMembers,
    FOnQueryUsersInfoCompleteDelegate::CreateUObject(this, &ThisClass::OnQueryUserInfoComplete, LocalUserNum, LeaderboardObj, OnComplete));
    }
    else
    {
    UE_LOG_PERIODIC_LEADERBOARD(Warning, TEXT("Startup subsystem is invalid"));
    OnComplete.ExecuteIfBound(false, TArray<ULeaderboardRank*>());
    }
    }
    void UPeriodicBoardSubsystem_Starter::OnQueryUserInfoComplete(
    const FOnlineError& Error,
    const TArray<TSharedPtr<FUserOnlineAccountAccelByte>>& UsersInfo,
    const int32 LocalUserNum,
    const FOnlineLeaderboardReadRef LeaderboardObj,
    const FOnGetLeaderboardRankingComplete OnComplete)
    {
    if (!ensure(UserInterface))
    {
    UE_LOG_PERIODIC_LEADERBOARD(Warning, TEXT("Cannot get periodic leaderboard. User Interface is not valid."));
    return;
    }

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

    UE_LOG_PERIODIC_LEADERBOARD(
    Warning,
    TEXT("Success in getting the periodic leaderboard rankings with code: %s"),
    *LeaderboardObj->LeaderboardName.ToString());

    // Return periodic 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("Cycle_Point")))
    {
    // The stat key is "Cycle_Point" if it was retrieved from FOnlineLeaderboardAccelByte::ReadLeaderboardCycleAroundRank().
    Row.Columns[FName("Cycle_Point")].GetValue(Score);
    }
    else if (Row.Columns.Contains(FName("Point")))
    {
    // The stat key is "Point" if it was retrieved from FOnlineLeaderboardAccelByte::ReadLeaderboardsCycle()
    Row.Columns[FName("Point")].GetValue(Score);
    }

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

    OnComplete.ExecuteIfBound(true, Rankings);
    }
  6. Create a function to get cycle ID by name, as you will need the cycle ID to get the periodic leaderboard rankings. Open the PeriodicBoardSubsystem_Starter class Header file again and declare the following function:

    public:
    // ...
    void GetLeaderboardCycleIdByName(const FString& InCycleName, const EAccelByteCycle& InCycleType, const FOnGetLeaderboardsCycleIdComplete& OnComplete = FOnGetLeaderboardsCycleIdComplete());
  7. Next, define the function above on the PeriodicBoardSubsystem_Starter class CPP file. This function queries the cycle IDs and returns the one that matches the given name.

    void UPeriodicBoardSubsystem_Starter::GetLeaderboardCycleIdByName(const FString& InCycleName, const EAccelByteCycle& InCycleType, const FOnGetLeaderboardsCycleIdComplete& OnComplete)
    {
    AccelByte::FRegistry::Statistic.GetListStatCycleConfigs(
    InCycleType,
    THandler<FAccelByteModelsStatCycleConfigPagingResult>::CreateWeakLambda(this, [InCycleName, OnComplete](const FAccelByteModelsStatCycleConfigPagingResult& Result)
    {
    FString FoundCycleId;
    for (auto& Cycle : Result.Data)
    {
    if (Cycle.Name.Equals(InCycleName))
    {
    FoundCycleId = Cycle.Id;
    break;
    }
    }

    if (!FoundCycleId.IsEmpty())
    {
    UE_LOG_PERIODIC_LEADERBOARD(Log, TEXT("Cycle ID of cycle with name %s is %s."), *InCycleName, *FoundCycleId);
    OnComplete.ExecuteIfBound(true, FoundCycleId);
    }
    else
    {
    UE_LOG_PERIODIC_LEADERBOARD(Warning, TEXT("Cycle ID of cycle with name %s is not found."), *InCycleName);
    OnComplete.ExecuteIfBound(false, FoundCycleId);
    }
    }),
    FErrorHandler::CreateWeakLambda(this, [InCycleName, OnComplete](int32 ErrorCode, const FString& ErrorMessage)
    {
    UE_LOG_PERIODIC_LEADERBOARD(Warning, TEXT("Failed to get Cycle ID of cycle with name %s. Error %d: %s"), *InCycleName, ErrorCode, *ErrorMessage);
    OnComplete.ExecuteIfBound(false, FString());
    })
    );
    }

Implement get player periodic leaderboard ranking

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

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

    public:
    // ...
    void GetPlayerPeriodicRanking(const APlayerController* PC, const FString& LeaderboardCode, const FString& CycleId, const FOnGetLeaderboardRankingComplete& OnComplete = FOnGetLeaderboardRankingComplete());
  2. Open the PeriodicBoardSubsystem_Starter class CPP file and define the function above. This function will send a request to get a certain player's periodic leaderboard rank. The callback will be handled by the OnGetPeriodicRankingsComplete() function, too.

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

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

    const FUniqueNetIdPtr PlayerNetId = GetUniqueNetIdFromPlayerController(PC);
    if (!ensure(PlayerNetId.IsValid()))
    {
    UE_LOG_PERIODIC_LEADERBOARD(Warning, TEXT("Cannot get player periodic 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 periodic leaderboard ranking.
    OnLeaderboardReadCompleteDelegateHandle = LeaderboardInterface->AddOnLeaderboardReadCompleteDelegate_Handle(FOnLeaderboardReadCompleteDelegate::CreateUObject(this, &ThisClass::OnGetPeriodicRankingsComplete, LocalUserNum, LeaderboardObj, OnComplete));
    LeaderboardInterface->ReadLeaderboardsCycle(TPartyMemberArray{ PlayerNetId->AsShared() }, LeaderboardObj, CycleId);
    }

Resources