Skip to main content

Unreal Engine Module - Track and display a player's high score - Implements stats subsystem

Last updated on January 13, 2024

Unwrap the Subsystem

In this section, you will learn how to implement Stats using AccelByte OSS. This section assumes that you have configured Setup Stats in Admin Portal in Admin Portal.

In Byte Wars, we are using the Game Instance Subsystem named UStatsEssentialsSubsystem to act as the wrapper to handle stats related functionalities using the AccelByte OSS, specifically the FOnlineStatisticAccelByte interface.

What's in the Starter Pack

Just like the UI section of this tutorial, we provide a starter class UStatsEssentialsSubsystem_Starter for you to modify. The files are available in the Resources section.

  • Header file: /Source/AccelByteWars/TutorialModules/Storage/StatisticsEssentials/StatsEssentialsSubsystem_Starter.h
  • CPP file: /Source/AccelByteWars/TutorialModules/Storage/StatisticsEssentials/StatsEssentialsSubsystem_Starter.cpp

The starter class has some functionality already supplied for you:

  • Include AccelByte Online Stats Interface, UE's Online Subsystem Utilities and our custom Game Mode in both the header and CPP file.

    • Header
    #include "OnlineStatisticInterfaceAccelByte.h"
    • CPP
    #include "OnlineSubsystem.h"
    #include "OnlineSubsystemUtils.h"
    #include "Core/AssetManager/TutorialModules/TutorialModuleUtilities/TutorialModuleUtility.h"
    #include "Core/GameModes/AccelByteWarsInGameGameMode.h"
    #include "Core/Player/AccelByteWarsPlayerState.h"
  • Pointer to AccelByte Online Stats Interface and AccelByte Online Identity Interface declared in the header file.

    private:
    IOnlineIdentityPtr IdentityPtr;
    FOnlineStatisticAccelBytePtr ABStatsPtr;
  • Multicast delegate for internal usage. Widget will pass a delegate when calling each function, but those delegate will be bound to these delegate variables due to a limitation on how the interface is programmed. If the delegate is not stored as the class member, it'll be nullptr when called, crashing the game. We're using the existing delegate from OnlineStatisticInterfaceAccelByte, so we don't need to declare any new delegate.

    private:
    FOnlineStatsQueryUsersStatsComplete OnQueryUsersStatsComplete;
    FOnlineStatsUpdateStatsComplete OnUpdateStatsComplete;
    FOnUpdateMultipleUserStatItemsComplete OnServerUpdateStatsComplete;
  • An empty function that is bound to a delegate that will be called on game ends.

    private:
    UFUNCTION()
    void UpdatePlayersStatOnGameEnds();
    // bind delegate if module active
    if (UTutorialModuleUtility::IsTutorialModuleActive(FPrimaryAssetId{ "TutorialModule:STATSESSENTIALS" }, this))
    {
    AAccelByteWarsInGameGameMode::OnGameEndsDelegate.AddUObject(this, &ThisClass::UpdatePlayersStatOnGameEnds);
    }
  • Static strings representing all the available stats code name. You may change the value of this variable to your own stats code if you wish to do so.

    public:
    inline static FString StatsCode_HighestElimination = "unreal-highestscore-elimination";
    inline static FString StatsCode_HighestTeamDeathMatch = "unreal-highestscore-teamdeathmatch";
    inline static FString StatsCode_HighestSinglePlayer = "unreal-highestscore-singleplayer";
    inline static FString StatsCode_KillCount = "unreal-killcount";
  • Lastly, validation of both Stats and Identity in UStatsEssentialsSubsystem_Starter::Initialize.

    const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
    ensure(Subsystem);

    const IOnlineStatsPtr StatsPtr = Subsystem->GetStatsInterface();
    ensure(StatsPtr);

    ABStatsPtr = StaticCastSharedPtr<FOnlineStatisticAccelByte>(StatsPtr);
    ensure(ABStatsPtr);

    IdentityPtr = Subsystem->GetIdentityInterface();
    ensure(IdentityPtr);

