Unreal Engine Module - Create a Joinable Session Using a Dedicated Server - implement subsystem
This tutorial isn't applicable to the AccelByte Gaming Service (AGS) Starter tier. It requires the AccelByte Multiplayer Server (AMS) or Armada, which isn't currently supported on AGS Starter.
Implementation for Match Session is done through two different classes: the Online Session and Game Instance Subsystem class. The Online Session is where you will implement all logic related to the game client, while the Game Instance Subsystem is for the server logic.
Browse session flow
Before starting this module, learn how the browse session flow works. We will be utilizing join and leave session too, but, since those have already been discussed in the Module: Introduction to Multiplayer Session, we will not go into detail for those two functions in this submodule.
Set up game client Online Session
We have created an Online Session class called MatchSessionDSOnlineSession_Starter
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 MatchSessionDSOnlineSession_Starter
class files at the following:
- Header file is in
/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/MatchSessionDSOnlineSession_Starter.h
. - CPP file is in
/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/MatchSessionDSOnlineSession_Starter.cpp
.
First, let's take a look at what we have provided in the class. Keep in mind that you still have access to all the functions that you have access to in Module: Introduction to Multiplayer Session since we are using USessionEssentialsOnlineSession
as the parent for this Online Session class.
In the
MatchSessionDSOnlineSession_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 player information from backend on server and show the username of the session's owner.public:
virtual void QueryUserInfo(
const int32 LocalUserNum,
const TArray<FUniqueNetIdRef>& UserIds,
const FOnQueryUsersInfoComplete& OnComplete) override;
virtual void DSQueryUserInfo(
const TArray<FUniqueNetIdRef>& UserIds,
const FOnDSQueryUsersInfoComplete& OnComplete) override;
protected:
virtual void OnQueryUserInfoComplete(
int32 LocalUserNum,
bool bSucceeded,
const TArray<FUniqueNetIdRef>& UserIds,
const FString& ErrorMessage,
const FOnQueryUsersInfoComplete& OnComplete) override;
virtual void OnDSQueryUserInfoComplete(
const FListBulkUserInfo& UserInfoList,
const FOnDSQueryUsersInfoComplete& OnComplete) override;
private:
void OnQueryUserInfoForFindSessionComplete(
const bool bSucceeded,
const TArray<FUserOnlineAccountAccelByte*>& UsersInfo);
FDelegateHandle OnQueryUserInfoCompleteDelegateHandle;
FDelegateHandle OnDSQueryUserInfoCompleteDelegateHandle;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 see a
TMap
variable calledMatchSessionTemplateNameMap
. Set the value of this to the session templates name that you have set up in the previous submodule.public:
const TMap<TPair<EGameModeNetworkType, EGameModeType>, FString> MatchSessionTemplateNameMap = {
{{EGameModeNetworkType::DS, EGameModeType::FFA}, ""},
{{EGameModeNetworkType::DS, EGameModeType::TDM}, ""}
};Lastly, 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
After joining a game session, the backend will the server's IP address to the game client. Game client needs a way to travel to the given IP address.
Declare the functions. Open
MatchSessionDSOnlineSession_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;Open the
MatchSessionDSOnlineSession_Starter
CPP file and add the following implementations. TheTravelToSession
function 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 received any update regarding the server from backend.bool UMatchSessionDSOnlineSession_Starter::TravelToSession(const FName SessionName)
{
UE_LOG_MATCHSESSIONDS(Verbose, TEXT("called"))
if (GetSessionType(SessionName) != EAccelByteV2SessionType::GameSession)
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("Not a game session"));
return false;
}
// Get Session Info
const FNamedOnlineSession- Session = GetSession(SessionName);
if (!Session)
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("Session is invalid"));
return false;
}
const TSharedPtr<FOnlineSessionInfo> SessionInfo = Session->SessionInfo;
if (!SessionInfo.IsValid())
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("Session Info is invalid"));
return false;
}
const TSharedPtr<FOnlineSessionInfoAccelByteV2> AbSessionInfo = StaticCastSharedPtr<FOnlineSessionInfoAccelByteV2>(SessionInfo);
if (!AbSessionInfo.IsValid())
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("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_MATCHSESSIONDS(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_MATCHSESSIONDS(Warning, TEXT("Player controller is not (derived from) AAccelByteWarsPlayerController"));
return false;
}
// Make sure this is not a P2P session
if (GetABSessionInt()->IsPlayerP2PHost(GetLocalPlayerUniqueNetId(PlayerController).ToSharedRef().Get(), SessionName))
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("Session is a P2P session"));
return false;
}
FString ServerAddress = "";
GetABSessionInt()->GetResolvedConnectString(SessionName, ServerAddress);
if (ServerAddress.IsEmpty())
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("Can't find session's server address"));
return false;
}
if (!bIsInSessionServer)
{
AbPlayerController->DelayedClientTravel(ServerAddress, TRAVEL_Absolute);
bIsInSessionServer = true;
}
else
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("Already in session's server"));
}
return true;
}
void UMatchSessionDSOnlineSession_Starter::OnSessionServerUpdateReceived(FName SessionName)
{
UE_LOG_MATCHSESSIONDS(Verbose, TEXT("called"))
if (bLeavingSession)
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("called but leave session is currently running. Cancelling attempt to travel to server"))
OnSessionServerUpdateReceivedDelegates.Broadcast(SessionName, FOnlineError(true), false);
return;
}
const bool bHasClientTravelTriggered = TravelToSession (SessionName);
OnSessionServerUpdateReceivedDelegates.Broadcast(SessionName, FOnlineError(true), bHasClientTravelTriggered);
}
void UMatchSessionDSOnlineSession_Starter::OnSessionServerErrorReceived(FName SessionName, const FString& Message)
{
UE_LOG_MATCHSESSIONDS(Verbose, TEXT("called"))
FOnlineError Error;
Error.bSucceeded = false;
Error.ErrorMessage = FText::FromString(Message);
OnSessionServerUpdateReceivedDelegates.Broadcast(SessionName, Error, false);
}Now, we need to handle what happen if the client disconnects from the server for whatever reason but is still connected to session service. In this case, the said player will still be treated as a part of the session. To solve that, simply call
LeaveSession
whenever this disconnect happens. We will be using a function provided by Unreal Engine's Online Session class, theHandleDisconnectInternal
. Go back toMatchSessionDSOnlineSession_Starter
header file and add this declaration.protected:
virtual bool HandleDisconnectInternal(UWorld- World, UNetDriver- NetDriver) override;Onto the implementation. Open the
MatchSessionDSOnlineSession_Starter
CPP file and add these implementations.bool UMatchSessionDSOnlineSession_Starter::HandleDisconnectInternal(UWorld- World, UNetDriver- NetDriver)
{
UE_LOG_MATCHSESSIONDS(Verbose, TEXT("called"))
LeaveSession(GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession));
bIsInSessionServer = false;
GEngine->HandleDisconnect(World, NetDriver);
return true;
}With that, client travel to server is implemented.
Create match session
Open the
MatchSessionDSOnlineSession_Starter
header file and make sure you set theMatchSessionTemplateNameMap
map value to the session template name that you have set up in the previous submodule. If you created your session template name with the exact same as the tutorial, you can just copy the following code:public:
const TMap<TPair<EGameModeNetworkType, EGameModeType>, FString> MatchSessionTemplateNameMap = {
{{EGameModeNetworkType::DS, EGameModeType::FFA}, "unreal-elimination-ds"},
{{EGameModeNetworkType::DS, EGameModeType::TDM}, "unreal-teamdeathmatch-ds"}
};In the header file, add this function declaration.
public:
virtual void CreateMatchSession(
const int32 LocalUserNum,
const EGameModeNetworkType NetworkType,
const EGameModeType GameModeType) override;Now, open the
MatchSessionDSOnlineSession_Starter
CPP file and add this implementation. Note that we set a flag, theGAME_SESSION_REQUEST_TYPE
, in theSessionSettings
so that theFindSessions
function can tell the backend to only return sessions with that flag. Remove the need to manually filter the response.void UMatchSessionDSOnlineSession_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-DS-USERCREATED" : "TEAMDEATHMATCH-DS-USERCREATED"));
// Check is using AMS
const bool bUseAMS = UTutorialModuleOnlineUtility::GetIsServerUseAMS();
// Get match pool id based on game mode type
FString MatchTemplateName = MatchSessionTemplateNameMap[{EGameModeNetworkType::DS, GameModeType}];
if(bUseAMS)
{
MatchTemplateName.Append("-ams");
}
CreateSession(
LocalUserNum,
GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession),
SessionSettings,
EAccelByteV2SessionType::GameSession,
MatchTemplateName);
}There you have it. Create match session, implemented.
Find match session
Open the
MatchSessionDSOnlineSession_Starter
header file and add these two function declarations, 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
MatchSessionDSOnlineSession_Starter
CPP file and add these implementations. Note that, since we have set a flag in theSessionSettings
in theCreateMatchSession
implementation, we will also set the same flag as theQuerySettings
. Also, note that, we pass a class member variable, theSessionSearch
, when calling theFindSessions
. TheFindSessions
references theFOnlineSessionSearch
variable and updates the variable that the reference refers to. Hence, theOnFindSessionsComplete
doesn't include the session info as the parameter.void UMatchSessionDSOnlineSession_Starter::FindSessions(
const int32 LocalUserNum,
const int32 MaxQueryNum,
const bool bForce)
{
UE_LOG_MATCHSESSIONDS(Verbose, TEXT("called"))
if (SessionSearch->SearchState == EOnlineAsyncTaskState::InProgress)
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("Currently searching"))
return;
}
// check cache
if (!bForce && MaxQueryNum <= SessionSearch->SearchResults.Num())
{
UE_LOG_MATCHSESSIONDS(Log, TEXT("Cache found"))
// return cache
ExecuteNextTick(FSimpleDelegate::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(FSimpleDelegate::CreateWeakLambda(this, [this]()
{
OnFindSessionsComplete(false);
}));
}
}
void UMatchSessionDSOnlineSession_Starter::OnFindSessionsComplete(bool bSucceeded)
{
UE_LOG_MATCHSESSIONDS(Log, TEXT("succeeded: %s"), *FString(bSucceeded ? "TRUE" : "FALSE"))
if (bSucceeded)
{
// remove owned session from result if exist
const FUniqueNetIdPtr LocalUserNetId = GetIdentityInt()->GetUniquePlayerId(LocalUserNumSearching);
SessionSearch->SearchResults.RemoveAll([this, LocalUserNetId](const FOnlineSessionSearchResult& Element)
{
return CompareAccelByteUniqueId(
FUniqueNetIdRepl(LocalUserNetId),
FUniqueNetIdRepl(Element.Session.OwningUserId));
});
// get owners user info for query user info
TArray<FUniqueNetIdRef> UserIds;
for (const FOnlineSessionSearchResult& SearchResult : SessionSearch->SearchResults)
{
UserIds.AddUnique(SearchResult.Session.OwningUserId->AsShared());
}
// trigger Query User info
QueryUserInfo(
LocalUserNumSearching,
UserIds,
FOnQueryUsersInfoComplete::CreateUObject(this, &ThisClass::OnQueryUserInfoForFindSessionComplete));
}
else
{
OnFindSessionsCompleteDelegates.Broadcast({}, false);
}
}Now that the implementation is done, bind the
OnFindSessionsComplete
to the actual delegate. Still in the CPP file, navigate toRegisterOnlineDelegates
and add the highlighted lines in the following code:void UMatchSessionDSOnlineSession_Starter::RegisterOnlineDelegates()
{
...
UGameOverWidget::OnQuitGameDelegate.Add(LeaveSessionDelegate);
GetSessionInt()->OnFindSessionsCompleteDelegates.AddUObject(this, &ThisClass::OnFindSessionsComplete);
}Implement a way to unbind that callback. Still in the CPP file, navigate to
ClearOnlineDelegates
and add the highlighted lines in the following code.void UMatchSessionDSOnlineSession::ClearOnlineDelegates()
{
...
UGameOverWidget::OnQuitGameDelegate.RemoveAll(this);
GetSessionInt()->OnFindSessionsCompleteDelegates.RemoveAll(this);
}Voilà! Game client Online Session implementation is done.
Set up dedicated server online subsystem
We have created a Game Instance Subsystem class called UMatchSessionDSServerSubsystem_Starter
to handle the Match Session server implementation. This Online Session class provides necessary declarations and definitions to help you begin implementing right away.
You can find the UMatchSessionDSServerSubsystem_Starter
class files at the following:
- Header file is in
/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/MatchSessionDSServerSubsystem_Starter.h
. - CPP file is in
/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/MatchSessionDSServerSubsystem_Starter.cpp
.
Let's begin by taking a look at the provided class.
In the
UMatchSessionDSServerSubsystem_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 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 kick that player. For this tutorial, ignore this section.protected:
virtual void OnAuthenticatePlayerComplete_PrePlayerSetup(APlayerController- PlayerController) override;There's also a variable called
OnlineSession
in the header file, which is a pointer to the Online Session that you have implemented earlier.private:
UPROPERTY()
UMatchSessionDSOnlineSession_Starter- OnlineSession;
Server received session
Right after the server register itself to backend, the backend will assign that DS to said session if a session requires a DS. When this happens, the DS will receive a notification. In Byte Wars, we used a flag in the SessionSettings
to determine which game mode the server should be using.
Open the
UMatchSessionDSServerSubsystem_Starter
header file and add this function declaration:protected:
virtual void OnServerSessionReceived(FName SessionName) override;Now, to the implementation. Most of the code here is specific for Byte Wars. We have highlighted the lines that you might need in your game, which is retrieving the session data itself and an example on how to retrieve your custom
SessionSettings
that you have set when creating the session.void UMatchSessionDSServerSubsystem_Starter::OnServerSessionReceived(FName SessionName)
{
Super::OnServerSessionReceived(SessionName);
UE_LOG_MATCHSESSIONDS(Verbose, TEXT("called"))
#pragma region "Assign game mode based on SessionTemplateName from backend"
// Get GameMode
const UWorld- World = GetWorld();
if (!World)
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("World is invalid"));
return;
}
AGameStateBase- GameState = World->GetGameState();
if (!GameState)
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("Game State is invalid"));
return;
}
AAccelByteWarsGameState- AbGameState = Cast<AAccelByteWarsGameState>(GameState);
if (!AbGameState)
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("Game State is not derived from AAccelByteWarsGameState"));
return;
}
// Get Game Session
if (OnlineSession->GetSessionType(SessionName) != EAccelByteV2SessionType::GameSession)
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("Is not a game session"));
return;
}
const FNamedOnlineSession- Session = OnlineSession->GetSession(SessionName);
if (!Session)
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("Session is invalid"));
return;
}
FString RequestedGameModeCode;
Session->SessionSettings.Get(GAMESETUP_GameModeCode, RequestedGameModeCode);
if (!RequestedGameModeCode.IsEmpty())
{
AbGameState->AssignGameMode(RequestedGameModeCode);
}
#pragma endregion
// Query all currently registered user's info
AuthenticatePlayer_OnRefreshSessionComplete(true);
}Bind that function to the Online Subsystem (OSS) delegate. Still in the CPP file, navigate to
Initialize
and add the highlighted line in the following code:void UMatchSessionDSServerSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
...
OnlineSession = Cast<UMatchSessionDSOnlineSession>(BaseOnlineSession);
GetABSessionInt()->OnServerReceivedSessionDelegates.AddUObject(this, &ThisClass::OnServerSessionReceived);
}We also need to unbind that function when no longer needed. Navigate to
Deinitialize
and add the highlighted line in the following code:void UMatchSessionDSServerSubsystem_Starter::Deinitialize()
{
Super::Deinitialize();
GetABSessionInt()->OnServerReceivedSessionDelegates.RemoveAll(this);
}There you have it! All done.
Resources
- The files used in this tutorial section are available in the Byte Wars GitHub repository.
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/MatchSessionDSOnlineSession_Starter.h
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/MatchSessionDSOnlineSession_Starter.cpp
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/MatchSessionDSServerSubsystem_Starter.h
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/MatchSessionDSServerSubsystem_Starter.cpp