Implement stats subsystem - Stat tracking and display - (Unreal Engine module)
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
andABStatsPtr
. 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 fromOnlineStatisticInterfaceAccelByte
, 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.
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);Next, open the
StatsEssentialsSubsystem_Starter
class CPP file and defineQueryUserStats()
. 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;
}Then, define the
QueryLocalUserStats()
function. This function queries the local player by passing the local player ID to theQueryUserStats()
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.
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 = {});Next, open the
StatsEssentialsSubsystem_Starter
class CPP file and define theUpdateUsersStats()
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;
}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 thebToReset
istrue
, 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 toUpdateUsersStats()
to update the connected players' statistics.infoThe 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);
}Finally, call the
UpdateConnectedPlayersStats()
by replacing the predefinedUpdateConnectedPlayersStatsOnGameEnds()
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
- The files used in this tutorial section are available in the Byte Wars GitHub repository.