Implement subsystem - Joinable sessions with peer-to-peer - (Unreal Engine module)
Implementation for a match session is done through two different classes: the Online Session class and Game Instance Subsystem class. The Online Session is where you will implement all logic related to game client, while the Game Instance Subsystem is for the server logic.
Browse session flow
Before you begin, understand how the browse session flow works for peer-to-peer (P2P).
Set up game client Online Session
An Online Session class called MatchSessionP2POnlineSession_Starter
has been created to handle the match session game client implementation. This Online Session class provides necessary declarations and definitions, so you can begin implementing right away.
You can find the MatchSessionP2POnlineSession_Starter
class files at the following locations:
- Header file:
/Source/AccelByteWars/TutorialModules/Play/MatchSessionP2P/MatchSessionP2POnlineSession_Starter.h
- CPP file:
/Source/AccelByteWars/TutorialModules/Play/MatchSessionP2P/MatchSessionP2POnlineSession_Starter.cpp
Take a look at what has been provided in the class. Keep in mind that you still have access to all the functions from the Introduction to Session module since we are using USessionEssentialsOnlineSession
as the parent for this Online Session class.
In the
MatchSessionP2POnlineSession_Starter
Header file, you will see functions and variables withQueryUserInfo
in the name. For tutorial purposes, ignore those functions and variables. They are not needed for match session implementation, but they are needed for Byte Wars. Byte Wars uses them to retrieve a player's info from the backend on the server and to show the session owner's username.private:
// ...
void OnQueryUserInfoForFindSessionComplete(
const FOnlineError& Error,
const TArray<TSharedPtr<FUserOnlineAccountAccelByte>>& UsersInfo);Still in the Header file, you will see a delegate and its getter. This delegate will be our way to connect the UI to the response call when making a request.
public:
// ...
virtual FOnServerSessionUpdateReceived* GetOnSessionServerUpdateReceivedDelegates() override
{
return &OnSessionServerUpdateReceivedDelegates;
}
// ...
virtual FOnMatchSessionFindSessionsComplete* GetOnFindSessionsCompleteDelegates() override
{
return &OnFindSessionsCompleteDelegates;
}private:
// ...
FOnServerSessionUpdateReceived OnSessionServerUpdateReceivedDelegates;
// ...
FOnMatchSessionFindSessionsComplete OnFindSessionsCompleteDelegates;There's also a
TMap
variable calledMatchSessionTemplateNameMap
. You will set the value of this to the session templates names that you set up previously.public:
// ...
const TMap<TPair<EGameModeNetworkType, EGameModeType>, FString> MatchSessionTemplateNameMap = {
{{EGameModeNetworkType::P2P, EGameModeType::FFA}, "unreal-elimination-p2p"},
{{EGameModeNetworkType::P2P, EGameModeType::TDM}, "unreal-teamdeathmatch-p2p"}
};There are two variables that will be used for the implementation that you will add later.
private:
// ...
bool bIsInSessionServer = false;
// ...
TSharedRef<FOnlineSessionSearch> SessionSearch = MakeShared<FOnlineSessionSearch>(FOnlineSessionSearch());
int32 LocalUserNumSearching;
Client travel to server or travel as host
For P2P, you need two types travel logic: travel to server for the player joining and travel as host for the player creating the session.
Begin with declaring the functions. Open the
MatchSessionP2POnlineSession_Starter
Header file and add the following declarations:public:
// ...
virtual bool TravelToSession(const FName SessionName) override;protected:
virtual void OnSessionServerUpdateReceived(FName SessionName) override;
virtual void OnSessionServerErrorReceived(FName SessionName, const FString& Message) override;
virtual void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result) override;
virtual void OnCreateSessionComplete(FName SessionName, bool bSucceeded) override;Open the
MatchSessionP2POnlineSession_Starter
CPP file and add the implementations below. TheTravelToSession
function, as the name suggests, will retrieve the server's IP address from the cached session info and attempt to travel to that server. TheOnSessionServerUpdateReceived
andOnSessionServerErrorReceived
are the functions that will be executed when the game client receives any update regarding the server from the backend. Since this is a P2P session, upon successful session creation, we need travel the player as the host right away, hence the implementation onOnCreateSessionComplete
.bool UMatchSessionP2POnlineSession_Starter::TravelToSession(const FName SessionName)
{
UE_LOG_MATCHSESSIONP2P(Verbose, TEXT("called"))
if (GetSessionType(SessionName) != EAccelByteV2SessionType::GameSession)
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("Not a game session"));
return false;
}
// Get session info
const FNamedOnlineSession* Session = GetSession(SessionName);
if (!Session)
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("The session is invalid"));
return false;
}
const TSharedPtr<FOnlineSessionInfo> SessionInfo = Session->SessionInfo;
if (!SessionInfo.IsValid())
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("The session info is invalid"));
return false;
}
const TSharedPtr<FOnlineSessionInfoAccelByteV2> AbSessionInfo = StaticCastSharedPtr<FOnlineSessionInfoAccelByteV2>(SessionInfo);
if (!AbSessionInfo.IsValid())
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("The session info is not FOnlineSessionInfoAccelByteV2"));
return false;
}
// get player controller of the local owner of the user
APlayerController* PlayerController = GetPlayerControllerByUniqueNetId(Session->LocalOwnerId);
// if nullptr, treat as failed
if (!PlayerController)
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("Can't find player controller with the session's local owner's unique ID"));
return false;
}
AAccelByteWarsPlayerController* AbPlayerController = Cast<AAccelByteWarsPlayerController>(PlayerController);
if (!AbPlayerController)
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("Player controller is not (derived from) AAccelByteWarsPlayerController"));
return false;
}
FString ServerAddress = "";
// If local user is not the P2P host -> connect to host
if (!(AbSessionInfo->GetServerType() == EAccelByteV2SessionConfigurationServerType::P2P && Session->bHosting))
{
UE_LOG_MATCHSESSIONP2P(Log, TEXT("Host is not a P2P host, traveling to host"));
GetABSessionInt()->GetResolvedConnectString(SessionName, ServerAddress);
if (ServerAddress.IsEmpty())
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("Can't find session's server address"));
return false;
}
}
else
{
UE_LOG_MATCHSESSIONP2P(Log, TEXT("Host is a P2P host, traveling as listen server"));
ServerAddress = "MainMenu?listen";
}
if (!bIsInSessionServer)
{
AbPlayerController->DelayedClientTravel(ServerAddress, TRAVEL_Absolute);
bIsInSessionServer = true;
}
else
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("Already in session's server"));
}
return true;
}void UMatchSessionP2POnlineSession_Starter::OnSessionServerUpdateReceived(FName SessionName)
{
UE_LOG_MATCHSESSIONP2P(Verbose, TEXT("called"))
if (bLeavingSession)
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("called but leave session is currently running. Canceling attempt to travel to server"))
OnSessionServerUpdateReceivedDelegates.Broadcast(SessionName, FOnlineError(true), false);
return;
}
const bool bHasClientTravelTriggered = TravelToSession(SessionName);
OnSessionServerUpdateReceivedDelegates.Broadcast(SessionName, FOnlineError(true), bHasClientTravelTriggered);
}void UMatchSessionP2POnlineSession_Starter::OnSessionServerErrorReceived(FName SessionName, const FString& Message)
{
UE_LOG_MATCHSESSIONP2P(Verbose, TEXT("called"))
FOnlineError Error;
Error.bSucceeded = false;
Error.ErrorMessage = FText::FromString(Message);
OnSessionServerUpdateReceivedDelegates.Broadcast(SessionName, Error, false);
}void UMatchSessionP2POnlineSession_Starter::OnJoinSessionComplete(
FName SessionName,
EOnJoinSessionCompleteResult::Type Result)
{
Super::OnJoinSessionComplete(SessionName, Result);
TravelToSession(SessionName);
}void UMatchSessionP2POnlineSession_Starter::OnCreateSessionComplete(FName SessionName, bool bSucceeded)
{
Super::OnCreateSessionComplete(SessionName, bSucceeded);
if (bSucceeded)
{
// attempt to travel -> P2P host will need to travel as listen server right now
TravelToSession(SessionName);
}
}You need to handle what happens if the client disconnects from the server for but is still connected to the session. In this case, the player will still be treated as a part of the session. To solve that, simply call
LeaveSession
whenever this disconnect happens. You will use a function provided by Unreal Engine's Online Session class:HandleDisconnectInternal
. Go back to theMatchSessionP2POnlineSession_Starter
Header file and add this declaration:protected:
// ...
virtual bool HandleDisconnectInternal(UWorld* World, UNetDriver* NetDriver) override;Open the
MatchSessionP2POnlineSession_Starter
CPP file and add these implementations:bool UMatchSessionP2POnlineSession_Starter::HandleDisconnectInternal(UWorld* World, UNetDriver* NetDriver)
{
UE_LOG_MATCHSESSIONP2P(Verbose, TEXT("called"))
LeaveSession(GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession));
bIsInSessionServer = false;
GEngine->HandleDisconnect(World, NetDriver);
return true;
}You also need to handle when the host leaves the session. In this case, the
HandleDisconnectInternal
won't be called, since they are the server. We will be using theLeaveSession
response callback to reset thebIsInSessionServer
variable. Open theMatchSessionP2POnlineSession_Starter
Header file and add this declaration:protected:
// ...
virtual void OnLeaveSessionComplete(FName SessionName, bool bSucceeded) override;Add this implementation to the
MatchSessionP2POnlineSession_Starter
CPP file:void UMatchSessionP2POnlineSession_Starter::OnLeaveSessionComplete(FName SessionName, bool bSucceeded)
{
Super::OnLeaveSessionComplete(SessionName, bSucceeded);
if (bSucceeded)
{
bIsInSessionServer = false;
}
}
Create match session
Open the
MatchSessionP2POnlineSession_Starter
Header file and make sure you set theMatchSessionTemplateNameMap
map value to the session template name that you set up previously.public:
// ...
const TMap<TPair<EGameModeNetworkType, EGameModeType>, FString> MatchSessionTemplateNameMap = {
{{EGameModeNetworkType::P2P, EGameModeType::FFA}, "unreal-elimination-p2p"},
{{EGameModeNetworkType::P2P, EGameModeType::TDM}, "unreal-teamdeathmatch-p2p"}
};Still in the header file, add this function declaration:
public:
// ...
virtual void CreateMatchSession(
const int32 LocalUserNum,
const EGameModeNetworkType NetworkType,
const EGameModeType GameModeType) override;Open the
MatchSessionP2POnlineSession_Starter
CPP file and add the implementation below. Note that you set a flag, theGAME_SESSION_REQUEST_TYPE
, in theSessionSettings
so that theFindSessions
function can tell the backend to only return sessions with that flag, removing the need to manually filter the response.void UMatchSessionP2POnlineSession_Starter::CreateMatchSession(
const int32 LocalUserNum,
const EGameModeNetworkType NetworkType,
const EGameModeType GameModeType)
{
FOnlineSessionSettings SessionSettings;
// Set a flag so we can request a filtered session from backend
SessionSettings.Set(GAME_SESSION_REQUEST_TYPE, GAME_SESSION_REQUEST_TYPE_MATCHSESSION);
// flag to signify the server which game mode to use
SessionSettings.Set(
GAMESETUP_GameModeCode,
FString(GameModeType == EGameModeType::FFA ? "ELIMINATION-P2P-USERCREATED" : "TEAMDEATHMATCH-P2P-USERCREATED"));
// Get match session template name based on game mode type
FString MatchTemplateName = MatchSessionTemplateNameMap[{EGameModeNetworkType::P2P, GameModeType}];
// Override match session template name if applicable.
if (!UTutorialModuleOnlineUtility::GetMatchSessionTemplateP2POverride().IsEmpty())
{
MatchTemplateName = UTutorialModuleOnlineUtility::GetMatchSessionTemplateP2POverride();
}
CreateSession(
LocalUserNum,
GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession),
SessionSettings,
EAccelByteV2SessionType::GameSession,
MatchTemplateName);
}
Find match session
Open the
MatchSessionP2POnlineSession_Starter
Header file and add the two function declarations below which will be the caller function and the response callback.public:
// ...
virtual void FindSessions(
const int32 LocalUserNum,
const int32 MaxQueryNum,
const bool bForce) override;protected:
// ...
virtual void OnFindSessionsComplete(bool bSucceeded) override;Open the
MatchSessionP2POnlineSession_Starter
CPP file and add the implementations below. Note that, since you set a flag in theSessionSettings
in theCreateMatchSession
implementation, you will also set the same flag as theQuerySettings
. Also note that you pass a class member variable, theSessionSearch
, when callingFindSessions
. The way theFindSessions
works is it takes a reference ofFOnlineSessionSearch
variable and updates the variable that the reference refers to, hence, theOnFindSessionsComplete
doesn't include the session info as the parameter.void UMatchSessionP2POnlineSession_Starter::FindSessions(
const int32 LocalUserNum,
const int32 MaxQueryNum,
const bool bForce)
{
UE_LOG_MATCHSESSIONP2P(Verbose, TEXT("called"))
if (SessionSearch->SearchState == EOnlineAsyncTaskState::InProgress)
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("Currently searching"))
return;
}
// check cache
if (!bForce && MaxQueryNum <= SessionSearch->SearchResults.Num())
{
UE_LOG_MATCHSESSIONP2P(Log, TEXT("Cache found"))
// return cache
ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [this]()
{
OnFindSessionsComplete(true);
}));
return;
}
SessionSearch->SearchState = EOnlineAsyncTaskState::NotStarted;
SessionSearch->MaxSearchResults = MaxQueryNum;
SessionSearch->SearchResults.Empty();
LocalUserNumSearching = LocalUserNum;
// reset
SessionSearch->QuerySettings = FOnlineSearchSettings();
// Request a filtered session from backend based on the flag we set on CreateSession_Caller
SessionSearch->QuerySettings.Set(GAME_SESSION_REQUEST_TYPE, GAME_SESSION_REQUEST_TYPE_MATCHSESSION, EOnlineComparisonOp::Equals);
if (!GetSessionInt()->FindSessions(LocalUserNum, SessionSearch))
{
ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [this]()
{
OnFindSessionsComplete(false);
}));
}
}void UMatchSessionP2POnlineSession_Starter::OnFindSessionsComplete(bool bSucceeded)
{
UE_LOG_MATCHSESSIONP2P(Log, TEXT("succeeded: %s"), *FString(bSucceeded ? "TRUE": "FALSE"))
if (bSucceeded)
{
// Remove owned session from result if exists
const FUniqueNetIdPtr LocalUserNetId = GetIdentityInt()->GetUniquePlayerId(LocalUserNumSearching);
SessionSearch->SearchResults.RemoveAll([this, LocalUserNetId](const FOnlineSessionSearchResult& Element)
{
return CompareAccelByteUniqueId(
FUniqueNetIdRepl(LocalUserNetId),
FUniqueNetIdRepl(Element.Session.OwningUserId));
});
// Get owner’s user info for queried user info.
TArray<FUniqueNetIdRef> UserIds;
for (const FOnlineSessionSearchResult& SearchResult : SessionSearch->SearchResults)
{
UserIds.AddUnique(SearchResult.Session.OwningUserId->AsShared());
}
// Trigger to query user info
if (UStartupSubsystem* StartupSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<UStartupSubsystem>())
{
StartupSubsystem->QueryUserInfo(
LocalUserNumSearching,
UserIds,
FOnQueryUsersInfoCompleteDelegate::CreateUObject(this, &ThisClass::OnQueryUserInfoForFindSessionComplete));
}
}
else
{
OnFindSessionsCompleteDelegates.Broadcast({}, false);
}
}Now that the implementation is done, bind the
OnFindSessionsComplete
to the delegate. Still in the CPP file, navigate toRegisterOnlineDelegates
and add the following code:void UMatchSessionP2POnlineSession_Starter::RegisterOnlineDelegates()
{
// ...
// Match session delegates
GetSessionInt()->OnFindSessionsCompleteDelegates.AddUObject(this, &ThisClass::OnFindSessionsComplete);
SessionSearch->SearchState = EOnlineAsyncTaskState::NotStarted;
}Implement a way to unbind that callback. Still in the CPP file, navigate to
ClearOnlineDelegates
and add the following code:void UMatchSessionP2POnlineSession_Starter::ClearOnlineDelegates()
{
// ...
GetSessionInt()->OnFindSessionsCompleteDelegates.RemoveAll(this);
}
Set up server online subsystem
A Game Instance Subsystem class called UMatchSessionP2PServerSubsystem_Starter
has been created to handle the match session server implementation. This Online Session class provides necessary declarations and definitions, so you can begin implementing right away.
You can find the UMatchSessionP2PServerSubsystem_Starter
class files at the following locations:
- Header file:
/Source/AccelByteWars/TutorialModules/Play/MatchSessionP2P/MatchSessionP2PServerSubsystem_Starter.h
- CPP file:
/Source/AccelByteWars/TutorialModules/Play/MatchSessionP2P/MatchSessionP2PServerSubsystem_Starter.cpp
Take a look at the provided class:
In the
UMatchSessionP2PServerSubsystem_Starter
Header file, you will see a region labeled Game specific. Just like theQueryUserInfo
in the Online Session, this code is not necessary for match session implementation, but is necessary for Byte Wars. This code will make sure that the player that just logged in to the server is indeed a part of the session. If they are not, then it will kick that player. For this tutorial, ignore this section.protected:
// ...
virtual void OnAuthenticatePlayerComplete_PrePlayerSetup(APlayerController* PlayerController) override;There's also a variable called
MatchmakingOnlineSession
in the Header file which is a pointer to the Online Session that you implemented earlier.private:
UPROPERTY()
UMatchSessionP2POnlineSession* OnlineSession;
Server received session
Right after the server registers itself to the backend, if a session requires a dedicated server, the backend will assign that server to the session. When this happens, the server will receive a notification. In Byte Wars, a flag in the SessionSettings
is used to determine which game mode the server should be using.
Open the
UMatchSessionP2PServerSubsystem_Starter
Header file and add this function declaration:protected:
// ...
virtual void OnServerSessionReceived(FName SessionName) override;Most of the code here is specific to Byte Wars. The lines you might need in your game are highlighted below, which retrieve the session data itself and are an example on how to retrieve your custom
SessionSettings
that you have set when creating the session.void UMatchSessionP2PServerSubsystem_Starter::OnServerSessionReceived(FName SessionName)
{
Super::OnServerSessionReceived(SessionName);
UE_LOG_MATCHSESSIONP2P(Verbose, TEXT("called"))
#pragma region "Assign game mode based on SessionTemplateName from backend"
// Get GameMode
const UWorld* World = GetWorld();
if (!World)
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("World is invalid"));
return;
}
AGameStateBase* GameState = World->GetGameState();
if (!GameState)
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("Game State is invalid"));
return;
}
AAccelByteWarsGameState* AbGameState = Cast<AAccelByteWarsGameState>(GameState);
if (!AbGameState)
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("Game State is not derived from AAccelByteWarsGameState"));
return;
}
// Get game session
if (OnlineSession->GetSessionType(SessionName) != EAccelByteV2SessionType::GameSession)
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("Is not a game session"));
return;
}
const FNamedOnlineSession* Session = OnlineSession->GetSession(SessionName);
if (!Session)
{
UE_LOG_MATCHSESSIONP2P(Warning, TEXT("The session is invalid"));
return;
}
// Get game related info from session
FString RequestedGameModeCode = TEXT("");
Session->SessionSettings.Get(GAMESETUP_GameModeCode, RequestedGameModeCode);
// Try getting manually set game rules, if GAMESETUP_DisplayName empty, go to the next flow
bool bIsCustomGame = false;
if (Session->SessionSettings.Get(GAMESETUP_IsCustomGame, bIsCustomGame) && bIsCustomGame)
{
AbGameState->AssignCustomGameMode(&Session->SessionSettings);
}
// If not complete, try getting requested game mode
else if (!RequestedGameModeCode.IsEmpty())
{
AbGameState->AssignGameMode(RequestedGameModeCode);
}
#pragma endregion
// Query all currently registered users' info
AuthenticatePlayer_OnRefreshSessionComplete(true);
}Bind that function to the Online Subsystem (OSS) delegate. Still in the CPP file, navigate to
Initialize
and add the following code:void UMatchSessionP2PServerSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
GetABSessionInt()->OnServerReceivedSessionDelegates.AddUObject(this, &ThisClass::OnServerSessionReceived);
}Unbind that function when it's no longer needed. Navigate to
Deinitialize
and add the following code:void UMatchSessionP2PServerSubsystem_Starter::Deinitialize()
{
// ...
GetABSessionInt()->OnServerReceivedSessionDelegates.RemoveAll(this);
}
Resources
- The files used in this tutorial section are available in the Unreal Byte Wars GitHub repository.
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchSessionP2P/MatchSessionP2POnlineSession_Starter.h
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchSessionP2P/MatchSessionP2POnlineSession_Starter.cpp
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchSessionP2P/MatchSessionP2PServerSubsystem_Starter.h
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchSessionP2P/MatchSessionP2PServerSubsystem_Starter.cpp