メインコンテンツまでスキップ

Unreal Engine Module - Quick Match with DS - Use the online subsystem to use matchmaking with dedicated server

Last updated on January 13, 2024
AGS Starter

This tutorial isn't yet applicable for the AccelByte Gaming Service (AGS) Starter tier. It requires the Armada, a dynamic game server manager feature, which isn't currently supported on AGS Starter.

Matchmaking with dedicated server flow

Before we begin, let's understand how matchmaking with dedicated server (DS) works. Take a look at the following diagram.

Setup game client online session

Matchmaking is essentially finding a match for a game session. Once the game session is found, the player will join the game session and travel to the game server. You should already have the basic knowledge of how game session works, as you have learned from the Module: Introduction to Multiplayer Session.

In this tutorial, you will be working with an online session class namely MatchmakingDSOnlineSession_Starter. This online session is used by the game client to perform matchmaking. This class is defined in the following files.

  • Header file can be found in /Source/AccelByteWars/TutorialModules/Play/MatchmakingDS/MatchmakingDSOnlineSession_Starter.h.
  • CPP file can be found in /Source/AccelByteWars/TutorialModules/Play/MatchmakingDS/MatchmakingDSOnlineSession_Starter.cpp.

This class has several pre-defined attributes and functions you can use to follow the tutorial. Let's take a look at what it has provided.

  • Similar to Module: Introduction to Multiplayer Session, this class uses session interfaces to perform session-related functions, including matchmaking.

    IOnlineSessionPtr UOnlineSessionClient::GetSessionInt()
    {
    UWorld* World = GetWorld();
    if (World == nullptr)
    {
    UE_LOG_ONLINE(Warning, TEXT("UOnlineSessionClient::GetSessionInt: Called with NULL world."));
    return nullptr;
    }

    return Online::GetSessionInterface(World);
    }

    FOnlineSessionV2AccelBytePtr UAccelByteWarsOnlineSessionBase::GetABSessionInt()
    {
    return StaticCastSharedPtr<FOnlineSessionV2AccelByte>(GetSessionInt());
    }
  • The header file has the following map that defines the Match Pool IDs that you have created from the previous section. As you can see, the IDs are the exact same as the ones you set up on Admin Portal. The name must be the same, so you can request matchmaking to the correct Match Pool.

    const TMap<EGameModeType, FString> MatchPoolIds = {
    {EGameModeType::FFA, "unreal-elimination-ds"},
    {EGameModeType::TDM, "unreal-teamdeathmatch-ds"}
    };
  • The header file also has the following map that translates Match Pool IDs to game mode supported by the game. The game server uses this map to configure the gameplay based on the game mode.

    const TMap<FString, FString> TargetGameModeMap = {
    {"unreal-elimination-ds", "ELIMINATION-DS"},
    {"unreal-teamdeathmatch-ds", "TEAMDEATHMATCH-DS"}
    };
  • In the CPP file, there are several functions to query players information. You will not use these functions directly in this tutorial. These functions are called on the game server side, particularly in the UAccelByteWarsServerSubsystemBase::AuthenticatePlayer_OnRefreshSessionComplete(). The queried information is saved in the game state which then is used by the Match Lobby widget to display the player information.

    void UMatchmakingDSOnlineSession_Starter::QueryUserInfo(
    const int32 LocalUserNum,
    const TArray<FUniqueNetIdRef>& UserIds,
    const FOnQueryUsersInfoComplete& OnComplete)
    {
    UE_LOG_MATCHMAKINGDS(Verbose, TEXT("called"))

    // safety
    if (!GetUserInt())
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("User interface null"))
    ExecuteNextTick(FSimpleDelegate::CreateWeakLambda(this, [this, OnComplete]()
    {
    OnComplete.ExecuteIfBound(false, {});
    }));
    return;
    }

    TArray<FUserOnlineAccountAccelByte*> UserInfo;
    if (RetrieveUserInfoCache(UserIds, UserInfo))
    {
    UE_LOG_MATCHMAKINGDS(Log, TEXT("Cache found"))
    ExecuteNextTick(FSimpleDelegate::CreateWeakLambda(this, [this, UserInfo, OnComplete]()
    {
    OnComplete.ExecuteIfBound(true, UserInfo);
    }));
    }
    // Some data does not exist in cache, query everything
    else
    {
    // Bind delegate
    if (OnQueryUserInfoCompleteDelegateHandle.IsValid())
    {
    GetUserInt()->OnQueryUserInfoCompleteDelegates->Remove(OnQueryUserInfoCompleteDelegateHandle);
    OnQueryUserInfoCompleteDelegateHandle.Reset();
    }
    OnQueryUserInfoCompleteDelegateHandle = GetUserInt()->OnQueryUserInfoCompleteDelegates->AddWeakLambda(
    this, [OnComplete, this](
    int32 LocalUserNum,
    bool bSucceeded,
    const TArray<FUniqueNetIdRef>& UserIds,
    const FString& ErrorMessage)
    {
    OnQueryUserInfoComplete(LocalUserNum, bSucceeded, UserIds, ErrorMessage, OnComplete);
    });

    if (!GetUserInt()->QueryUserInfo(LocalUserNum, UserIds))
    {
    OnQueryUserInfoComplete(LocalUserNum, false, UserIds, "", OnComplete);
    }
    }
    }

    void UMatchmakingDSOnlineSession_Starter::DSQueryUserInfo(
    const TArray<FUniqueNetIdRef>& UserIds,
    const FOnDSQueryUsersInfoComplete& OnComplete)
    {
    UE_LOG_MATCHMAKINGDS(Verbose, TEXT("called"))

    const TArray<const FBaseUserInfo*> UserInfo;
    if (DSRetrieveUserInfoCache(UserIds, UserInfo))
    {
    UE_LOG_MATCHMAKINGDS(Log, TEXT("Cache found"))
    ExecuteNextTick(FSimpleDelegate::CreateWeakLambda(this, [this, OnComplete, UserInfo]()
    {
    OnComplete.ExecuteIfBound(true, UserInfo);
    }));
    }
    else
    {
    // gather user ids
    TArray<FString> AbUserIds;
    for (const FUniqueNetIdRef& UserId : UserIds)
    {
    const FUniqueNetIdAccelByteUserPtr AbUniqueNetId = FUniqueNetIdAccelByteUser::TryCast(*UserId);
    const FString AbUserId = AbUniqueNetId->GetAccelByteId();
    if (!AbUserId.IsEmpty())
    {
    AbUserIds.Add(AbUserId);
    }
    }

    AccelByte::FRegistry::User.BulkGetUserInfo(
    AbUserIds,
    THandler<FListBulkUserInfo>::CreateWeakLambda(this, [OnComplete, this](FListBulkUserInfo UserInfo)
    {
    OnDSQueryUserInfoComplete(UserInfo, OnComplete);
    }),
    FErrorHandler::CreateWeakLambda(this, [this, OnComplete](int32, const FString&)
    {
    ExecuteNextTick(FSimpleDelegate::CreateWeakLambda(this, [this, OnComplete]()
    {
    OnDSQueryUserInfoComplete(FListBulkUserInfo(), OnComplete);
    }));
    })
    );
    }
    }

