Skip to main content

Implement subsystem - Joinable sessions with peer-to-peer - (Unreal Engine module)

Last updated on March 9, 2024

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 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 a player's info from the backend on the server and to show the session owner's username.

    public:
    virtual void QueryUserInfo(
    const int32 LocalUserNum,
    const TArray<FUniqueNetIdRef>& UserIds,
    const FOnQueryUsersInfoComplete& OnComplete) override;

    protected:
    virtual void OnQueryUserInfoComplete(
    int32 LocalUserNum,
    bool bSucceeded,
    const TArray<FUniqueNetIdRef>& UserIds,
    const FString& ErrorMessage,
    const FOnQueryUsersInfoComplete& OnComplete) override;

    private:
    void OnQueryUserInfoForFindSessionComplete(
    const bool bSucceeded,
    const TArray<FUserOnlineAccountAccelByte*>& UsersInfo);

    FDelegateHandle OnQueryUserInfoCompleteDelegateHandle;
  • 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 called MatchSessionTemplateNameMap. 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}, "<Your Elimination Session Template Name>"},
    {{EGameModeNetworkType::P2P, EGameModeType::TDM}, "<Your Team Deathmatch Session Template Name>"}
    };
  • 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.

  1. 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;
  2. Open the MatchSessionP2POnlineSession_Starter CPP file and add the implementations below. The TravelToSession function, as the name suggests, 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 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 on OnCreateSessionComplete.

    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("Session is invalid"));
    return false;
    }

    const TSharedPtr<FOnlineSessionInfo> SessionInfo = Session->SessionInfo;
    if (!SessionInfo.IsValid())
    {
    UE_LOG_MATCHSESSIONP2P(Warning, TEXT("Session Info is invalid"));
    return false;
    }

    const TSharedPtr<FOnlineSessionInfoAccelByteV2> AbSessionInfo = StaticCastSharedPtr<FOnlineSessionInfoAccelByteV2>(SessionInfo);
    if (!AbSessionInfo.IsValid())
    {
    UE_LOG_MATCHSESSIONP2P(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_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("Is not P2P host, travelling 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("Is P2P host, travelling 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. 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 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);
    }
    }
  3. 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 the MatchSessionP2POnlineSession_Starter Header file and add this declaration:

    protected:
    virtual bool HandleDisconnectInternal(UWorld* World, UNetDriver* NetDriver) override;
  4. 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;
    }
  5. 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 the LeaveSession response callback to reset the bIsInSessionServer variable. Open the MatchSessionP2POnlineSession_Starter Header file and add this declaration:

    protected:
    virtual void OnLeaveSessionComplete(FName SessionName, bool bSucceeded) override;
  6. 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

  1. Open the MatchSessionP2POnlineSession_Starter Header file and make sure you set the MatchSessionTemplateNameMap map value to the session template name that you set up previously.

    public:
    const TMap<TPair<EGameModeNetworkType, EGameModeType>, FString> MatchSessionTemplateNameMap = {
    {{EGameModeNetworkType::P2P, EGameModeType::FFA}, "<Your Elimination Session Template Name>"},
    {{EGameModeNetworkType::P2P, EGameModeType::TDM}, "<Your Team Deathmatch Session Template Name>"}
    };
  2. Still 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 MatchSessionP2POnlineSession_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, 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"));

    CreateSession(
    LocalUserNum,
    GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession),
    SessionSettings,
    EAccelByteV2SessionType::GameSession,
    MatchTemplateName);
    }

Find match session

  1. 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;
  2. Open the MatchSessionP2POnlineSession_Starter CPP file and add the implementations below. Note that, since you set a flag in the SessionSettings in the CreateMatchSession implementation, you will also set the same flag as the QuerySettings. Also note that you pass a class member variable, the SessionSearch, when calling FindSessions. The way the FindSessions works is it takes a reference of FOnlineSessionSearch variable and updates the variable that the reference refers to, hence, the OnFindSessionsComplete 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(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 UMatchSessionP2POnlineSession_Starter::OnFindSessionsComplete(bool bSucceeded)
    {
    UE_LOG_MATCHSESSIONP2P(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);
    }
    }
  3. Now that the implementation is done, bind the OnFindSessionsComplete to the delegate. Still in the CPP file, navigate to RegisterOnlineDelegates and add the highlighted lines in the following code:

    void UMatchSessionP2POnlineSession_Starter::RegisterOnlineDelegates()
    {
    ...
    UGameOverWidget::OnQuitGameDelegate.Add(LeaveSessionDelegate);

    GetSessionInt()->OnFindSessionsCompleteDelegates.AddUObject(this, &ThisClass::OnFindSessionsComplete);
    }
  4. 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 UMatchSessionP2POnlineSession_Starter::ClearOnlineDelegates()
    {
    ...
    UGameOverWidget::OnQuitGameDelegate.RemoveAll(this);

    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 labelled Game specific. Just like the QueryUserInfo 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()
    UMatchmakingP2POnlineSession_Starter* MatchmakingOnlineSession;

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.

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

    protected:
    virtual void OnServerSessionReceived(FName SessionName) override;
  2. 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_MATCHMAKINGP2P(Verbose, TEXT("called"))
    #pragma region "Assign game mode based on SessionTemplateName from backend"

    // Get GameMode
    const UWorld* World = GetWorld();
    if (!World)
    {
    UE_LOG_MATCHMAKINGP2P(Warning, TEXT("World is invalid"));
    return;
    }

    AGameStateBase* GameState = World->GetGameState();
    if (!GameState)
    {
    UE_LOG_MATCHMAKINGP2P(Warning, TEXT("Game State is invalid"));
    return;
    }

    AAccelByteWarsGameState* AbGameState = Cast<AAccelByteWarsGameState>(GameState);
    if (!AbGameState)
    {
    UE_LOG_MATCHMAKINGP2P(Warning, TEXT("Game State is not derived from AAccelByteWarsGameState"));
    return;
    }

    // Get Game Session
    if (MatchmakingOnlineSession->GetSessionType(SessionName) != EAccelByteV2SessionType::GameSession)
    {
    UE_LOG_MATCHMAKINGP2P(Warning, TEXT("Is not a game session"));
    return;
    }

    const FNamedOnlineSession* Session = MatchmakingOnlineSession->GetSession(SessionName);
    if (!Session)
    {
    UE_LOG_MATCHMAKINGP2P(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);
    }
  3. 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 UMatchSessionP2PServerSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    ...
    MatchmakingOnlineSession = Cast<UMatchmakingP2POnlineSession>(BaseOnlineSession);

    GetABSessionInt()->OnServerReceivedSessionDelegates.AddUObject(this, &ThisClass::OnServerSessionReceived);
    }
  4. Unbind that function when it's no longer needed. Navigate to Deinitialize and add the highlighted line in the following code:

    void UMatchSessionP2PServerSubsystem_Starter::Deinitialize()
    {
    Super::Deinitialize();

    GetABSessionInt()->OnServerReceivedSessionDelegates.RemoveAll(this);
    }

Resources