Skip to main content

Implement subsystem - Joinable sessions with dedicated servers - (Unreal Engine module)

Last updated on October 24, 2024

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 with QueryUserInfo 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 called MatchSessionTemplateNameMap. 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.

  1. 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;
  2. Open the MatchSessionDSOnlineSession_Starter CPP file and add the implementations below. The TravelToSession function will retrieve the server's IP address from the cached session info and attempt to travel to that server. The OnSessionServerUpdateReceived and OnSessionServerErrorReceived 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);
    }
  3. 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, the HandleDisconnectInternal. Go back to the MatchSessionDSOnlineSession_Starter Header file and add this declaration:

    protected:
    // ...
    virtual bool HandleDisconnectInternal(UWorld* World, UNetDriver* NetDriver) override;
  4. 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

  1. Open the MatchSessionDSOnlineSession_Starter Header file and make sure you set the MatchSessionTemplateNameMap 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"}
    };
  2. In the Header file, add this function declaration:

    public:
    // ...
    virtual void CreateMatchSession(
    const int32 LocalUserNum,
    const EGameModeNetworkType NetworkType,
    const EGameModeType GameModeType) override;
  3. Open the MatchSessionDSOnlineSession_Starter CPP file and add the implementation below. Note that you set a flag, the GAME_SESSION_REQUEST_TYPE, in the SessionSettings so that the FindSessions 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

  1. 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;
  2. Open the MatchSessionDSOnlineSession_Starter CPP file and add the implementations below. Note that, since we have set a flag in SessionSettings in the CreateMatchSession implementation, we will also set the same flag as the QuerySettings. Also note that we pass a class member variable, the SessionSearch, when calling FindSessions. FindSessions references the FOnlineSessionSearch variable and updates the variable that the reference refers to, hence, the OnFindSessionsComplete 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);
    }
  3. Bind the OnFindSessionsComplete to the delegate. Still in the CPP file, navigate to RegisterOnlineDelegates and add the following code:

    void UMatchSessionDSOnlineSession_Starter::RegisterOnlineDelegates()
    {
    // ...
    // Match session delegates
    GetSessionInt()->OnFindSessionsCompleteDelegates.AddUObject(this, &ThisClass::OnFindSessionsComplete);

    SessionSearch->SearchState = EOnlineAsyncTaskState::NotStarted;
    }
  4. 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 the QueryUserInfo 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.

  1. Open the UMatchSessionDSServerSubsystem_Starter Header file and add this function declaration:

    protected:
    // ...
    virtual void OnServerSessionReceived(FName SessionName) override;
  2. 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);
    }
  3. 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);
    }
  4. 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