Start matchmaking

In this section, you will implement functions to start matchmaking.

  1. First, open the MatchmakingDSOnlineSession_Starter header file and then declare a function to start matchmaking.

    public:
    virtual void StartMatchmaking(
    const APlayerController* PC,
    const FName& SessionName,
    const EGameModeNetworkType NetworkType,
    const EGameModeType GameModeType) override;
  2. Still in the same file, declare a callback function that will be called when the start matchmaking process is completed.

    protected:
    virtual void OnStartMatchmakingComplete(
    FName SessionName,
    const FOnlineError& ErrorDetails,
    const FSessionMatchmakingResults& Results) override;
  3. Next, declare a delegate that will be called when the start matchmaking process is completed. You can use this delegate to bind some events later, especially when connecting UIs with the matchmaking implementation.

    public:
    virtual FOnMatchmakingResponse* GetOnStartMatchmakingCompleteDelegates() override
    {
    return &OnStartMatchmakingCompleteDelegates;
    }

    private:
    FOnMatchmakingResponse OnStartMatchmakingCompleteDelegates;
  4. Then, to start matchmaking, you will need to leave any left-over game sessions. Thus, declare the following function to handle that.

    private:
    void OnLeaveSessionForReMatchmakingComplete(
    FName SessionName,
    bool bSucceeded,
    const int32 LocalUserNum,
    const EGameModeType GameModeType);

    FDelegateHandle OnLeaveSessionForReMatchmakingCompleteDelegateHandle;
  5. All right, let's define the functions you just declared above. Open the MatchmakingDSOnlineSession_Starter CPP file and let's begin with the StartMatchmaking() function. In this function, we construct the matchmaking handle to define what kind of matchmaking we want. We set the Match Pool to the matchmaking handle, so it will start the matchmaking based on the Match Pool we have configured in Admin Portal. Also, before it begins to match make, we first check whether the player is already in the game session or not. If yes, leaves the game session first and then retries matchmaking, which will be handled by the OnLeaveSessionForReMatchmakingComplete() function. When the matchmaking is successfully started, it then calls the OnStartMatchmakingComplete() function.

    void UMatchmakingDSOnlineSession_Starter::StartMatchmaking(
    const APlayerController* PC,
    const FName& SessionName,
    const EGameModeNetworkType NetworkType,
    const EGameModeType GameModeType)
    {
    UE_LOG_MATCHMAKINGDS(Verbose, TEXT("called"))

    // safety
    if (!ensure(GetSessionInt()))
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Session Interface is not valid."));
    ExecuteNextTick(FSimpleDelegate::CreateWeakLambda(this, [this, SessionName]()
    {
    OnStartMatchmakingComplete(SessionName, FOnlineError(false), {});
    }));
    return;
    }

    // If the player is already in a session, then leave the session first.
    if (GetSession(SessionName))
    {
    UE_LOG_MATCHMAKINGDS(Log, TEXT("Already in session. Leaving session first."))
    if (OnLeaveSessionForReMatchmakingCompleteDelegateHandle.IsValid())
    {
    GetOnLeaveSessionCompleteDelegates()->Remove(OnLeaveSessionForReMatchmakingCompleteDelegateHandle);
    OnLeaveSessionForReMatchmakingCompleteDelegateHandle.Reset();
    }

    OnLeaveSessionForReMatchmakingCompleteDelegateHandle = GetOnLeaveSessionCompleteDelegates()->AddUObject(
    this,
    &ThisClass::OnLeaveSessionForReMatchmakingComplete,
    GetLocalUserNumFromPlayerController(PC),
    GameModeType);
    LeaveSession(SessionName);
    return;
    }

    const FUniqueNetIdPtr PlayerNetId = GetLocalPlayerUniqueNetId(PC);
    if (!ensure(PlayerNetId.IsValid()))
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Player UniqueNetId is not valid."));
    ExecuteNextTick(FSimpleDelegate::CreateWeakLambda(this, [this, SessionName]()
    {
    OnStartMatchmakingComplete(SessionName, FOnlineError(false), {});
    }));
    return;
    }

    // Check is using AMS
    const bool bUseAMS = UTutorialModuleOnlineUtility::GetIsServerUseAMS();

    // Get match pool id based on game mode type
    FString MatchPoolId = MatchPoolIds[GameModeType];
    if(bUseAMS)
    {
    MatchPoolId.Append("-ams");
    }

    // Setup matchmaking search handle, it will be used to store session search results.
    TSharedRef<FOnlineSessionSearch> MatchmakingSearchHandle = MakeShared<FOnlineSessionSearch>();
    MatchmakingSearchHandle->QuerySettings.Set(
    SETTING_SESSION_MATCHPOOL, MatchPoolId, EOnlineComparisonOp::Equals);

    // Check for DS version override.
    const FString OverriddenDSVersion = UTutorialModuleOnlineUtility::GetDedicatedServerVersionOverride();
    if (!OverriddenDSVersion.IsEmpty())
    {
    MatchmakingSearchHandle->QuerySettings.Set(SETTING_GAMESESSION_CLIENTVERSION, OverriddenDSVersion, EOnlineComparisonOp::Equals);
    }

    // Set local server name for matchmaking request if any.
    // This is useful if you want to try matchmaking using local dedicated server.
    FString ServerName;
    FParse::Value(FCommandLine::Get(), TEXT("-ServerName="), ServerName);
    if (!ServerName.IsEmpty())
    {
    UE_LOG_MATCHMAKINGDS(Log, TEXT("Requesting local server with name: %s"), *ServerName)
    MatchmakingSearchHandle->QuerySettings.Set(SETTING_GAMESESSION_SERVERNAME, ServerName, EOnlineComparisonOp::Equals);
    }

    if (!GetSessionInt()->StartMatchmaking(
    USER_ID_TO_MATCHMAKING_USER_ARRAY(PlayerNetId.ToSharedRef()),
    SessionName,
    FOnlineSessionSettings(),
    MatchmakingSearchHandle,
    FOnStartMatchmakingComplete::CreateUObject(this, &ThisClass::OnStartMatchmakingComplete)))
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Failed executing"))
    // Failed to start matchmaking.
    ExecuteNextTick(FSimpleDelegate::CreateWeakLambda(this, [this, SessionName]()
    {
    OnStartMatchmakingComplete(SessionName, FOnlineError(false), {});
    }));
    }
    }
  6. Next, let's define the OnStartMatchmakingComplete() function. This function does not do anything special, it just triggers the delegate you just created to inform that the matchmaking is started, whether it was successful or not.

    void UMatchmakingDSOnlineSession_Starter::OnStartMatchmakingComplete(
    FName SessionName,
    const FOnlineError& ErrorDetails,
    const FSessionMatchmakingResults& Results)
    {
    UE_LOG_MATCHMAKINGDS(
    Log,
    TEXT("succeeded: %s | error: (%s) %s"),
    *FString(ErrorDetails.bSucceeded ? "TRUE": "FALSE"),
    *ErrorDetails.ErrorCode, *ErrorDetails.ErrorMessage.ToString())

    OnStartMatchmakingCompleteDelegates.Broadcast(SessionName, ErrorDetails.bSucceeded);
    }
  7. Then, define the OnLeaveSessionForReMatchmakingComplete() function. This function will be called after leaving the game session. It then checks whether it is valid to retry to start matchmaking or not.

    void UMatchmakingDSOnlineSession_Starter::OnLeaveSessionForReMatchmakingComplete(
    FName SessionName,
    bool bSucceeded,
    const int32 LocalUserNum,
    const EGameModeType GameModeType)
    {
    UE_LOG_MATCHMAKINGDS(Verbose, TEXT("called"))

    GetOnLeaveSessionCompleteDelegates()->Remove(OnLeaveSessionForReMatchmakingCompleteDelegateHandle);

    if (bSucceeded)
    {
    // Retry matchmaking.
    const APlayerController* PC = GetPlayerControllerByLocalUserNum(LocalUserNum);
    if (!ensure(PC))
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("PlayerController is null."));
    OnStartMatchmakingComplete(SessionName, FOnlineError(false), {});
    return;
    }

    StartMatchmaking(PC, SessionName, EGameModeNetworkType::DS, GameModeType);
    }
    else
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Is not a game session."));
    OnStartMatchmakingComplete(SessionName, FOnlineError(false), {});
    }
    }
  8. Now, you need to handle when the matchmaking is completed. Open the MatchmakingDSOnlineSession_Starter header file and then declare the following function.

    protected:
    virtual void OnMatchmakingComplete(FName SessionName, bool bSucceeded) override;
  9. Next, declare a delegate that will be called when the matchmaking is completed. You can use this delegate to bind some events later, especially when connecting UIs with the matchmaking implementation.

    public:
    virtual FOnMatchmakingResponse* GetOnMatchmakingCompleteDelegates() override
    {
    return &OnMatchmakingCompleteDelegates;
    }

    private:
    FOnMatchmakingResponse OnMatchmakingCompleteDelegates;
  10. Now, open the MatchmakingDSOnlineSession_Starter CPP file and then define the OnMatchmakingComplete() function. This function will check whether a game session was found after the matchmaking is completed. If found, it then joins the game session. It also invokes the delegate you created earlier to inform the game that the matchmaking process is completed.

    void UMatchmakingDSOnlineSession_Starter::OnMatchmakingComplete(FName SessionName, bool bSucceeded)
    {
    UE_LOG_MATCHMAKINGDS(Log, TEXT("succeeded: %s"), *FString(bSucceeded ? "TRUE": "FALSE"))

    const TSharedPtr<FOnlineSessionSearchAccelByte> CurrentMatchmakingSearchHandle = GetABSessionInt()->CurrentMatchmakingSearchHandle;
    if (!bSucceeded ||
    !ensure(CurrentMatchmakingSearchHandle.IsValid()) /*This might happen when the MM finished just as we are about to cancel it*/ ||
    !ensure(CurrentMatchmakingSearchHandle->SearchResults.IsValidIndex(0)) ||
    !ensure(CurrentMatchmakingSearchHandle->SearchingPlayerId.IsValid()))
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("There is no match result returned."));
    OnMatchmakingCompleteDelegates.Broadcast(SessionName, false);
    return;
    }

    OnMatchmakingCompleteDelegates.Broadcast(SessionName, bSucceeded);

    // Get searching player
    const int32 LocalUserNum =
    GetLocalUserNumFromPlayerController(GetPlayerControllerByUniqueNetId(CurrentMatchmakingSearchHandle->SearchingPlayerId));

    // Join the first session from matchmaking result.
    JoinSession(
    LocalUserNum,
    GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession),
    CurrentMatchmakingSearchHandle->SearchResults[0]);
    }
  11. Okay, now you need to bind the function above, so it will be called when the matchmaking process is completed. You can do this by adding the following code to the RegisterOnlineDelegates() function, which is the first function to be called when the online session is initialized.

    void UMatchmakingDSOnlineSession_Starter::RegisterOnlineDelegates()
    {
    Super::RegisterOnlineDelegates();

    // Bind matchmaking delegates.
    GetSessionInt()->OnMatchmakingCompleteDelegates.AddUObject(this, &ThisClass::OnMatchmakingComplete);
    }
  12. Finally, when the online session is deinitialized, you need to unbind to stop listening to the event. You can do this by adding the following code in the pre-defined ClearOnlineDelegates() function, which is the first function to be called when the online session is deinitialized.

    void UMatchmakingDSOnlineSession_Starter::ClearOnlineDelegates()
    {
    Super::ClearOnlineDelegates();

    // Unbind matchmaking delegates.
    GetSessionInt()->OnMatchmakingCompleteDelegates.RemoveAll(this);
    }
  13. Congratulations! You have created functions to start matchmaking and handle when the matchmaking is completed.

