統計データサブシステムを実装する - 統計データを追跡し表示する - (Unreal Engine モジュール)
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 fromOnlineStatisticInterfaceAccelByte
, 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
Open
AccelByteWars.sln
in Visual Studio. From the Solution Explorer, open theStatsEssentialsSubsystem_Starter
class Header file.Create a new function declaration called
UpdateUsersStats()
with parameters consisting of Local User Index, an array ofFOnlineStatsUserUpdatedStats
, and two callbacks for client and server.public:
bool UpdateUsersStats(
const int32 LocalUserNum,
const TArray<FOnlineStatsUserUpdatedStats>& UpdatedUsersStats,
const FOnlineStatsUpdateStatsComplete& OnCompleteClient = {},
const FOnUpdateMultipleUserStatItemsComplete& OnCompleteServer = {});Open the
StatsEssentialsSubsystem_Starter
class CPP file and create a definition forUpdateUsersStats()
. 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;
}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 namedQueryLocalUserStats()
andQueryUserStats
. 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);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 theStatsUsers
array.QueryLocalUserStats
will callQueryUserStats
, but only with the ID of the local user associated with the givenLocalUserNum
.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;
}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 theUStatsEssentialsSubsystem_Starter::Initialize
method. To theUpdatePlayersStatOnGameEnds
method, you will implement functionality to gather all new values for each player through theirPlayerState
object. Then, you will send the request to update those stats. Navigate toUStatsEssentialsSubsystem_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);
}Add a function to reset a user's stats to 0. Go back to the
StatsEssentialsSubsystem_Starter
Header file and add a function declaration calledResetConnectedUsersStats()
.public:
bool ResetConnectedUsersStats(
const int32 LocalUserNum,
const FOnlineStatsUpdateStatsComplete& OnCompleteClient = {},
const FOnUpdateMultipleUserStatItemsComplete& OnCompleteServer = {});Open the
StatsEssentialsSubsystem_Starter
CPP file and add a definition for the function we just declared. This function will use theUpdateUsersStats()
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, asUpdateUsersStats()
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.
Build the project and make sure there are no compile errors.
Resources
- The files used in this tutorial section are available in the Byte Wars GitHub repository.