Skip to main content

Implement stats subsystem - Stat tracking and display - (Unreal Engine module)

Last updated on October 24, 2024

Unwrap the subsystem

In this tutorial, you will learn how to implement AccelByte Gaming Services (AGS) Statistics using AGS Online Subsystem (OSS). In Byte Wars, there is a game instance subsystem defined in the StatsEssentialsSubsystem class. This subsystem acts as a wrapper to manipulate the AGS statistics values.

In this tutorial, you will manipulate the statistics values using the StatsEssentialsSubsystem_Starter subsystem, which is the starter version of the StatsEssentialsSubsystem class.

What's in the starter pack

To follow this tutorial, a starter subsystem class has been prepared named StatsEssentialsSubsystem_Starter. This class is available in the Resources section and consists of the following files:

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

The StatsEssentialsSubsystem_Starter class has several functions provided:

  • AGS OSS interfaces declarations named IdentityPtr and ABStatsPtr. You will use these interfaces to implement statistics-related functions later.

    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 you don't need to declare any new delegate.

    private:
    // ...
    FOnlineStatsQueryUsersStatsComplete OnQueryUsersStatsComplete;
    FOnlineStatsUpdateStatsComplete OnUpdateStatsComplete;
    FOnUpdateMultipleUserStatItemsComplete OnServerUpdateStatsComplete;
  • An empty function that will be called when the game ends. You will use this function to update the player statistics later.

    private:
    UFUNCTION()
    void UpdateConnectedPlayersStatsOnGameEnds();

Get statistics

In this section, you will implement functionality to query and get a player's statistics.

  1. Open the StatsEssentialsSubsystem_Starter class Header file and declare the following functions:

    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);
  2. Next, open the StatsEssentialsSubsystem_Starter class CPP file and define QueryUserStats(). This function queries the statistics based on given player IDs. Once the query is complete, the given callback delegate will be executed.

    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;
    }
  3. Then, define the QueryLocalUserStats() function. This function queries the local player by passing the local player ID to the QueryUserStats() function.

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

Congratulations! You've implemented getting a player's statistics.

Update statistics