Cancel matchmaking

In this section, you will implement functions to cancel matchmaking.

  1. First, open the MatchmakingDSOnlineSession_Starter header file and then declare a function to cancel matchmaking.

    public:
    virtual void CancelMatchmaking(APlayerController* PC, const FName& SessionName) override;
  2. Still in the same file, declare a callback function that will be called when the cancel matchmaking process is completed.

    protected:
    virtual void OnCancelMatchmakingComplete(FName SessionName, bool bSucceeded) override;
  3. Next, declare a delegate that will be called when the cancel matchmaking process is completed. You can use this delegate to bind some events later, especially when connecting UIs with the matchmaking implementation.

    public:
    virtual FOnMatchmakingResponse* GetOnCancelMatchmakingCompleteDelegates() override
    {
    return &OnCancelMatchmakingCompleteDelegates;
    }

    private:
    FOnMatchmakingResponse OnCancelMatchmakingCompleteDelegates;
  4. Now, open the MatchmakingDSOnlineSession_Starter CPP file, and let's define the CancelMatchmaking() function first. This function checks whether it is valid to cancel matchmaking or not before executing the action. When the process is complete, it then calls the OnCancelMatchmakingComplete() function.

    void UMatchmakingDSOnlineSession_Starter::CancelMatchmaking(APlayerController* PC, const FName& SessionName)
    {
    UE_LOG_MATCHMAKINGDS(Verbose, TEXT("called"))

    // safety
    if (!ensure(GetSessionInt()))
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Session Interface is not valid."));
    ExecuteNextTick(FSimpleDelegate::CreateWeakLambda(this, [this, SessionName]()
    {
    OnCancelMatchmakingComplete(SessionName, false);
    }));
    return;
    }

    if (!ensure(GetABSessionInt()->CurrentMatchmakingSearchHandle.IsValid() &&
    GetABSessionInt()->CurrentMatchmakingSearchHandle->SearchingPlayerId.IsValid()))
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Searching player ID is not valid."));
    ExecuteNextTick(FSimpleDelegate::CreateWeakLambda(this, [this, SessionName]()
    {
    OnCancelMatchmakingComplete(SessionName, false);
    }));
    return;
    }

    if (!GetSessionInt()->CancelMatchmaking(
    *GetABSessionInt()->CurrentMatchmakingSearchHandle->SearchingPlayerId,
    GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession)))
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Failed executing"))
    // Failed to start matchmaking.
    ExecuteNextTick(FSimpleDelegate::CreateWeakLambda(this, [this, SessionName]()
    {
    OnCancelMatchmakingComplete(SessionName, false);
    }));
    }
    }
  5. Next, let's define the OnCancelMatchmakingComplete() function. This function does not do anything special, it just triggers the delegate you just created to inform that the matchmaking is canceled, whether it was successful or not.

    void UMatchmakingDSOnlineSession_Starter::OnCancelMatchmakingComplete(FName SessionName, bool bSucceeded)
    {
    UE_LOG_MATCHMAKINGDS(Log, TEXT("succeeded: %s"), *FString(bSucceeded ? "TRUE": "FALSE"))

    OnCancelMatchmakingCompleteDelegates.Broadcast(SessionName, bSucceeded);
    }
  6. Okay, now you need to bind the function above, so it will be called when the cancel matchmaking process is completed. You can do this by adding the following code to the RegisterOnlineDelegates() function, which is the first function to be called when the online session is initialized.

    void UMatchmakingDSOnlineSession_Starter::RegisterOnlineDelegates()
    {
    Super::RegisterOnlineDelegates();

    // Bind matchmaking delegates.
    GetSessionInt()->OnMatchmakingCompleteDelegates.AddUObject(this, &ThisClass::OnMatchmakingComplete);
    GetSessionInt()->OnCancelMatchmakingCompleteDelegates.AddUObject(this, &ThisClass::OnCancelMatchmakingComplete);
    }
  7. Finally, when the online session is deinitialized, you need to unbind to stop listening to the event. You can do this by adding the following code in the pre-defined ClearOnlineDelegates() function, which is the first function to be called when the online session is deinitialized.

    void UMatchmakingDSOnlineSession_Starter::ClearOnlineDelegates()
    {
    Super::ClearOnlineDelegates();

    // Unbind matchmaking delegates.
    GetSessionInt()->OnMatchmakingCompleteDelegates.RemoveAll(this);
    GetSessionInt()->OnCancelMatchmakingCompleteDelegates.RemoveAll(this);
    }
  8. Congratulations! You have created functions to cancel matchmaking.