Implement Stats using AccelByte OSS

  1. Open AccelByteWars.sln using your preferred IDE, then from Solution Explorer, open StatsEssentialsSubsystem_Starter class header file.

  2. Create a new function declaration called UpdateUsersStats() with parameters consisting of Local User Index, array of FOnlineStatsUserUpdatedStats, and two callbacks for client and server.

    public:
    bool UpdateUsersStats(
    const int32 LocalUserNum,
    const TArray<FOnlineStatsUserUpdatedStats>& UpdatedUsersStats,
    const FOnlineStatsUpdateStatsComplete& OnCompleteClient = {},
    const FOnUpdateMultipleUserStatItemsComplete& OnCompleteServer = {});
  3. Open the StatsEssentialsSubsystem_Starter class CPP file and create a definition for UpdateUsersStats(). This function will update user's stats based on whether the current instance is a dedicated server or not and will use the corresponding callback. The return value of this function is an indication whether the async task was started successfully or not.

    bool UStatsEssentialsSubsystem_Starter::UpdateUsersStats(const int32 LocalUserNum,
    const TArray<FOnlineStatsUserUpdatedStats>& UpdatedUsersStats,
    const FOnlineStatsUpdateStatsComplete& OnCompleteClient,
    const FOnUpdateMultipleUserStatItemsComplete& OnCompleteServer)
    {
    // AB OSS limitation: delegate must be a class member
    if (OnUpdateStatsComplete.IsBound() || OnServerUpdateStatsComplete.IsBound())
    {
    return false;
    }

    if (IsRunningDedicatedServer())
    {
    OnServerUpdateStatsComplete = FOnUpdateMultipleUserStatItemsComplete::CreateWeakLambda(
    this, [OnCompleteServer, this](const FOnlineError& ResultState, const TArray<FAccelByteModelsUpdateUserStatItemsResponse>& Result)
    {
    OnCompleteServer.ExecuteIfBound(ResultState, Result);
    OnServerUpdateStatsComplete.Unbind();
    });

    ABStatsPtr->UpdateStats(LocalUserNum, UpdatedUsersStats, OnServerUpdateStatsComplete);
    }
    else
    {
    OnUpdateStatsComplete = FOnlineStatsUpdateStatsComplete::CreateWeakLambda(
    this, [OnCompleteClient, this](const FOnlineError& ResultState)
    {
    OnCompleteClient.ExecuteIfBound(ResultState);
    OnUpdateStatsComplete.Unbind();
    });

    const FUniqueNetIdRef LocalUserId = IdentityPtr->GetUniquePlayerId(LocalUserNum).ToSharedRef();
    ABStatsPtr->UpdateStats(LocalUserId, UpdatedUsersStats, OnUpdateStatsComplete);
    }

    return true;
    }
  4. Next up, we'll add two functions to perform query of user's stats. Go back to StatsEssentialsSubsystem_Starter class header file and declare two functions named QueryLocalUserStats() and QueryUserStats. The first one will be used to query local user's stats and the second one is to stats for another user.

    public:
    bool QueryLocalUserStats(
    const int32 LocalUserNum,
    const TArray<FString>& StatNames,
    const FOnlineStatsQueryUsersStatsComplete& OnComplete);

    bool QueryUserStats(
    const int32 LocalUserNum,
    const TArray<FUniqueNetIdRef>& StatsUsers,
    const TArray<FString>& StatNames,
    const FOnlineStatsQueryUsersStatsComplete& OnComplete);
  5. Go back to StatsEssentialsSubsystem_Starter class CPP file and add implementations for both functions by adding the code below. QueryUserStats will make a request to retrieve stats data for the user IDs in the StatsUsers array. QueryLocalUserStats will call QueryUserStats, but with only the ID of the local user associated with the given LocalUserNum.

    bool UStatsEssentialsSubsystem_Starter::QueryLocalUserStats(
    const int32 LocalUserNum,
    const TArray<FString>& StatNames,
    const FOnlineStatsQueryUsersStatsComplete& OnComplete)
    {
    const FUniqueNetIdRef LocalUserId = IdentityPtr->GetUniquePlayerId(LocalUserNum).ToSharedRef();
    return QueryUserStats(LocalUserNum, {LocalUserId}, StatNames, OnComplete);
    }

    bool UStatsEssentialsSubsystem_Starter::QueryUserStats(
    const int32 LocalUserNum,
    const TArray<FUniqueNetIdRef>& StatsUsers,
    const TArray<FString>& StatNames,
    const FOnlineStatsQueryUsersStatsComplete& OnComplete)
    {
    // AB OSS limitation: delegate must be a class member
    if (OnQueryUsersStatsComplete.IsBound())
    {
    return false;
    }

    OnQueryUsersStatsComplete = FOnlineStatsQueryUsersStatsComplete::CreateWeakLambda(this, [OnComplete, this](const FOnlineError& ResultState, const TArray<TSharedRef<const FOnlineStatsUserStats>>& UsersStatsResult)
    {
    OnComplete.ExecuteIfBound(ResultState, UsersStatsResult);
    OnQueryUsersStatsComplete.Unbind();
    });

    const FUniqueNetIdRef LocalUserId = IdentityPtr->GetUniquePlayerId(LocalUserNum).ToSharedRef();
    ABStatsPtr->QueryStats(LocalUserId, StatsUsers, StatNames, OnQueryUsersStatsComplete);
    return true;
    }
  6. Now that we have set up functions to push updates for statistics, we will add the functionality to update each user's stats on game end. This will be set up in the UStatsEssentialsSubsystem_Starter::UpdatePlayersStatOnGameEnds method. This method is already bound to the game end delegate in the UStatsEssentialsSubsystem_Starter::Initialize method. To the UpdatePlayersStatOnGameEnds method, we will implement functionality to gather all new values for each player through their PlayerState object. After this, we will send the request to update those stats. Navigate to UStatsEssentialsSubsystem_Starter::UpdatePlayersStatOnGameEnds and add the following implementation.

    void UStatsEssentialsSubsystem_Starter::UpdatePlayersStatOnGameEnds()
    {
    AGameStateBase* GameState = GetWorld()->GetGameState();
    if (!ensure(GameState))
    {
    return;
    }

    AAccelByteWarsGameState* ABGameState = Cast<AAccelByteWarsGameState>(GameState);
    if (!ensure(ABGameState))
    {
    return;
    }

    // Updated stats builder. Update only existing player -> use PlayerArray
    TArray<FOnlineStatsUserUpdatedStats> UpdatedUsersStats;
    for (const TObjectPtr<APlayerState> PlayerState : GameState->PlayerArray)
    {
    AAccelByteWarsPlayerState* ABPlayerState = Cast<AAccelByteWarsPlayerState>(PlayerState);
    if (!ABPlayerState)
    {
    continue;
    }

    const FUniqueNetIdRepl& PlayerUniqueId = PlayerState->GetUniqueId();
    if (!PlayerUniqueId.IsValid())
    {
    continue;
    }

    FOnlineStatsUserUpdatedStats UpdatedUserStats(PlayerUniqueId->AsShared());

    TTuple<FString, FOnlineStatUpdate> StatHighest;
    TTuple<FString, FOnlineStatUpdate> StatKillCount;

    if (ABGameState->GameSetup.NetworkType == EGameModeNetworkType::LOCAL)
    {
    StatHighest.Key = StatsCode_HighestSinglePlayer;
    }
    else
    {
    switch (ABGameState->GameSetup.GameModeType)
    {
    case EGameModeType::FFA:
    StatHighest.Key = StatsCode_HighestElimination;
    break;
    case EGameModeType::TDM:
    StatHighest.Key = StatsCode_HighestTeamDeathMatch;
    break;
    default: ;
    }

    StatKillCount.Key = StatsCode_KillCount;
    StatKillCount.Value = FOnlineStatUpdate{ABPlayerState->KillCount, FOnlineStatUpdate::EOnlineStatModificationType::Sum};
    UpdatedUserStats.Stats.Add(StatKillCount);
    }

    FGameplayTeamData TeamData;
    float TeamScore;
    int32 TeamTotalLives;
    int32 TeamTotalKillCount;
    ABGameState->GetTeamDataByTeamId(ABPlayerState->TeamId, TeamData, TeamScore, TeamTotalLives, TeamTotalKillCount);
    StatHighest.Value = FOnlineStatUpdate{TeamScore, FOnlineStatUpdate::EOnlineStatModificationType::Largest};
    UpdatedUserStats.Stats.Add(StatHighest);

    UpdatedUsersStats.Add(UpdatedUserStats);
    }

    // Update stats
    UpdateUsersStats(0, UpdatedUsersStats);
    }
  7. Lastly, we're going to add a function to reset user's stats to 0. Go back to the StatsEssentialsSubsystem_Starter header file and add a function declaration called ResetConnectedUsersStats().

    public:
    bool ResetConnectedUsersStats(
    const int32 LocalUserNum,
    const FOnlineStatsUpdateStatsComplete& OnCompleteClient = {},
    const FOnUpdateMultipleUserStatItemsComplete& OnCompleteServer = {});
  8. Open the StatsEssentialsSubsystem_Starter CPP file and add a definition for the function we just declared. This function will use the UpdateUsersStats() method to set every connected user's stats back to 0. We do not need to worry about calling a different function to update stats for client and server, as UpdateUsersStats() already does so for us.

    bool UStatsEssentialsSubsystem_Starter::ResetConnectedUsersStats(
    const int32 LocalUserNum,
    const FOnlineStatsUpdateStatsComplete& OnCompleteClient,
    const FOnUpdateMultipleUserStatItemsComplete& OnCompleteServer)
    {
    AGameStateBase* GameState = GetWorld()->GetGameState();
    if (!ensure(GameState))
    {
    return false;
    }

    AAccelByteWarsGameState* ABGameState = Cast<AAccelByteWarsGameState>(GameState);
    if (!ensure(ABGameState))
    {
    return false;
    }

    // Updated stats builder. Update only existing player -> use PlayerArray
    TArray<FOnlineStatsUserUpdatedStats> UpdatedUsersStats;
    for (const TObjectPtr<APlayerState> PlayerState : GameState->PlayerArray)
    {
    AAccelByteWarsPlayerState* ABPlayerState = Cast<AAccelByteWarsPlayerState>(PlayerState);
    if (!ABPlayerState)
    {
    continue;
    }

    const FUniqueNetIdRepl& PlayerUniqueId = PlayerState->GetUniqueId();
    if (!PlayerUniqueId.IsValid())
    {
    continue;
    }

    FOnlineStatsUserUpdatedStats UpdatedUserStats(PlayerUniqueId->AsShared());
    if (IsRunningDedicatedServer())
    {
    // server side stats, need to be set by the server
    UpdatedUserStats.Stats.Add(StatsCode_HighestElimination, FOnlineStatUpdate{0.0f, FOnlineStatUpdate::EOnlineStatModificationType::Set});
    UpdatedUserStats.Stats.Add(StatsCode_HighestTeamDeathMatch, FOnlineStatUpdate{0.0f, FOnlineStatUpdate::EOnlineStatModificationType::Set});
    UpdatedUserStats.Stats.Add(StatsCode_KillCount, FOnlineStatUpdate{0.0f, FOnlineStatUpdate::EOnlineStatModificationType::Set});
    }
    else
    {
    // client side stats, need to be set by the client
    UpdatedUserStats.Stats.Add(StatsCode_HighestSinglePlayer, FOnlineStatUpdate{0.0f, FOnlineStatUpdate::EOnlineStatModificationType::Set});
    }
    }

    // Update stats
    return UpdateUsersStats(LocalUserNum, UpdatedUsersStats, OnCompleteClient, OnCompleteServer);
    }
    Note

    You can also use FOnlineStatisticAccelByte::ResetStats() to reset user's stats. But this function is meant to be used for debugging only. This function will not be compiled for Shipping build.

    Resetting user's stats can also be done through Admin Portal in Game Management > Statistics > Statistics Value.

  9. Build the AccelByteWars project and make sure there is no compile error.

  10. There you have it! We have finished implementing AGS Statistics Subsystem.

Resources