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

統計データサブシステムを実装する - 統計データを追跡し表示する - (Unreal Engine モジュール)

Last updated on May 30, 2024

Unwrap the subsystem

In this tutorial, you will learn how to implement stats using AccelByte Gaming Services (AGS) Online Subsystem (OSS).

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

What's in the starter pack

The provided starter class UStatsEssentialsSubsystem_Starter for you to modify is available in the Resources section and consists of:

  • 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:

  • Include AGS Online Stats Interface, Unreal Engine's OSS 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 AGS Online Stats Interface and AGS Online Identity Interface declared in the Header file.

    private:
    IOnlineIdentityPtr IdentityPtr;
    FOnlineStatisticAccelBytePtr ABStatsPtr;
  • Multicast delegate for internal usage. The Widget will pass a delegate when calling each function, but this 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. This uses 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 end.

    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 stat code names. You may change the value of this variable to your own stat codes.

    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";
  • 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 AGS OSS

  1. Open AccelByteWars.sln in Visual Studio. From the Solution Explorer, open the StatsEssentialsSubsystem_Starter class Header file.

  2. Create a new function declaration called UpdateUsersStats() with parameters consisting of Local User Index, an 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 a 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. Add two functions to perform a query of a user's stats. Go back to the StatsEssentialsSubsystem_Starter class Header file and declare two functions named QueryLocalUserStats() and QueryUserStats. The first one will be used to query a local user's stats and the second one is for stats of 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 the 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 only with 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. 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, you will implement functionality to gather all new values for each player through their PlayerState object. Then, you 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. Add a function to reset a 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. You don't need to worry about calling a different function to update stats for client and server, as UpdateUsersStats() already does that.

    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);
    }
    注記

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

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

  9. Build the project and make sure there are no compile errors.

Resources