Handle on a dedicated server received

When the matchmaking is finished and the game client joins the game session, the game client then waits to receive a dedicated server (DS) to travel. In this section, you will learn how to handle it.

  1. First, let's create a function to travel the game client to the dedicated server. Open the MatchmakingDSOnlineSession_Starter header file and then declare the following function.

    public:
    virtual bool TravelToSession(const FName SessionName) override;
  2. Next, open the MatchmakingDSOnlineSession_Starter CPP file and define the function above. This function performs some validation before traveling the game client to the DS. If it is valid, then it travels to the DS using the server address.

    bool UMatchmakingDSOnlineSession_Starter::TravelToSession(const FName SessionName)
    {
    UE_LOG_MATCHMAKINGDS(Verbose, TEXT("called"))

    if (GetSessionType(SessionName) != EAccelByteV2SessionType::GameSession)
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Not a game session"));
    return false;
    }

    // Get Session Info
    const FNamedOnlineSession* Session = GetSession(SessionName);
    if (!Session)
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Session is invalid"));
    return false;
    }

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

    const TSharedPtr<FOnlineSessionInfoAccelByteV2> AbSessionInfo = StaticCastSharedPtr<FOnlineSessionInfoAccelByteV2>(SessionInfo);
    if (!AbSessionInfo.IsValid())
    {
    UE_LOG_MATCHMAKINGDS(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_MATCHMAKINGDS(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_MATCHMAKINGDS(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_MATCHMAKINGDS(Warning, TEXT("Session is a P2P session"));
    return false;
    }

    FString ServerAddress = "";
    GetABSessionInt()->GetResolvedConnectString(SessionName, ServerAddress);

    if (ServerAddress.IsEmpty())
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Can't find session's server address"));
    return false;
    }

    if (!bIsInSessionServer)
    {
    AbPlayerController->DelayedClientTravel(ServerAddress, TRAVEL_Absolute);
    bIsInSessionServer = true;
    }
    else
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Already in session's server"));
    }

    return true;
    }
  3. Now, let's create a function to listen when the game client receives a server update. Open the MatchmakingDSOnlineSession_Starter header file and then declare the following function.

    protected:
    virtual void OnSessionServerUpdateReceived(FName SessionName) override;
  4. Next, declare a delegate that will be called when the dedicated server (DS) is received. You can use this delegate to bind some events later, especially when connecting UIs with the matchmaking implementation.

    public:
    virtual FOnServerSessionUpdateReceived* GetOnSessionServerUpdateReceivedDelegates() override
    {
    return &OnSessionServerUpdateReceivedDelegates;
    }

    private:
    FOnServerSessionUpdateReceived OnSessionServerUpdateReceivedDelegates;
  5. Now, open the MatchmakingDSOnlineSession_Starter CPP file and then define the OnSessionServerUpdateReceived() function. This function checks whether it is valid to travel to the dedicated server or not. It also invokes the delegate you created earlier to inform that the server update is received.

    void UMatchmakingDSOnlineSession_Starter::OnSessionServerUpdateReceived(FName SessionName)
    {
    UE_LOG_MATCHMAKINGDS(Verbose, TEXT("called"))

    if (bLeavingSession)
    {
    UE_LOG_MATCHMAKINGDS(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);
    }
  6. On some occasions, the backend might not be able to provide DS to the game client. Let's create a function to handle it. Open the MatchmakingDSOnlineSession_Starter header file and declare the following function.

    protected:
    virtual void OnSessionServerErrorReceived(FName SessionName, const FString& Message) override;
  7. Next, open the MatchmakingDSOnlineSession_Starter CPP file and define the function above. This function invokes the session server update delegate you created earlier but marks it as failed.

    void UMatchmakingDSOnlineSession_Starter::OnSessionServerErrorReceived(FName SessionName, const FString& Message)
    {
    UE_LOG_MATCHMAKINGDS(Verbose, TEXT("called"))

    FOnlineError Error;
    Error.bSucceeded = false;
    Error.ErrorMessage = FText::FromString(Message);

    OnSessionServerUpdateReceivedDelegates.Broadcast(SessionName, Error, false);
    }
  8. Okay, now you need to bind the function above, so it will be called when the server update is received. You can do this by adding the following code to the RegisterOnlineDelegates() function, which is the first function to be called when the online session is initialized.

    void UMatchmakingDSOnlineSession_Starter::RegisterOnlineDelegates()
    {
    Super::RegisterOnlineDelegates();

    // Bind matchmaking delegates.
    GetSessionInt()->OnMatchmakingCompleteDelegates.AddUObject(this, &ThisClass::OnMatchmakingComplete);
    GetSessionInt()->OnCancelMatchmakingCompleteDelegates.AddUObject(this, &ThisClass::OnCancelMatchmakingComplete);

    // Bind server event delegates.
    GetABSessionInt()->OnSessionServerUpdateDelegates.AddUObject(this, &ThisClass::OnSessionServerUpdateReceived);
    GetABSessionInt()->OnSessionServerErrorDelegates.AddUObject(this, &ThisClass::OnSessionServerErrorReceived);
    }
  9. Finally, when the online session is deinitialized, you need to unbind to stop listening to the event. You can do this by adding the following code in the pre-defined ClearOnlineDelegates() function, which is the first function to be called when the online session is deinitialized.

    void UMatchmakingDSOnlineSession_Starter::ClearOnlineDelegates()
    {
    Super::ClearOnlineDelegates();

    // Unbind matchmaking delegates.
    GetSessionInt()->OnMatchmakingCompleteDelegates.RemoveAll(this);
    GetSessionInt()->OnCancelMatchmakingCompleteDelegates.RemoveAll(this);

    // Unbind server event delegates.
    GetABSessionInt()->OnSessionServerUpdateDelegates.RemoveAll(this);
    GetABSessionInt()->OnSessionServerErrorDelegates.RemoveAll(this);
    }
  10. Congratulations! You have created functions to handle when receiving server update and then travel the game to the dedicated server.

Handle backfill

Backfill is a flow that allows players to join the game server that is not full yet. In this section, you will implement functions to handle backfill.

note

In this tutorial, when the backfill proposal is received, we will always accept the proposal. To reject the proposal, you can use the GetABSessionInt()->RejectBackfillProposal() function. The flow between accepting and rejecting the proposal is similar.

  1. First, open the MatchmakingDSOnlineSession_Starter header file and then declare the following function.

    protected:
    virtual void OnBackfillProposalReceived(FAccelByteModelsV2MatchmakingBackfillProposalNotif Proposal) override;
  2. Next, declare a delegate that will be called when the accepting backfill is completed.

    public:
    virtual FOnMatchmakingAcceptBackfillProposalComplete* GetOnAcceptBackfillProposalCompleteDelegates() override
    {
    return &OnAcceptBackfillProposalCompleteDelegates;
    }

    private:
    FOnMatchmakingAcceptBackfillProposalComplete OnAcceptBackfillProposalCompleteDelegates;
  3. Now, open the MatchmakingDSOnlineSession_Starter CPP file and then define the OnBackfillProposalReceived() function. This function accepts the received backfill, so the player can join the game server that is not full yet. It also invokes the delegate you created earlier to inform the game that the accepting backfill process is completed.

    void UMatchmakingDSOnlineSession_Starter::OnBackfillProposalReceived(FAccelByteModelsV2MatchmakingBackfillProposalNotif Proposal)
    {
    UE_LOG_MATCHMAKINGDS(Verbose, TEXT("called"))

    // Safety
    if (!ensure(GetABSessionInt().IsValid()))
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Session Interface is not valid."));
    return;
    }

    // Accept backfill proposal.
    GetABSessionInt()->AcceptBackfillProposal(
    GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession),
    Proposal,
    false,
    FOnAcceptBackfillProposalComplete::CreateWeakLambda(this, [this](bool bSucceeded)
    {
    UE_LOG_MATCHMAKINGDS(Log, TEXT("succeeded: %s To accept backfill."), *FString(bSucceeded ? "TRUE": "FALSE"));
    OnAcceptBackfillProposalCompleteDelegates.Broadcast(bSucceeded);
    }));
    }
  4. Okay, now you need to bind the function above, so it will be called when the backfill proposal is received. You can do this by adding the following code to the RegisterOnlineDelegates() function, which is the first function to be called when the online session is initialized.

    void UMatchmakingDSOnlineSession_Starter::RegisterOnlineDelegates()
    {
    Super::RegisterOnlineDelegates();

    // Bind matchmaking delegates.
    GetSessionInt()->OnMatchmakingCompleteDelegates.AddUObject(this, &ThisClass::OnMatchmakingComplete);
    GetSessionInt()->OnCancelMatchmakingCompleteDelegates.AddUObject(this, &ThisClass::OnCancelMatchmakingComplete);
    GetABSessionInt()->OnBackfillProposalReceivedDelegates.AddUObject(this, &ThisClass::OnBackfillProposalReceived);

    // Bind server event delegates.
    GetABSessionInt()->OnSessionServerUpdateDelegates.AddUObject(this, &ThisClass::OnSessionServerUpdateReceived);
    GetABSessionInt()->OnSessionServerErrorDelegates.AddUObject(this, &ThisClass::OnSessionServerErrorReceived);
    }
  5. Finally, when the online session is deinitialized, you need to unbind to stop listening to the event. You can do this by adding the following code in the pre-defined ClearOnlineDelegates() function, which is the first function to be called when the online session is deinitialized.

    void UMatchmakingDSOnlineSession_Starter::ClearOnlineDelegates()
    {
    Super::ClearOnlineDelegates();

    // Unbind matchmaking delegates.
    GetSessionInt()->OnMatchmakingCompleteDelegates.RemoveAll(this);
    GetSessionInt()->OnCancelMatchmakingCompleteDelegates.RemoveAll(this);
    GetABSessionInt()->OnBackfillProposalReceivedDelegates.RemoveAll(this);

    // Unbind server event delegates.
    GetABSessionInt()->OnSessionServerUpdateDelegates.RemoveAll(this);
    GetABSessionInt()->OnSessionServerErrorDelegates.RemoveAll(this);
    }
  6. Congratulations! You have created functions to handle backfill is completed. Build your project and make sure there's no error.

