サブシステムを実装する - 専用サーバーを使用した参加可能なセッション - (Unreal Engine モジュール)
Implementation for match sessions is done through two different classes: the Online Session and the 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, understand the browse session flow:
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 locations:
- Header file:
/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/MatchSessionDSOnlineSession_Starter.h
- CPP** file:
/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/MatchSessionDSOnlineSession_Starter.cpp
Take a look at what has been provided in the class. Keep in mind, you still have access to all the functions that you have access to in the Introduction to Session module 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 the backend on the server and to show the username of the session's owner.public:
// ...
virtual void DSQueryUserInfo(
const TArray<FUniqueNetIdRef>& UserIds,
const FOnDSQueryUsersInfoComplete& OnComplete) override;protected:
// ...
virtual void OnDSQueryUserInfoComplete(
const FListBulkUserInfo& UserInfoList,
const FOnDSQueryUsersInfoComplete& OnComplete) override;private:
// ...
FDelegateHandle OnDSQueryUserInfoCompleteDelegateHandle;
// ...
void OnQueryUserInfoForFindSessionComplete(
const ::FOnlineError& Error,
const TArray<TSharedPtr<FUserOnlineAccountAccelByte>>& UsersInfo);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
. Set the value of this to the session template's name that you set up previously.public:
// ...
const TMap<TPair<EGameModeNetworkType, EGameModeType>, FString> MatchSessionTemplateNameMap = {
{{EGameModeNetworkType::DS, EGameModeType::FFA}, "unreal-elimination-ds-ams"},
{{EGameModeNetworkType::DS, EGameModeType::TDM}, "unreal-teamdeathmatch-ds-ams"}
};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 send the server's IP address to the game client. The game client needs a way to travel to the given IP address.
Declare the functions. Open the
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 implementations below. 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 receives 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("The session is invalid"));
return false;
}
const TSharedPtr<FOnlineSessionInfo> SessionInfo = Session->SessionInfo;
if (!SessionInfo.IsValid())
{
UE_LOG_MATCHSESSIONDS(Warning, TEXT("The session info is invalid"));
return false;
}
const TSharedPtr<FOnlineSessionInfoAccelByteV2> AbSessionInfo = StaticCastSharedPtr<FOnlineSessionInfoAccelByteV2>(SessionInfo);
if (!AbSessionInfo.IsValid())
{
UE_LOG_MATCHSESSIONDS(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_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("The 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. 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 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);
}You need to handle what happens if the client disconnects from the server but is still connected to the session service. In this case, the player will still be treated as a part of the session. Call
LeaveSession
whenever this disconnect happens. You will use a function provided by Unreal Engine's Online Session class, theHandleDisconnectInternal
. Go back to theMatchSessionDSOnlineSession_Starter
Header file and add this declaration:protected:
// ...
virtual bool HandleDisconnectInternal(UWorld* World, UNetDriver* NetDriver) override;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;
}
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 previously. 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-ams"},
{{EGameModeNetworkType::DS, EGameModeType::TDM}, "unreal-teamdeathmatch-ds-ams"}
};In the Header file, add this function declaration:
public:
// ...
virtual void CreateMatchSession(
const int32 LocalUserNum,
const EGameModeNetworkType NetworkType,
const EGameModeType GameModeType) override;Open the
MatchSessionDSOnlineSession_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. 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"));
// Get match session template name based on game mode type
FString MatchTemplateName = MatchSessionTemplateNameMap[{EGameModeNetworkType::DS, GameModeType}];
// AMS is the default multiplayer server on AGS. If the game runs on legacy AGS Armada, remove the -ams suffix.
if(!UTutorialModuleOnlineUtility::GetIsServerUseAMS())
{
MatchTemplateName = MatchTemplateName.Replace(TEXT("-ams"), TEXT(""));
}
// Override match session template name if applicable.
if (!UTutorialModuleOnlineUtility::GetMatchSessionTemplateDSOverride().IsEmpty())
{
MatchTemplateName = UTutorialModuleOnlineUtility::GetMatchSessionTemplateDSOverride();
}
CreateSession(
LocalUserNum,
GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession),
SessionSettings,
EAccelByteV2SessionType::GameSession,
MatchTemplateName);
}
Find match session
Open the
MatchSessionDSOnlineSession_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
MatchSessionDSOnlineSession_Starter
CPP file and add the implementations below. Note that, since we have set a flag inSessionSettings
in theCreateMatchSession
implementation, we will also set the same flag as theQuerySettings
. Also note that we pass a class member variable, theSessionSearch
, when callingFindSessions
.FindSessions
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(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 UMatchSessionDSOnlineSession_Starter::OnFindSessionsComplete(bool bSucceeded)
{
UE_LOG_MATCHSESSIONDS(Log, TEXT("succeeded: %s"), *FString(bSucceeded ? TEXT("TRUE") : TEXT("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<UStartupSubsystem>())
{
StartupSubsystem->QueryUserInfo(
LocalUserNumSearching,
UserIds,
FOnQueryUsersInfoCompleteDelegate::CreateUObject(this, &ThisClass::OnQueryUserInfoForFindSessionComplete));
return;
}
}
OnFindSessionsCompleteDelegates.Broadcast({}, false);
}Bind the
OnFindSessionsComplete
to the delegate. Still in the CPP file, navigate toRegisterOnlineDelegates
and add the following code:void UMatchSessionDSOnlineSession_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 UMatchSessionDSOnlineSession_Starter::ClearOnlineDelegates()
{
// ...
GetSessionInt()->OnFindSessionsCompleteDelegates.RemoveAll(this);
}Your Game client Online Session implementation is done.
Set up dedicated server online subsystem
A Game Instance Subsystem class called UMatchSessionDSServerSubsystem_Starter
has been created 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 locations:
- Header file:
/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/MatchSessionDSServerSubsystem_Starter.h
- CPP file:
/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/MatchSessionDSServerSubsystem_Starter.cpp
Take 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, it kicks 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 registers itself to the backend, the backend will assign that dedicated server to the session if a session requires a dedicated server. When this happens, the dedicated server 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;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("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 UMatchSessionDSServerSubsystem_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 UMatchSessionDSServerSubsystem_Starter::Deinitialize()
{
// ...
GetABSessionInt()->OnServerReceivedSessionDelegates.RemoveAll(this);
}
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