In this section, you will implement functions to update player's statistics value.

  1. Open the StatsEssentialsSubsystem_Starter class Header file and declare the functions below:

    public:
    // ...
    bool UpdateUsersStats(
    const int32 LocalUserNum,
    const TArray<FOnlineStatsUserUpdatedStats>& UpdatedUsersStats,
    const FOnlineStatsUpdateStatsComplete& OnCompleteClient = {},
    const FOnUpdateMultipleUserStatItemsComplete& OnCompleteServer = {});
    // ...
    bool UpdateConnectedPlayersStats(
    const int32 LocalUserNum,
    const bool bToReset = false,
    const FOnlineStatsUpdateStatsComplete& OnCompleteClient = {},
    const FOnUpdateMultipleUserStatItemsComplete& OnCompleteServer = {});
  2. Next, open the StatsEssentialsSubsystem_Starter class CPP file and define the UpdateUsersStats() function. This function will update a player's statistics 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 of 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;
    }
  3. Then, define the UpdateConnectedPlayersStats() function. This function is a wrapper function to update statistics of all connected players in the game. First, it determines which game mode is currently being played. If the bToReset is true, the function will reset all statistics values to 0. Otherwise, it loops through the connected players to store their data into a statistics object. Finally, the function collects all the statistics objects and passes them to UpdateUsersStats() to update the connected players' statistics.

    info

    The function below shows how to modify all statistics available in Byte Wars. However, in this module, you will only configure statistics for the player's highest score. Other statistics values won't be updated.

    bool UStatsEssentialsSubsystem_Starter::UpdateConnectedPlayersStats(
    const int32 LocalUserNum,
    const bool bToReset,
    const FOnlineStatsUpdateStatsComplete& OnCompleteClient,
    const FOnUpdateMultipleUserStatItemsComplete& OnCompleteServer)
    {
    UE_LOG_STATSESSENTIALS(Log, TEXT("Updating connected player stats to %s"), bToReset ? TEXT("reset") : TEXT("new value"));

    UAccelByteWarsGameInstance* GameInstance = Cast<UAccelByteWarsGameInstance>(GetGameInstance());
    if (!GameInstance)
    {
    UE_LOG_STATSESSENTIALS(Warning, TEXT("Failed to update players' statistics. Game instance is invalid."));
    return false;
    }

    AAccelByteWarsGameState* GameState = Cast<AAccelByteWarsGameState>(GetWorld()->GetGameState());
    if (!GameState)
    {
    UE_LOG_STATSESSENTIALS(Warning, TEXT("Failed to update players' statistics. Game state is invalid."));
    return false;
    }

    // Get valid game stats data.
    const bool bIsLocalGame = GameState->GameSetup.NetworkType == EGameModeNetworkType::LOCAL;
    bool bIsGameStatsDataValid = false;
    FGameStatsData GameStatsData{};
    if (bIsLocalGame)
    {
    bIsGameStatsDataValid = GameInstance->GetGameStatsDataById(GAMESTATS_GameModeSinglePlayer, GameStatsData);
    }
    else if (GameState->GameSetup.GameModeType == EGameModeType::FFA)
    {
    bIsGameStatsDataValid = GameInstance->GetGameStatsDataById(GAMESTATS_GameModeElimination, GameStatsData);
    }
    else if (GameState->GameSetup.GameModeType == EGameModeType::TDM)
    {
    bIsGameStatsDataValid = GameInstance->GetGameStatsDataById(GAMESTATS_GameModeTeamDeathmatch, GameStatsData);
    }

    if (!bIsGameStatsDataValid)
    {
    UE_LOG_STATSESSENTIALS(Warning, TEXT("Failed to update players' statistics. No statistics data to update."));
    return false;
    }

    // Update players' stats.
    TArray<FOnlineStatsUserUpdatedStats> UpdatedUsersStats;
    const int32 WinnerTeamId = GameState->GetWinnerTeamId();
    for (const TObjectPtr<APlayerState> PlayerState : GameState->PlayerArray)
    {
    AAccelByteWarsPlayerState* ABPlayerState = Cast<AAccelByteWarsPlayerState>(PlayerState);
    if (!ABPlayerState)
    {
    UE_LOG_STATSESSENTIALS(Warning, TEXT("Failed to update player's statistics. Player state is invalid."));
    continue;
    }

    const FUniqueNetIdRepl& PlayerUniqueId = PlayerState->GetUniqueId();
    if (!PlayerUniqueId.IsValid())
    {
    UE_LOG_STATSESSENTIALS(Warning, TEXT("Failed to update player's statistics. User ID is invalid."));
    continue;
    }

    FGameplayTeamData TeamData{};
    float TeamScore = 0;
    int32 TeamTotalLives = 0, TeamTotalKillCount = 0, TeamTotalDeaths = 0;

    /* Local gameplay only has one valid account, which is the player who logged in to the game.
    * Thus, set the stats based on the highest team data.*/
    if (bIsLocalGame)
    {
    GameState->GetHighestTeamData(TeamScore, TeamTotalLives, TeamTotalKillCount, TeamTotalDeaths);
    }
    // Each connected player account in online gameplay is valid, so set the stats based on their respective teams.
    else
    {
    GameState->GetTeamDataByTeamId(ABPlayerState->TeamId, TeamData, TeamScore, TeamTotalLives, TeamTotalKillCount, TeamTotalDeaths);
    }

    /* If the gameplay is local, set the winner status based on if the game ends in a draw or not.
    * If the gameplay is online, set the winner status based on whether the team ID matches with the winning team ID.*/
    const bool bIsWinner = bIsLocalGame ? WinnerTeamId != INDEX_NONE : TeamData.TeamId == WinnerTeamId;

    FOnlineStatsUserUpdatedStats UpdatedUserStats(PlayerUniqueId->AsShared());
    // Reset statistics values to zero.
    if (bToReset)
    {
    for (const FString& Code : GameStatsData.GetStatsCodes())
    {
    UpdatedUserStats.Stats.Add(TTuple<FString, FOnlineStatUpdate>
    {
    Code,
    FOnlineStatUpdate { 0, FOnlineStatUpdate::EOnlineStatModificationType::Set }
    });
    }
    }
    // Update statistics values.
    else
    {
    UpdatedUserStats.Stats.Add(TTuple<FString, FOnlineStatUpdate>
    {
    GameStatsData.HighestScoreStats.CodeName,
    FOnlineStatUpdate
    {
    TeamScore,
    FOnlineStatUpdate::EOnlineStatModificationType::Largest
    }
    });
    UpdatedUserStats.Stats.Add(TTuple<FString, FOnlineStatUpdate>
    {
    GameStatsData.TotalScoreStats.CodeName,
    FOnlineStatUpdate
    {
    TeamScore,
    FOnlineStatUpdate::EOnlineStatModificationType::Sum
    }
    });
    UpdatedUserStats.Stats.Add(TTuple<FString, FOnlineStatUpdate>
    {
    GameStatsData.MatchesPlayedStats.CodeName,
    FOnlineStatUpdate
    {
    1,
    FOnlineStatUpdate::EOnlineStatModificationType::Sum
    }
    });
    UpdatedUserStats.Stats.Add(TTuple<FString, FOnlineStatUpdate>
    {
    GameStatsData.MatchesWonStats.CodeName,
    FOnlineStatUpdate
    {
    bIsWinner ? 1 : 0,
    FOnlineStatUpdate::EOnlineStatModificationType::Sum
    }
    });
    UpdatedUserStats.Stats.Add(TTuple<FString, FOnlineStatUpdate>
    {
    GameStatsData.KillCountStats.CodeName,
    FOnlineStatUpdate
    {
    TeamTotalKillCount,
    FOnlineStatUpdate::EOnlineStatModificationType::Sum
    }
    });
    UpdatedUserStats.Stats.Add(TTuple<FString, FOnlineStatUpdate>
    {
    GameStatsData.DeathStats.CodeName,
    FOnlineStatUpdate
    {
    TeamTotalDeaths,
    FOnlineStatUpdate::EOnlineStatModificationType::Sum
    }
    });
    }

    UpdatedUsersStats.Add(UpdatedUserStats);
    }

    // Update stats
    return UpdateUsersStats(LocalUserNum, UpdatedUsersStats, OnCompleteClient, OnCompleteServer);
    }
  4. Finally, call the UpdateConnectedPlayersStats() by replacing the predefined UpdateConnectedPlayersStatsOnGameEnds() function with the code below to update the connected players statistics when the game ends.

    void UStatsEssentialsSubsystem_Starter::UpdateConnectedPlayersStatsOnGameEnds()
    {
    const bool bStarted = UpdateConnectedPlayersStats(0, false);
    if (bStarted)
    {
    UE_LOG_STATSESSENTIALS(Log, TEXT("Update connected player statistics on game ends is started"));
    }
    else
    {
    UE_LOG_STATSESSENTIALS(Warning, TEXT("Update connected player statistics on game ends is failed"));
    }
    }

Congratulations! You have implemented updating player statistics.

Resources