Setup dedicated server online subsystem

In this tutorial, you will be working with a game instance subsystem namely MatchmakingDSServerSubsystem_Starter class. This class is used by the dedicated server (DS) to handle the game session produced by matchmaking. This class is defined in the following files.

  • Header file can be found in /Source/AccelByteWars/TutorialModules/Play/MatchmakingDS/MatchmakingDSServerSubsystem_Starter.h.
  • CPP file can be found in /Source/AccelByteWars/TutorialModules/Play/MatchmakingDS/MatchmakingDSServerSubsystem_Starter.cpp.

Handle dedicated server receives session

When the matchmaking is finished, the backend creates and claims a dedicated server (DS) and assigns a game session to the DS. In this section, you will implement a function, so the DS can handle the received session.

  1. First, open the MatchmakingDSServerSubsystem_Starter header file and create the following function.

    protected:
    virtual void OnServerSessionReceived(FName SessionName) override;
  2. Next, open the MatchmakingDSServerSubsystem_Starter CPP file and define the function above. When the DS receives a game session from the backend, this function then assigns the correct game mode based on the information provided by the game session. This way, the DS can provide the correct gameplay to the connected players. Most of this implementation is specific to Byte Wars, so we won't be explaining much of it. But, the main point of this function is to get data from the session info on backend, which is done by the highlighted lines. You can use this as a reference on how to get specific data from the session info.

    void UMatchmakingDSServerSubsystem_Starter::OnServerSessionReceived(FName SessionName)
    {
    Super::OnServerSessionReceived(SessionName);
    UE_LOG_MATCHMAKINGDS(Verbose, TEXT("called"))

    #pragma region "Assign game mode based on SessionTemplateName from backend"
    // Get GameMode
    const UWorld* World = GetWorld();
    if (!World)
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("World is invalid"));
    return;
    }

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

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

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

    const FNamedOnlineSession* Session = MatchmakingOnlineSession->GetSession(SessionName);
    if (!Session)
    {
    UE_LOG_MATCHMAKINGDS(Warning, TEXT("Session is invalid"));
    return;
    }

    FString SessionTemplateName;
    Session->SessionSettings.Get(SETTING_SESSION_MATCHPOOL, SessionTemplateName);
    if (!SessionTemplateName.IsEmpty())
    {
    AbGameState->AssignGameMode(MatchmakingOnlineSession->TargetGameModeMap[SessionTemplateName]);
    }
    #pragma endregion

    // Query all currently registered user's info
    AuthenticatePlayer_OnRefreshSessionComplete(true);
    }
  3. Now, you need to bind the function above, so it will be called when the DS receives a game session from the backend. You can do this by adding the following code to the Initialize() function, which is the first function to be called when the subsystem is initialized.

    void UMatchmakingDSServerSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    Super::Initialize(Collection);

    UOnlineSession* BaseOnlineSession = GetWorld()->GetGameInstance()->GetOnlineSession();
    if (!ensure(BaseOnlineSession))
    {
    return;
    }
    MatchmakingOnlineSession = Cast<UMatchmakingDSOnlineSession_Starter>(BaseOnlineSession);

    GetABSessionInt()->OnServerReceivedSessionDelegates.AddUObject(this, &ThisClass::OnServerSessionReceived);
    }
  4. Finally, unbind the function when the subsystem is deinitialized. You can do this by adding the following code to the Deinitialize() function.

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

    GetABSessionInt()->OnServerReceivedSessionDelegates.RemoveAll(this);
    }
  5. Congratulations! You have created functions, so the DS can handle when a game session is received. Build your project again and make sure there's no error.

Resources