Use the Online Subsystem to get recent players - Recent players - (Unreal Engine module)
Unwrap the Subsystem
This tutorial shows how to implement the functions to get the recent player list and the game session player list using the AccelByte Gaming Services (AGS) Online Subsystem (OSS). In the Byte Wars project, there is already a game instance subsystem created named RecentPlayersSubsystem
, which has the completed implementation. However, for this tutorial, you will use a starter version of that subsystem so you can implement the functions from scratch.
What's in the Starter Pack
To follow this tutorial, a starter subsystem class named RecentPlayersSubsystem_Starter
has been prepared for you. This class is available in the Resources section and consists of the following files:
- Header file:
/Source/AccelByteWars/TutorialModules/Social/RecentPlayers/RecentPlayersSubsystem_Starter.h
- CPP file:
/Source/AccelByteWars/TutorialModules/Social/RecentPlayers/RecentPlayersSubsystem_Starter.cpp
The RecentPlayersSubsystem_Starter
class contains several helpers:
AGS OSS interfaces declarations:
FriendsInterface
andSessionInterface
. You will use these interfaces to implement the recent players feature.protected:
FOnlineFriendsAccelBytePtr FriendsInterface;
IOnlineSessionPtr SessionInterface;A helper function to get
UniqueNetId
from aPlayerController
. You will need this function for the AGS OSS interfaces mentioned above.FUniqueNetIdPtr URecentPlayersSubsystem_Starter::GetUniqueNetIdFromPlayerController(const APlayerController* PlayerController)
{
if (!PlayerController)
{
return nullptr;
}
const ULocalPlayer* LocalPlayer = PlayerController->GetLocalPlayer();
if (!LocalPlayer)
{
return nullptr;
}
return LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId();
}
There is also a model file containing helper delegates. The file is located in /Source/AccelByteWars/TutorialModules/Social/RecentPlayers/RecentPlayersModels.h
. You will use these delegates to handle callbacks.
DECLARE_DELEGATE_TwoParams(FOnGetRecentPlayersComplete, bool /*bWasSuccessful*/, TArray<UFriendData*> /*RecentPlayersData*/)
DECLARE_DELEGATE_TwoParams(FOnGetGameSessionPlayerListComplete, bool /*bWasSuccessful*/, TArray<UFriendData*> /*GameSessionPlayersData*/)
Prepare the helpers
In this section, you will prepare the functions to be used as helpers for the RecentPlayersSubsystem_Starter
implementation.
Open the
RecentPlayersSubsystem_Starter
class Header file and declare the function below. This function updates the player friend invitation status.private:
void UpdatePlayersInviteStatus(const APlayerController* PlayerController, const FOnGetGameSessionPlayerListComplete& OnComplete, TArray<UFriendData*>& PlayersData);Open the
RecentPlayersSubsystem_Starter
class CPP file to define theUpdatePlayersInviteStatus()
function by adding the code below. This function updates the player's friend invitation status by comparing the player's friend list with the blocked players list. The reason you need this function is that the blocked players list is handled by a different interface of the AGS OSS. Hence, you need to sync the status to make sure the player is only able to send friend requests to players who aren't blocked.void URecentPlayersSubsystem_Starter::UpdatePlayersInviteStatus(const APlayerController* PlayerController, const FOnGetGameSessionPlayerListComplete& OnComplete, TArray<UFriendData*>& PlayersData)
{
UAccelByteWarsGameInstance* GameInstance = Cast<UAccelByteWarsGameInstance>(GetGameInstance());
ensure(GameInstance);
UFriendsSubsystem* FriendsSubsystem = GameInstance->GetSubsystem<UFriendsSubsystem>();
ensure(FriendsSubsystem);
FriendsSubsystem->GetFriendsInviteStatus(PlayerController, PlayersData, FOnGetPlayersInviteStatusComplete::CreateWeakLambda(this, [this, PlayerController, OnComplete, &PlayersData, GameInstance](bool bWasSuccessful, const FString& ErrorMessage)
{
if(bWasSuccessful)
{
//check blocked player list, as blocked player returned from Friends subsystem has friend status as EFriendStatus::Unknown
UManagingFriendsSubsystem* ManagingFriendsSubsystem = GameInstance->GetSubsystem<UManagingFriendsSubsystem>();
ensure(ManagingFriendsSubsystem);
ManagingFriendsSubsystem->GetBlockedPlayerList(
PlayerController,
false,
FOnGetBlockedPlayerListComplete::CreateWeakLambda(this, [this, OnComplete, &PlayersData](bool bWasSuccessful, TArray<UFriendData*> BlockedPlayers, const FString& ErrorMessage)
{
if(bWasSuccessful)
{
for(UFriendData* PlayerData: PlayersData)
{
if(BlockedPlayers.ContainsByPredicate([PlayerData](const UFriendData* Data)
{
return Data->UserId == PlayerData->UserId;
}))
{
PlayerData->Status = EFriendStatus::Blocked;
PlayerData->bCannotBeInvited = true;
PlayerData->ReasonCannotBeInvited = NSLOCTEXT("AccelByteWars", "Blocked", "Blocked").ToString();
}
}
}
OnComplete.ExecuteIfBound(bWasSuccessful, PlayersData);
}));
}
else
{
OnComplete.ExecuteIfBound(false, GameSessionPlayersData);
UE_LOG_RECENTPLAYERS(Warning, TEXT("Failed get invite status. Error message: %s"), *ErrorMessage);
}
}));
}
Implement getting recent players
In this section, you will implement functions to get the recent players list.
Open the
RecentPlayersSubsystem_Starter
class Header file and declare the helper variable below. This variable will be used to cache the recent players list.private:
// ...
UPROPERTY()
TArray<UFriendData*> RecentPlayersData{};Still in the Header file, declare the functions below. These functions will be used to query and get the recent players list.
public:
// ...
void GetRecentPlayers(const APlayerController* PlayerController, const FOnGetRecentPlayersComplete& OnComplete = FOnGetRecentPlayersComplete());
void QueryRecentPlayers(const APlayerController* PlayerController);
FDelegateHandle BindRecentPlayerDelegate(FOnQueryRecentPlayersCompleteDelegate& Delegate);
void UnBindRecentPlayerDelegate(FDelegateHandle& Delegate);Still in the Header file, declare the functions below. These functions will be used to query and get the recent players list.
private:
// ...
void OnSessionDestroyed(FName SessionName, bool bWasSuccessful);Open the
RecentPlayersSubsystem_Starter
class CPP file to define the functions above. Define theGetRecentPlayers()
function by adding the code below. This function collects the recent players list from the AGS OSS cache and then updates the friend invitation status.void URecentPlayersSubsystem_Starter::GetRecentPlayers(const APlayerController* PlayerController,
const FOnGetRecentPlayersComplete& OnComplete)
{
const FUniqueNetIdPtr LocalPlayerId = GetUniqueNetIdFromPlayerController(PlayerController);
if (!ensure(LocalPlayerId.IsValid()))
{
UE_LOG_RECENTPLAYERS(Warning, TEXT("Cannot get recent player. LocalPlayer NetId is not valid."));
return;
}
TArray<TSharedRef<FOnlineRecentPlayer>> RecentPlayers;
bool bSuccess = FriendsInterface->GetRecentPlayers(LocalPlayerId.ToSharedRef().Get(), TEXT(""), RecentPlayers);
if(bSuccess)
{
UE_LOG_RECENTPLAYERS(Log, TEXT("Success to get recent player list."));
RecentPlayersData.Empty();
for(const TSharedRef<FOnlineRecentPlayer>& Player: RecentPlayers)
{
RecentPlayersData.Add(UFriendData::ConvertToFriendData(Player));
}
UpdatePlayersInviteStatus(PlayerController, OnComplete, RecentPlayersData);
}
}Next, define the
QueryRecentPlayers()
function by adding the code below. This function sends a request to the backend to query the recent players list.void URecentPlayersSubsystem_Starter::QueryRecentPlayers(const APlayerController* PlayerController)
{
const FUniqueNetIdPtr LocalPlayerId = GetUniqueNetIdFromPlayerController(PlayerController);
if (!ensure(LocalPlayerId.IsValid()))
{
UE_LOG_RECENTPLAYERS(Warning, TEXT("Cannot query recent player. LocalPlayer NetId is not valid."));
return;
}
FriendsInterface->QueryRecentPlayers(LocalPlayerId.ToSharedRef().Get(), TEXT(""));
}Define the
BindRecentPlayerDelegate()
function by adding the code below. This function adds the passed delegate to be triggered upon completion of querying the recent players list. You will use this function with the UI to refresh the displayed recent players list.FDelegateHandle URecentPlayersSubsystem_Starter::BindRecentPlayerDelegate(FOnQueryRecentPlayersCompleteDelegate& Delegate)
{
return FriendsInterface->AddOnQueryRecentPlayersCompleteDelegate_Handle(Delegate);
}Then, define the
UnBindRecentPlayerDelegate()
function by adding the code below. This function removes the passed delegate from the query recent players list delegate. You will use this function with the UI to stop refreshing the displayed recent players list.void URecentPlayersSubsystem_Starter::UnBindRecentPlayerDelegate(FDelegateHandle& Delegate)
{
FriendsInterface->ClearOnQueryRecentPlayersCompleteDelegate_Handle(Delegate);
}Next, define the
OnSessionDestroyed()
function by adding the code below. This function will query the recent players list every time a game session ends.void URecentPlayersSubsystem_Starter::OnSessionDestroyed(FName SessionName, bool bWasSuccessful)
{
// refresh recent player if a game session is destroyed
if(bWasSuccessful && SessionName.IsEqual(NAME_GameSession))
{
const UAccelByteWarsGameInstance* GameInstance = Cast<UAccelByteWarsGameInstance>(GetGameInstance());
ensure(GameInstance);
QueryRecentPlayers(GameInstance->GetFirstLocalPlayerController());
}
}Locate the predefined
Initialize()
function, which is the function to be called when the subsystem is initialized. Then, add the code below to bind theOnSessionDestroyed()
function to be called every time a game session ends.void URecentPlayersSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
SessionInterface->AddOnDestroySessionCompleteDelegate_Handle(FOnDestroySessionCompleteDelegate::CreateUObject(this, &ThisClass::OnSessionDestroyed));
}Finally, locate the predefined
Deinitialize()
function, which is the function to be called when the subsystem is deinitialized. Then, add the code below to clear the on game session ended delegate.void URecentPlayersSubsystem_Starter::Deinitialize()
{
// ...
SessionInterface->ClearOnDestroySessionCompleteDelegates(this);
}
Implement getting game session players list
In this section, you will implement functionality to get the game session player list.
Open the
RecentPlayersSubsystem_Starter
class Header file and declare the helper variable below. This variable will be used to cache the game session players list.private:
// ...
UPROPERTY()
TArray<UFriendData*> GameSessionPlayersData{};Still in the Header file, declare the functions below. These functions will be used get the game session players list.
public:
// ...
void GetGameSessionPlayerList(const APlayerController* PlayerController, const FOnGetGameSessionPlayerListComplete& OnComplete = FOnGetGameSessionPlayerListComplete());
FString GetGameSessionPlayerStatus(UFriendData* Player);Open the
RecentPlayersSubsystem_Starter
class CPP file to define the functions above. Define theGetGameSessionPlayerList()
function by adding the code below. This function collects and returns the game session player list via the passed delegate parameter.void URecentPlayersSubsystem_Starter::GetGameSessionPlayerList(const APlayerController* PlayerController, const FOnGetGameSessionPlayerListComplete& OnComplete)
{
const FNamedOnlineSession* GameSession = SessionInterface->GetNamedSession(NAME_GameSession);
if(!GameSession)
{
return;
}
const TSharedPtr<FOnlineSessionInfo> SessionInfo = GameSession->SessionInfo;
if (!SessionInfo.IsValid())
{
return;
}
const TSharedPtr<FOnlineSessionInfoAccelByteV2> AbSessionInfo = StaticCastSharedPtr<FOnlineSessionInfoAccelByteV2>(SessionInfo);
if (!AbSessionInfo.IsValid())
{
return;
}
const TSharedPtr<FAccelByteModelsV2BaseSession> AbBaseSessionInfo = AbSessionInfo->GetBackendSessionData();
if (!AbBaseSessionInfo.IsValid())
{
return;
}
const FUniqueNetIdAccelByteUserPtr TargetUserABId = StaticCastSharedPtr<const FUniqueNetIdAccelByteUser>(GetUniqueNetIdFromPlayerController(PlayerController));
if(!TargetUserABId.IsValid())
{
UE_LOG_RECENTPLAYERS(Warning, TEXT("Local userid invalid"));
return;
}
GameSessionPlayersData.Empty();
TArray<FAccelByteModelsV2SessionUser> AbMembers = AbBaseSessionInfo->Members;
TArray<FUniqueNetIdRef> UniqueNetIds;
for (const FAccelByteModelsV2SessionUser& AbMember : AbMembers)
{
// skip local player
if(TargetUserABId->GetAccelByteId().Equals(AbMember.ID))
{
continue;
}
FAccelByteUniqueIdComposite CompositeId;
CompositeId.Id = AbMember.ID;
FUniqueNetIdAccelByteUserRef AccelByteUser = FUniqueNetIdAccelByteUser::Create(CompositeId);
UniqueNetIds.Add(AccelByteUser);
const FOnlineSubsystemAccelByte* Subsystem = static_cast<FOnlineSubsystemAccelByte*>(Online::GetSubsystem(GetWorld()));
if (!ensure(Subsystem))
{
UE_LOG_RECENTPLAYERS(Warning, TEXT("The online subsystem is invalid."));
return;
}
const TSharedPtr<const FAccelByteUserInfo> User = Subsystem->GetUserCache()->GetUser(AccelByteUser.Get());
if(User.IsValid())
{
UFriendData* PlayerData = UFriendData::ConvertToFriendData(User.ToSharedRef());
GameSessionPlayersData.Add(PlayerData);
}
else
{
UE_LOG_RECENTPLAYERS(Warning, TEXT("User is invalid"));
}
}
UE_LOG_RECENTPLAYERS(Log, TEXT("Success to get game session player list."));
UpdatePlayersInviteStatus(PlayerController, OnComplete, GameSessionPlayersData);
}Next, define the
GetGameSessionPlayerStatus()
function by adding the code below. This function returns the player status in the game session (e.g., joined or left game session).FString URecentPlayersSubsystem_Starter::GetGameSessionPlayerStatus(UFriendData* Player)
{
FString StatusAsString = TEXT("");
const FNamedOnlineSession* GameSession = SessionInterface->GetNamedSession(NAME_GameSession);
const TSharedPtr<FOnlineSessionInfo> SessionInfo = GameSession->SessionInfo;
if (!SessionInfo.IsValid())
{
return StatusAsString;
}
const TSharedPtr<FOnlineSessionInfoAccelByteV2> AbSessionInfo = StaticCastSharedPtr<FOnlineSessionInfoAccelByteV2>(SessionInfo);
if (!AbSessionInfo.IsValid())
{
return StatusAsString;
}
const TSharedPtr<FAccelByteModelsV2BaseSession> AbBaseSessionInfo = AbSessionInfo->GetBackendSessionData();
if (!AbBaseSessionInfo.IsValid())
{
return StatusAsString;
}
TArray<FAccelByteModelsV2SessionUser> AbMembers = AbBaseSessionInfo->Members;
const FUniqueNetIdAccelByteUserPtr TargetUserABId = StaticCastSharedPtr<const FUniqueNetIdAccelByteUser>(Player->UserId);
FAccelByteModelsV2SessionUser* OnlineUser = AbMembers.FindByPredicate([TargetUserABId](FAccelByteModelsV2SessionUser User)
{
return User.ID.Equals(TargetUserABId->GetAccelByteId());
});
if(OnlineUser != nullptr)
{
StatusAsString = UEnum::GetValueAsString<EAccelByteV2SessionMemberStatus>(OnlineUser->StatusV2);
StatusAsString = StatusAsString.RightChop(StatusAsString.Find(TEXT("::")) + 2);
}
return StatusAsString;
}infoYou can refer to the
EAccelByteV2SessionMemberStatus
enumeration for a detailed list of all player game session status types. This enum outlines the various statuses a player may have during a game session.
Resources
- The files used in this tutorial section are available in the Unreal Byte Wars GitHub repository.
- AccelByteWars/Source/AccelByteWars/TutorialModules/Social/RecentPlayers/RecentPlayersModels.h
- AccelByteWars/Source/AccelByteWars/TutorialModules/Social/RecentPlayers/RecentPlayersSubsystem_Starter.h
- AccelByteWars/Source/AccelByteWars/TutorialModules/Social/RecentPlayers/RecentPlayersSubsystem_Starter.cpp