Use the OSS to play with party - Play with party - (Unreal Engine module)
Unwrap the subsystem
In this section, you will learn how to implement playing with a party using AccelByte Gaming Services (AGS) Online Subsystem (OSS). In the Byte Wars project, there is already a game instance subsystem class created named PlayWithPartySubsystem
. This subsystem has the complete party playing implementations. In this tutorial, you will use the starter version of that subsystem so you can implement playing with a party from scratch.
Prepare your project
To follow this tutorial, you first need to enable the starter mode. To do this, build your project and open it in the Unreal Engine editor. In the editor, go to /Content/TutorialModules/PlayingWithParty
. There, you will find a data asset called DA_PlayingWithParty
. Open it and enable Is Starter Mode Active
. Then, save the data asset again.
Understand what's in the starter pack
Now, back to your project source code. There is a prepared starter subsystem class named PlayWithPartySubsystem_Starter
. This class is available in the Resources section and consists of the following files:
- Header file:
/Source/AccelByteWars/TutorialModules/PlayingWithParty/PlayWithPartySubsystem_Starter.h
- CPP file:
/Source/AccelByteWars/TutorialModules/PlayingWithParty/PlayWithPartySubsystem_Starter.cpp
The PlayWithPartySubsystem_Starter
class has several features provided:
AGS OSS interfaces getter functions named
GetSessionInterface()
andGetIdentityInterface()
. You will use these interfaces to implement playing with the party.FOnlineSessionV2AccelBytePtr UPlayWithPartySubsystem_Starter::GetSessionInterface() const
{
const UWorld* World = GetWorld();
if (!ensure(World))
{
return nullptr;
}
return StaticCastSharedPtr<FOnlineSessionV2AccelByte>(Online::GetSessionInterface(World));
}FOnlineIdentityAccelBytePtr UPlayWithPartySubsystem_Starter::GetIdentityInterface() const
{
const UWorld* World = GetWorld();
if (!ensure(World))
{
return nullptr;
}
return StaticCastSharedPtr<FOnlineIdentityAccelByte>(Online::GetIdentityInterface(World));
}You will need a reference to the current active online session to bring your party into. For this, a provided getter function has been provided named
GetOnlineSession()
. For more information regarding online sessions and party sessions, refer back to Introduction to session and Introduction to party.UAccelByteWarsOnlineSessionBase* UPlayWithPartySubsystem_Starter::GetOnlineSession() const
{
if (!GetGameInstance())
{
return nullptr;
}
return Cast<UAccelByteWarsOnlineSessionBase>(GetGameInstance()->GetOnlineSession());
}Below is a helper function to show prompts and notifications. You will need this to show messages to the player regarding events related to playing with a party, e.g., a message prompt to show that the party leader has started the party matchmaking.
UPromptSubsystem* UPlayWithPartySubsystem_Starter::GetPromptSubystem()
{
if (UAccelByteWarsGameInstance* GameInstance = Cast<UAccelByteWarsGameInstance>(GetGameInstance()))
{
return GameInstance->GetSubsystem<UPromptSubsystem>();
}
return nullptr;
}PlayWithPartySubsystem_Starter
uses some constants from thePlayWithPartyModels
class located in/Source/AccelByteWars/TutorialModules/PlayingWithParty/PlayWithPartyModels.h
. This file contains string constants you can use to show notifications and prompt messages. It also contains a constant namedPARTY_MEMBERS_GAME_SESSION_ID
that you will use to save party members' game session IDs.#define PARTY_MEMBERS_GAME_SESSION_ID "PARTY_MEMBERS_GAME_SESSION_ID"
#define ACCELBYTEWARS_LOCTEXT_NAMESPACE "AccelByteWars"
#define PARTY_MATCHMAKING_STARTED_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Party Matchmaking Started", "Party Matchmaking Started by Party Leader")
#define PARTY_MATCHMAKING_SUCCESS_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Party Matchmaking Success", "Party Matchmaking Found, Joining Match")
#define PARTY_MATCHMAKING_FAILED_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Party Matchmaking Failed", "Party Matchmaking failed")
#define PARTY_MATCHMAKING_CANCELED_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Party Matchmaking Canceled", "Party Matchmaking is canceled by party leader")
#define PARTY_MATCHMAKING_EXPIRED_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Party Matchmaking Expired", "Party Matchmaking expired")
#define PARTY_MATCHMAKING_SAFEGUARD_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Party Matchmaking Safeguard", "Matchmaking with this game mode is not supported when in a party")
#define JOIN_PARTY_GAME_SESSION_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Join Party Game Session", "Joining Party Leader Game Session")
#define JOIN_PARTY_GAME_SESSION_FAILED_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Join Party Game Session Failed", "Failed to Join Party Game Session")
#define JOIN_PARTY_GAME_SESSION_CANCELED_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Join Party Game Session Canceled", "Party game session is canceled by party leader")
#define JOIN_PARTY_GAME_SESSION_WAIT_SERVER_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Join Party Game Session Wait Server", "Joined Party Game Session. Waiting for Server")
#define JOIN_PARTY_GAME_SESSION_SERVER_ERROR_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Join Party Game Session Server Error", "Party Game Session failure. Cannot find game server.")
#define JOIN_PARTY_GAME_SESSION_SAFEGUARD_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Join Party Game Session Safeguard", "Cannot join session. Insufficient slots to join with party")
#define PARTY_GAME_SESSION_LEADER_SAFEGUARD_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Party Game Session Leader Safeguard", "Cannot play online session since party members are on other session")
#define PARTY_GAME_SESSION_MEMBER_SAFEGUARD_MESSAGE NSLOCTEXT(ACCELBYTEWARS_LOCTEXT_NAMESPACE, "Party Game Session Member Safeguard", "Only party leader can start online session")
Implement matchmaking with a party
In this section, you will implement matchmaking with a party.
The AGS OSS has built-in functions to handle matchmaking with parties automatically, so in this section, you will only need to implement the relevant callbacks to show a prompt to the player about the party matchmaking events. These prompts will only be shown to the party members, you don't need to show the prompts to the party leader since they will be initiating matchmaking.
Open the
PlayWithPartySubsystem_Starter
class Header file and create the following function declarations.protected:
void OnStartPartyMatchmakingComplete();
void OnPartyMatchmakingComplete(FName SessionName, bool bSucceeded);
void OnPartyMatchmakingCanceled();
void OnPartyMatchmakingExpired(TSharedPtr<FOnlineSessionSearchAccelByte> SearchHandler);Define these functions starting with
OnStartPartyMatchmakingComplete()
. Open thePlayWithPartySubsystem_Starter
class CPP file and add the code below. This function will show a notification that the party matchmaking has started. The notification will only be shown to the party members.void UPlayWithPartySubsystem_Starter::OnStartPartyMatchmakingComplete()
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on start party matchmaking completed. Interfaces or online session are not valid."));
return;
}
// Abort if not a party matchmaking.
if (!GetSessionInterface()->IsInPartySession() ||
GetOnlineSession()->GetPartyMembers().Num() <= 1)
{
return;
}
UE_LOG_PLAYINGWITHPARTY(Log, TEXT("Party matchmaking started."));
/* Show notification that the party matchmaking is started.
* Only show the notification if the player is a party member.*/
FUniqueNetIdPtr UserId = nullptr;
if (GetIdentityInterface())
{
UserId = GetIdentityInterface()->GetUniquePlayerId(0);
}
if (GetOnlineSession()->IsPartyLeader(UserId))
{
return;
}
if (GetPromptSubystem())
{
GetPromptSubystem()->HideLoading();
GetPromptSubystem()->ShowLoading(PARTY_MATCHMAKING_STARTED_MESSAGE);
}
}Define the
OnPartyMatchmakingComplete()
function. In thePlayWithPartySubsystem_Starter
class CPP file, add the code below. This function will show a prompt to the party whether matchmaking has succeeded or not. The prompt will only be shown to the party members.void UPlayWithPartySubsystem_Starter::OnPartyMatchmakingComplete(FName SessionName, bool bSucceeded)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on party matchmaking completed. Interfaces or online session are not valid."));
return;
}
// Abort if not a party matchmaking.
if (!GetSessionInterface()->IsInPartySession() ||
GetOnlineSession()->GetPartyMembers().Num() <= 1 ||
!GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession).IsEqual(SessionName))
{
return;
}
if (bSucceeded)
{
UE_LOG_PLAYINGWITHPARTY(Log, TEXT("Party matchmaking found. Currently joining the match."));
}
else
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Party matchmaking failed."));
}
/* Show notification that the party matchmaking is completed.
* Only show the notification if the player is a party member.*/
FUniqueNetIdPtr UserId = nullptr;
if (GetIdentityInterface())
{
UserId = GetIdentityInterface()->GetUniquePlayerId(0);
}
if (GetOnlineSession()->IsPartyLeader(UserId))
{
return;
}
if (GetPromptSubystem())
{
if (bSucceeded)
{
GetPromptSubystem()->HideLoading();
GetPromptSubystem()->ShowLoading(PARTY_MATCHMAKING_SUCCESS_MESSAGE);
}
else
{
GetPromptSubystem()->HideLoading();
GetPromptSubystem()->PushNotification(PARTY_MATCHMAKING_FAILED_MESSAGE, FString(""));
}
}
}Define the
OnPartyMatchmakingCanceled()
function. This function will show a notification that the party matchmaking is canceled. The notification will only be shown to the party members.void UPlayWithPartySubsystem_Starter::OnPartyMatchmakingCanceled()
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on party matchmaking canceled. Interfaces or online session are not valid."));
return;
}
// Abort if not a party matchmaking.
if (!GetSessionInterface()->IsInPartySession() ||
GetOnlineSession()->GetPartyMembers().Num() <= 1)
{
return;
}
UE_LOG_PLAYINGWITHPARTY(Log, TEXT("Party Matchmaking is canceled."));
/* Show notification that the party matchmaking is canceled.
* Only show the notification if the player is a party member.*/
FUniqueNetIdPtr UserId = nullptr;
if (GetIdentityInterface())
{
UserId = GetIdentityInterface()->GetUniquePlayerId(0);
}
if (GetOnlineSession()->IsPartyLeader(UserId))
{
return;
}
if (GetPromptSubystem())
{
GetPromptSubystem()->HideLoading();
GetPromptSubystem()->PushNotification(PARTY_MATCHMAKING_CANCELED_MESSAGE, FString(""));
}
}Define the
OnPartyMatchmakingExpired()
. This will show a notification that the party matchmaking has expired. The notification will only be shown to the party members.void UPlayWithPartySubsystem_Starter::OnPartyMatchmakingExpired(TSharedPtr<FOnlineSessionSearchAccelByte> SearchHandler)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on party matchmaking expired. Interfaces or online session are not valid."));
return;
}
// Abort if not a party matchmaking.
if (!GetSessionInterface()->IsInPartySession() ||
GetOnlineSession()->GetPartyMembers().Num() <= 1)
{
return;
}
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Party matchmaking expired."));
/* Show notification that the party matchmaking is expired.
* Only show the notification if the player is a party member.*/
FUniqueNetIdPtr UserId = nullptr;
if (GetIdentityInterface())
{
UserId = GetIdentityInterface()->GetUniquePlayerId(0);
}
if (GetOnlineSession()->IsPartyLeader(UserId))
{
return;
}
if (GetPromptSubystem())
{
GetPromptSubystem()->HideLoading();
GetPromptSubystem()->PushNotification(PARTY_MATCHMAKING_EXPIRED_MESSAGE, FString(""));
}
}Bind those functions to the respective online session events. You can do this by adding the code below in the predefined
Initialize()
function, which is the first function to be called when the subsystem is initialized.void UPlayWithPartySubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
if (GetSessionInterface())
{
// Bind party matchmaking events.
GetSessionInterface()->OnMatchmakingStartedDelegates.AddUObject(this, &ThisClass::OnStartPartyMatchmakingComplete);
GetSessionInterface()->OnMatchmakingCompleteDelegates.AddUObject(this, &ThisClass::OnPartyMatchmakingComplete);
GetSessionInterface()->OnMatchmakingCanceledDelegates.AddUObject(this, &ThisClass::OnPartyMatchmakingCanceled);
GetSessionInterface()->OnMatchmakingExpiredDelegates.AddUObject(this, &ThisClass::OnPartyMatchmakingExpired);
// ...
}
// ...
}Unbind those functions when the subsystem is deinitialized. Add the code below in the predefined
Deinitialize()
function.void UPlayWithPartySubsystem_Starter::Deinitialize()
{
// ...
if (GetSessionInterface())
{
// Unbind party matchmaking events.
GetSessionInterface()->OnMatchmakingStartedDelegates.RemoveAll(this);
GetSessionInterface()->OnMatchmakingCompleteDelegates.RemoveAll(this);
GetSessionInterface()->OnMatchmakingCanceledDelegates.RemoveAll(this);
GetSessionInterface()->OnMatchmakingExpiredDelegates.RemoveAll(this);
// ...
}
// ...
}
Implement playing game sessions with a party
The Create Session, Match Session, and Browse Match features in the Byte Wars are essentially game sessions. In this section, you will implement playing game sessions with a party.
Let's understand how to bring the party to a game session first. The party leader creates or joins a game session. Once the party leader successfully enters the game session, the party leader will send a game session invitation to the party members. Then, the party members will join the party leader's game session through that invitation.
Create the functions to handle party game session invitations. Open the
PlayWithPartySubsystem_Starter
class header file and add the following code:protected:
// ...
void InvitePartyMembersToJoinPartyGameSession(const FUniqueNetIdPtr LeaderUserId);
void OnPartyGameSessionInviteReceived(const FUniqueNetId& UserId, const FUniqueNetId& FromId, const FOnlineSessionInviteAccelByte& Invite);Define these functions starting with
InvitePartyMembersToJoinPartyGameSession()
. In thePlayWithPartySubsystem_Starter
class CPP file, add the code below. This function will send the game session invitation to the party members, excluding the party leader.void UPlayWithPartySubsystem_Starter::InvitePartyMembersToJoinPartyGameSession(const FUniqueNetIdPtr LeaderUserId)
{
if (!LeaderUserId)
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot invite party members to join party game session. Party leader is not valid."));
return;
}
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on invite party members to join party game session. Interfaces or online session are not valid."));
return;
}
if (!GetSessionInterface()->IsInPartySession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot invite party members to join party game session. Inviter is not in a party."));
return;
}
if (!GetOnlineSession()->IsPartyLeader(LeaderUserId))
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot invite party members to join party game session. Inviter is not the party leader."));
return;
}
// Not necessary to send party game session invitation if there is only one member.
if (GetOnlineSession()->GetPartyMembers().Num() <= 1)
{
return;
}
// Send party game session invitation to each party members.
for (auto& Member : GetOnlineSession()->GetPartyMembers())
{
if (GetOnlineSession()->IsPartyLeader(Member))
{
continue;
}
if (FUniqueNetIdAccelByteUserPtr MemberABId = StaticCastSharedRef<const FUniqueNetIdAccelByteUser>(Member))
{
UE_LOG_PLAYINGWITHPARTY(Log, TEXT("Send party game session invitation to: %s."), *MemberABId->GetAccelByteId());
GetSessionInterface()->SendSessionInviteToFriend(
LeaderUserId.ToSharedRef().Get(),
GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession),
Member.Get());
}
}
}Define the
OnPartyGameSessionInviteReceived()
function. Still in thePlayWithPartySubsystem_Starter
class CPP file, add the code below. This function will accept the game session invitation from the party leader, enabling the party members to join the party leader's game session.void UPlayWithPartySubsystem_Starter::OnPartyGameSessionInviteReceived(const FUniqueNetId& UserId, const FUniqueNetId& FromId, const FOnlineSessionInviteAccelByte& Invite)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on party game session invite received. Interfaces or online session are not valid."));
return;
}
// Abort if not a party game session and if the invitation is not from the party leader.
if (Invite.SessionType != EAccelByteV2SessionType::GameSession ||
!GetSessionInterface()->IsInPartySession() ||
!GetOnlineSession()->IsPartyLeader(FromId.AsShared()) ||
GetOnlineSession()->GetPartyMembers().Num() <= 1)
{
return;
}
// Abort if the receiver is the party leader.
if (GetOnlineSession()->IsPartyLeader(UserId.AsShared()))
{
return;
}
// Join party game session.
const APlayerController* PC = GetOnlineSession()->GetPlayerControllerByUniqueNetId(UserId.AsShared());
if (!PC)
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot join a party game session invitation from party leader. PlayerController is not valid."));
return;
}
const int32 LocalUserNum = GetOnlineSession()->GetLocalUserNumFromPlayerController(PC);
UE_LOG_PLAYINGWITHPARTY(Log, TEXT("Received a party game session invitation from party leader. Joining the party game session."));
if (GetPromptSubystem())
{
GetPromptSubystem()->HideLoading();
GetPromptSubystem()->ShowLoading(JOIN_PARTY_GAME_SESSION_MESSAGE);
}
GetOnlineSession()->JoinSession(LocalUserNum,
GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession),
Invite.Session);
}Create functions to handle party game session events. Open the
PlayWithPartySubsystem_Starter
class Header file and add the following code:protected:
// ...
void UpdatePartyMemberGameSession(const FUniqueNetIdPtr MemberUserId, const bool bResetGameSessionId = false);
bool IsGameSessionDifferFromParty(const FUniqueNetIdPtr MemberUserId);
void OnCreatePartyGameSessionComplete(FName SessionName, bool bSucceeded);
void OnJoinPartyGameSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
void OnLeavePartyGameSessionComplete(FName SessionName, bool bSucceeded);
void OnPartyGameSessionUpdateReceived(FName SessionName);By default, unlike party matchmaking, any party member can create or join a game session, so it is not necessary for the party leader to initiate the game session. In this tutorial, however, you only want the party leader to initiate the game session. You also want the party game session to be started when each party member is not in other game sessions. To do this, you need to save the game session ID of each party member to the party session settings. This way, you can tell if any party member is in another game session. That is what the
UpdatePartyMemberGameSession()
will do. Open thePlayWithPartySubsystem_Starter
class CPP file and add the following code:void UPlayWithPartySubsystem_Starter::UpdatePartyMemberGameSession(const FUniqueNetIdPtr MemberUserId, const bool bResetGameSessionId)
{
if (!MemberUserId)
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot update party member game session. Party member is not valid."));
return;
}
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot update party member game session. Interfaces or online session are not valid."));
return;
}
if (!GetSessionInterface()->IsInPartySession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot update party member game session. Party member is not in a party."));
return;
}
GetSessionInterface()->RefreshSession(
GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::PartySession),
FOnRefreshSessionComplete::CreateWeakLambda(this, [this, MemberUserId, bResetGameSessionId](bool bWasSuccessful)
{
if (!bWasSuccessful)
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot update party member game session. Failed to refresh party session."));
return;
}
FNamedOnlineSession* GameSession = GetSessionInterface()->GetNamedSession(
GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession));
// Get game session id only if it has dedicated server or host.
FString GameSessionId = TEXT("");
if (GameSession && !bResetGameSessionId)
{
FString ServerAddress = TEXT("");
GetSessionInterface()->GetResolvedConnectString(GameSession->SessionName, ServerAddress);
const bool bIsP2PHost = GetSessionInterface()->IsPlayerP2PHost(MemberUserId.ToSharedRef().Get(), GameSession->SessionName);
if (!ServerAddress.IsEmpty() || bIsP2PHost)
{
GameSessionId = GameSession->GetSessionIdStr();
}
}
FNamedOnlineSession* PartySession = GetSessionInterface()->GetPartySession();
if (!PartySession)
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot update party member game session. Party session is not valid."));
return;
}
// Construct party game session data.
const FUniqueNetIdAccelByteUserRef MemberUserABId = StaticCastSharedRef<const FUniqueNetIdAccelByteUser>(MemberUserId.ToSharedRef());
TSharedPtr<FJsonObject> MembersGameSessionId = MakeShareable(new FJsonObject);
FOnlineSessionSetting PartyGameSessionSetting;
if (PartySession->SessionSettings.Settings.Contains(PARTY_MEMBERS_GAME_SESSION_ID))
{
PartyGameSessionSetting = PartySession->SessionSettings.Settings[PARTY_MEMBERS_GAME_SESSION_ID];
TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(PartyGameSessionSetting.Data.ToString());
if (!FJsonSerializer::Deserialize(JsonReader, MembersGameSessionId))
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot update party member game session. Failed to parse party members game session."));
return;
}
}
// Update party member game session id.
if (!MembersGameSessionId)
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot update party member game session. Failed to parse party members game session."));
return;
}
MembersGameSessionId->RemoveField(MemberUserABId->GetAccelByteId());
if (!GameSessionId.IsEmpty())
{
MembersGameSessionId->SetStringField(MemberUserABId->GetAccelByteId(), GameSessionId);
}
// Remove invalid party member data.
for (auto Pair : MembersGameSessionId->Values)
{
bool bIsValidMember = false;
for (auto& ValidMember : PartySession->RegisteredPlayers)
{
const FUniqueNetIdAccelByteUserRef ValidMemberUserABId = StaticCastSharedRef<const FUniqueNetIdAccelByteUser>(ValidMember);
if (Pair.Key.Equals(ValidMemberUserABId->GetAccelByteId()))
{
bIsValidMember = true;
break;
}
}
if (!bIsValidMember)
{
MembersGameSessionId->RemoveField(Pair.Key);
}
}
// Update party game session data to the party session settings.
FString MembersGameSessionIdStr;
TSharedRef<TJsonWriter<TCHAR>> JsonWriter = TJsonWriterFactory<TCHAR>::Create(&MembersGameSessionIdStr);
if (!FJsonSerializer::Serialize(MembersGameSessionId.ToSharedRef(), JsonWriter))
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot update party member game session. Failed to parse party members game session."));
return;
}
PartyGameSessionSetting.Data = MembersGameSessionIdStr;
// Update party game session to store party game session data.
PartySession->SessionSettings.Settings.Remove(PARTY_MEMBERS_GAME_SESSION_ID);
PartySession->SessionSettings.Settings.Add(PARTY_MEMBERS_GAME_SESSION_ID, PartyGameSessionSetting);
GetSessionInterface()->UpdateSession(
GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::PartySession),
PartySession->SessionSettings);
}
));
}Define the
IsGameSessionDifferFromParty()
function. This is a helper function to check whether the current player has a different game session than other party members. You will use this helper function later to make sure that the party game session can only be started if party members are not in other game sessions.bool UPlayWithPartySubsystem_Starter::IsGameSessionDifferFromParty(const FUniqueNetIdPtr MemberUserId)
{
if (!MemberUserId)
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot check whether the game session is differ from party. Party member is not valid."));
return false;
}
bool bResult = false;
// Abort if interfaces and data is not valid.
if (!GetSessionInterface() || !GetOnlineSession() || !MemberUserId)
{
return bResult;
}
// Abort if not in a party session.
FNamedOnlineSession* PartySession = GetSessionInterface()->GetPartySession();
if (!PartySession)
{
return bResult;
}
// Get current game session id.
FString GameSessionId;
FNamedOnlineSession* GameSession = GetSessionInterface()->GetNamedSession(
GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession));
if (GameSession)
{
GameSessionId = GameSession->GetSessionIdStr();
}
// Get party game session data.
FOnlineSessionSetting* PartyGameSessionSetting = PartySession->SessionSettings.Settings.Find(PARTY_MEMBERS_GAME_SESSION_ID);
if (!PartyGameSessionSetting)
{
return bResult;
}
TSharedPtr<FJsonObject> MembersGameSessionId = MakeShareable(new FJsonObject);
TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(PartyGameSessionSetting->Data.ToString());
if (!FJsonSerializer::Deserialize(JsonReader, MembersGameSessionId))
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot check whether the game session is differ from party. Failed to parse party members game session."));
return bResult;
}
if (!MembersGameSessionId)
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot check whether the game session is differ from party. Failed to parse party members game session."));
return bResult;
}
FString MemberGameSessionIdStr;
for (auto& Member : GetOnlineSession()->GetPartyMembers())
{
// Not necessary to check the player itself.
if (Member.Get() == MemberUserId.ToSharedRef().Get())
{
continue;
}
// Check if the current game session is the same as the party.
const FUniqueNetIdAccelByteUserRef MemberABId = StaticCastSharedRef<const FUniqueNetIdAccelByteUser>(Member);
if (!MembersGameSessionId->TryGetStringField(MemberABId->GetAccelByteId(), MemberGameSessionIdStr))
{
continue;
}
if (!GameSessionId.Equals(MemberGameSessionIdStr))
{
bResult = true;
break;
}
}
return bResult;
}Define the
OnCreatePartyGameSessionComplete()
function. Still in thePlayWithPartySubsystem_Starter
class CPP file, add the code below. When the party leader creates a game session, this function will send invitations so the party members can join the party game session. It will also update the game session ID to be saved in the party session using theUpdatePartyMemberGameSession()
function.void UPlayWithPartySubsystem_Starter::OnCreatePartyGameSessionComplete(FName SessionName, bool bSucceeded)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on create party game session completed. Interfaces or online session are not valid."));
return;
}
// Abort if not a party game session.
if (!GetSessionInterface()->IsInPartySession() ||
!GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession).IsEqual(SessionName))
{
return;
}
// Abort if failed to create a party game session.
if (!bSucceeded)
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Failed to create party game session."));
return;
}
UE_LOG_PLAYINGWITHPARTY(Log, TEXT("Success to create party game session."));
// Update party member game session id.
FUniqueNetIdPtr UserId = nullptr;
if (GetIdentityInterface())
{
UserId = GetIdentityInterface()->GetUniquePlayerId(0);
UpdatePartyMemberGameSession(UserId);
}
// Invite party members to join the party game session.
if (GetOnlineSession()->IsPartyLeader(UserId))
{
InvitePartyMembersToJoinPartyGameSession(UserId);
}
}Define the
OnJoinPartyGameSessionComplete()
function. When the party leader joins a game session, this function will send an invitation so the party members can join the same party game session. It will also update the game session ID to be saved in the party session using theUpdatePartyMemberGameSession()
function.void UPlayWithPartySubsystem_Starter::OnJoinPartyGameSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on join party game session completed. Interfaces or online session are not valid."));
return;
}
// Abort if not a party game session.
if (!GetSessionInterface()->IsInPartySession() ||
!GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession).IsEqual(SessionName))
{
return;
}
const bool bSucceeded = (Result == EOnJoinSessionCompleteResult::Type::Success);
if (bSucceeded)
{
UE_LOG_PLAYINGWITHPARTY(Log, TEXT("Success to join party game session."));
}
else
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Failed to join party game session."));
}
// Update party member game session id.
FUniqueNetIdPtr UserId = nullptr;
if (GetIdentityInterface() && bSucceeded)
{
UserId = GetIdentityInterface()->GetUniquePlayerId(0);
UpdatePartyMemberGameSession(UserId);
}
// Send invitation to other party members if the one who joined the session is party leader.
if (GetOnlineSession()->IsPartyLeader(UserId) && bSucceeded)
{
InvitePartyMembersToJoinPartyGameSession(UserId);
}
// Show relevant notification if the one who joined the session is party member.
else if (GetPromptSubystem())
{
GetPromptSubystem()->HideLoading();
FString ServerAddress = TEXT("");
GetSessionInterface()->GetResolvedConnectString(SessionName, ServerAddress);
if (bSucceeded && ServerAddress.IsEmpty())
{
GetPromptSubystem()->ShowLoading(JOIN_PARTY_GAME_SESSION_WAIT_SERVER_MESSAGE);
}
else
{
GetPromptSubystem()->PushNotification(JOIN_PARTY_GAME_SESSION_FAILED_MESSAGE, FString(""));
}
}
}Define the
OnLeavePartyGameSessionComplete()
function. This function will also update the game session ID to be saved in the party session using theUpdatePartyMemberGameSession()
function. Since it is called when the party member leaves the game session, the game session ID is empty. So, in this case, callingUpdatePartyMemberGameSession()
clears the party member's game session ID from the party session.void UPlayWithPartySubsystem_Starter::OnLeavePartyGameSessionComplete(FName SessionName, bool bSucceeded)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on leave party game session completed. Interfaces or online session are not valid."));
return;
}
// Abort if not a party game session.
if (!GetSessionInterface()->IsInPartySession() ||
!GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession).IsEqual(SessionName))
{
return;
}
// Abort if failed to leave a party game session.
if (!bSucceeded)
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Failed to leave party game session."));
return;
}
UE_LOG_PLAYINGWITHPARTY(Log, TEXT("Success to leave party game session."));
// Update party member game session id.
FUniqueNetIdPtr UserId = nullptr;
if (GetIdentityInterface())
{
UserId = GetIdentityInterface()->GetUniquePlayerId(0);
UpdatePartyMemberGameSession(UserId);
}
}Next, define the
OnPartyGameSessionUpdateReceived()
function. This function will be called when the party session gets updated, for example when a party member gets kicked, and a new party leader gets promoted. When that happens, you need to update the game session ID to be saved in the party session by calling theUpdatePartyMemberGameSession()
function.void UPlayWithPartySubsystem_Starter::OnPartyGameSessionUpdateReceived(FName SessionName)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on party game session update received. Interfaces or online session are not valid."));
return;
}
// Abort if not a party game session.
if (!GetSessionInterface()->IsInPartySession() ||
!GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession).IsEqual(SessionName))
{
return;
}
// Update party member game session id.
FUniqueNetIdPtr UserId = nullptr;
if (GetIdentityInterface())
{
UserId = GetIdentityInterface()->GetUniquePlayerId(0);
UpdatePartyMemberGameSession(UserId);
}
}Bind those functions to the respective online session events. You can do this by adding the code below to the predefined
Initialize()
function, which is the first function to be called when the subsystem is initialized.void UPlayWithPartySubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
if (GetSessionInterface())
{
// ...
// Bind party game session events.
GetSessionInterface()->OnCreateSessionCompleteDelegates.AddUObject(this, &ThisClass::OnCreatePartyGameSessionComplete);
GetSessionInterface()->OnJoinSessionCompleteDelegates.AddUObject(this, &ThisClass::OnJoinPartyGameSessionComplete);
GetSessionInterface()->OnV2SessionInviteReceivedDelegates.AddUObject(this, &ThisClass::OnPartyGameSessionInviteReceived);
GetSessionInterface()->OnDestroySessionCompleteDelegates.AddUObject(this, &ThisClass::OnLeavePartyGameSessionComplete);
GetSessionInterface()->OnSessionUpdateReceivedDelegates.AddUObject(this, &ThisClass::OnPartyGameSessionUpdateReceived);
// ...
}
// ...
}Unbind the functions when the subsystem is deinitialized. Add the following code in the predefined
Deinitialize()
function:void UPlayWithPartySubsystem_Starter::Deinitialize()
{
// ...
if (GetSessionInterface())
{
// ...
// Unbind party game session events.
GetSessionInterface()->OnCreateSessionCompleteDelegates.RemoveAll(this);
GetSessionInterface()->OnJoinSessionCompleteDelegates.RemoveAll(this);
GetSessionInterface()->OnV2SessionInviteReceivedDelegates.RemoveAll(this);
GetSessionInterface()->OnDestroySessionCompleteDelegates.RemoveAll(this);
GetSessionInterface()->OnSessionUpdateReceivedDelegates.RemoveAll(this);
// ...
}
// ...
}
Handle playing with party failures
In Byte Wars, you want the party game session to be started when each party member is not in other game sessions. So, you need to make sure the game session ID is in sync between party members, hence the UpdatePartyMemberGameSession()
function is created.
Sometimes, playing with a party can fail if, for example, there are networking issue and or there is a failure to get a dedicated server. When these events happen, you need to make sure the game session ID is still in sync between party members. In this section, you will learn how to handle that.
Open the
PlayWithPartySubsystem_Starter
class header file and declare the functions below.protected:
// ...
void OnPartyGameSessionFailure(const FUniqueNetId& UserId, ESessionFailure::Type FailureType);
void OnPartyGameSessionUpdateConflictError(FName SessionName, FOnlineSessionSettings FailedSessionSettings);
void OnPartyGameSessionServerUpdate(FName SessionName);
void OnPartyGameSessionServerError(FName SessionName, const FString& ErrorMessage);
void OnPartyGameSessionParticipantRemoved(FName SessionName, const FUniqueNetId& UserId);
void OnNetworkFailure(UWorld* World, UNetDriver* NetDriver, ENetworkFailure::Type FailureType, const FString& Message);Define these functions starting with
OnPartyGameSessionFailure()
. In thePlayWithPartySubsystem_Starter
class CPP file, add the code below. This function updates the game session ID to be saved in the party session using theUpdatePartyMemberGameSession()
function, when an unexpected error occurs that impacts game session connectivity or use.void UPlayWithPartySubsystem_Starter::OnPartyGameSessionFailure(const FUniqueNetId& UserId, ESessionFailure::Type FailureType)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on party game session update conflict error. Interfaces or online session are not valid."));
return;
}
// Abort if not a party game session.
if (!GetSessionInterface()->IsInPartySession())
{
return;
}
// Update party member game session id.
if (UserId.IsValid())
{
UpdatePartyMemberGameSession(UserId.AsShared());
}
}Define the
OnPartyGameSessionUpdateConflictError()
to update the game session ID saved in the party session when the session update fails due to a version mismatch.void UPlayWithPartySubsystem_Starter::OnPartyGameSessionUpdateConflictError(FName SessionName, FOnlineSessionSettings FailedSessionSettings)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on party game session update conflict error. Interfaces or online session are not valid."));
return;
}
// Abort if not a party game session.
if (!GetSessionInterface()->IsInPartySession() ||
!GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession).IsEqual(SessionName))
{
return;
}
// Update party member game session id.
FUniqueNetIdPtr UserId = nullptr;
if (GetIdentityInterface())
{
UserId = GetIdentityInterface()->GetUniquePlayerId(0);
UpdatePartyMemberGameSession(UserId);
}
}Define the
OnPartyGameSessionServerUpdate()
to update the game session ID saved in the party session when the party game session has received the info of the server to travel to.void UPlayWithPartySubsystem_Starter::OnPartyGameSessionServerUpdate(FName SessionName)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on party game session server update event. Interfaces or online session are not valid."));
return;
}
// Abort if not a party game session.
if (!GetSessionInterface()->IsInPartySession() ||
!GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession).IsEqual(SessionName))
{
return;
}
// Update party member game session id.
FUniqueNetIdPtr UserId = nullptr;
if (GetIdentityInterface())
{
UserId = GetIdentityInterface()->GetUniquePlayerId(0);
UpdatePartyMemberGameSession(UserId);
}
}Define the
OnPartyGameSessionServerError()
to update the game session ID saved in the party session when the party game session has failed to get the info of the server to travel to.void UPlayWithPartySubsystem_Starter::OnPartyGameSessionServerError(FName SessionName, const FString& ErrorMessage)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on party game session server error event. Interfaces or online session are not valid."));
return;
}
// Abort if not a party game session.
if (!GetSessionInterface()->IsInPartySession() ||
!GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession).IsEqual(SessionName))
{
return;
}
// Reset party member game session id.
FUniqueNetIdPtr UserId = nullptr;
if (GetIdentityInterface())
{
UserId = GetIdentityInterface()->GetUniquePlayerId(0);
UpdatePartyMemberGameSession(UserId, true);
}
// Hide loading party game session related prompts if any.
if (GetPromptSubystem())
{
GetPromptSubystem()->HideLoading();
GetPromptSubystem()->PushNotification(JOIN_PARTY_GAME_SESSION_SERVER_ERROR_MESSAGE, FString(""));
}
}Define the
OnNetworkFailure()
to update the game session ID saved in the party session when there is a network connection error (for example: network timeout, connection to host disconnected).void UPlayWithPartySubsystem_Starter::OnNetworkFailure(UWorld* World, UNetDriver* NetDriver, ENetworkFailure::Type FailureType, const FString& Message)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on-network failure event. Interfaces or online session are not valid."));
return;
}
// Abort if not a party game session.
if (!GetSessionInterface()->IsInPartySession())
{
return;
}
// Reset party member game session id.
FUniqueNetIdPtr UserId = nullptr;
if (GetIdentityInterface())
{
UserId = GetIdentityInterface()->GetUniquePlayerId(0);
UpdatePartyMemberGameSession(UserId, true);
}
}Define the
OnPartyGameSessionParticipantRemoved()
by adding the code below. This function will be called when the party members have joined the party game session, but then a member leaves that game session. When this happens, if the member who left the game session is the party leader, and if the game session has not received the info of the server to travel to, then it means the party game session creation was not complete. Thus, the party members need to leave that game session.void UPlayWithPartySubsystem_Starter::OnPartyGameSessionParticipantRemoved(FName SessionName, const FUniqueNetId& UserId)
{
if (!GetSessionInterface() || !GetOnlineSession())
{
UE_LOG_PLAYINGWITHPARTY(Warning, TEXT("Cannot handle on party game session participant removed event. Interfaces or online session are not valid."));
return;
}
// Abort if not a party game session.
if (!GetSessionInterface()->IsInPartySession() ||
!GetOnlineSession()->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession).IsEqual(SessionName))
{
return;
}
FString ServerAddress = TEXT("");
GetSessionInterface()->GetResolvedConnectString(SessionName, ServerAddress);
const FUniqueNetIdPtr PartyLeaderUserId = GetOnlineSession()->GetPartyLeader();
const bool bIsLeaverThePartyLeader = PartyLeaderUserId && UserId == PartyLeaderUserId.ToSharedRef().Get();
const bool bIsGameSessionReceivedServer = !ServerAddress.IsEmpty();
/* If the party leader leaves the game session before it has received the server,
* the party member must leave the game session, too.
* This is to sync game session creation between the party leader and party members. */
if (bIsLeaverThePartyLeader && !bIsGameSessionReceivedServer)
{
// Push notification to the party that the game session is canceled.
if (GetPromptSubystem())
{
GetPromptSubystem()->HideLoading();
GetPromptSubystem()->PushNotification(JOIN_PARTY_GAME_SESSION_CANCELED_MESSAGE, FString(""));
}
// Leave party game session.
GetOnlineSession()->LeaveSession(SessionName);
}
}Bind functions above to the respective online session events. You can do this by adding the code below to the predefined
Initialize()
function, which is the first function to be called when the subsystem is initialized.void UPlayWithPartySubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
if (GetSessionInterface())
{
// ...
GetSessionInterface()->OnSessionFailureDelegates.AddUObject(this, &ThisClass::OnPartyGameSessionFailure);
GetSessionInterface()->OnSessionUpdateConflictErrorDelegates.AddUObject(this, &ThisClass::OnPartyGameSessionUpdateConflictError);
GetSessionInterface()->OnSessionServerUpdateDelegates.AddUObject(this, &ThisClass::OnPartyGameSessionServerUpdate);
GetSessionInterface()->OnSessionServerErrorDelegates.AddUObject(this, &ThisClass::OnPartyGameSessionServerError);
GetSessionInterface()->OnSessionParticipantRemovedDelegates.AddUObject(this, &ThisClass::OnPartyGameSessionParticipantRemoved);
}
// Handle network failure.
if (GEngine)
{
GEngine->NetworkFailureEvent.AddUObject(this, &ThisClass::OnNetworkFailure);
}
// ...
}Unbind the functions when the subsystem is deinitialized. Add the following code in the predefined
Deinitialize()
function:void UPlayWithPartySubsystem_Starter::Deinitialize()
{
// ...
if (GetSessionInterface())
{
// ...
GetSessionInterface()->OnSessionFailureDelegates.RemoveAll(this);
GetSessionInterface()->OnSessionUpdateConflictErrorDelegates.RemoveAll(this);
GetSessionInterface()->OnSessionServerUpdateDelegates.RemoveAll(this);
GetSessionInterface()->OnSessionServerErrorDelegates.RemoveAll(this);
GetSessionInterface()->OnSessionParticipantRemovedDelegates.RemoveAll(this);
}
// Remove network failure handler
if (GEngine)
{
GEngine->NetworkFailureEvent.RemoveAll(this);
}
// ...
}
Resources
The files used in this tutorial section are available in the Unreal Engine Byte Wars GitHub repository.