Skip to main content

Integrate matchmaking (Unreal)

Last updated on December 16, 2024
note

This article covers setting up matchmaking in an Unreal Engine game. If you're using Unity, use the Unity matchmaking integration article.

Overview

Matchmaking is the process by which two or more players can be evaluated for a match, and joined to a game session so they can play together. There are several components that AccelByte Gaming Services (AGS) Matchmaking interacts with during the matchmaking process.

  • Session Template: defines the characteristics a session will be created with. This includes joinability, what game server deployment to use, player requirements, team composition, etc.

  • Match Ticket: defines a request for matchmaking, either by a solo player or a party, with the player information attached for evaluation.

  • Backfill Ticket: similar to a match ticket, however, it is only created if auto-backfill is enabled in the match ruleset. In which case, when two or more players are matched, they will be joined to the session and a backfill ticket is submitted to AGS Matchmaking to continue to find matches until the minimum number of players have connected to the session.

  • Match Pool: defines a collection of match tickets that can be evaluated by the service for valid matches. Tickets end up in the same pool based on the selected game mode and preferences, if applicable.

  • Match Ruleset: defines the attributes and comparison criteria that will be used by the default match function to determine if tickets make a valid match. In the case that the match function has been overridden, the ruleset will still need to be configured with any attributes that are stored in AGS Statistics that are required for evaluation.

  • Match Function: defines the logic that the service will use to evaluate match tickets. By default, the service will use the criteria defined in the associated match ruleset.

This article will show you how to integrate AGS Matchmaking into your Unreal game client using the Unreal OnlineSubsystem (OSS) V2 plugin.

Goals

This article aims to provide you an understanding of the matchmaking player experience flow and show you how to integrate:

  • Starting matchmaking for solo players and parties
  • Canceling matchmaking before a match is found
  • Listening for and handling match results
  • Listening for game session invites and joining sessions

Prerequisites

To complete all the steps in this article, you will need:

  • A familiarity with AGS Lobby, Session, and Matchmaking.

  • An basic understanding of Unreal Engine, including the Online Subsystem (OSS).

  • The Unreal OSS V2 Plugin installed into your game project.

  • Access to the AGS Admin Portal.

  • A session template, match ruleset, and match pool created and configured.

  • Sessions V2 enabled in the AccelByte OSS by adding the following to the DefaultEngine.ini file:

    [OnlineSubsystemAccelByte]
    bEnableV2Sessions=true
  • The required permissions for AGS Matchmaking and Session V2 set up for your game client in the Admin Portal.

  • Your game client authenticated with the AGS backend and connected to AGS Lobby.

To integrate the ability to start matchmaking into your game project, there are several OSS components you need to be familiar with:

  • Session Interface: provides the functionality used to perform matchmaking and session management.

  • Session Search Handle: stores the results of a session search, required during matchmaking to store the game session returned if matching was successful.

  • Matchmaking Callback Functions: listens for specific matchmaking events, such as matchmaking started and matchmaking completed.

  • Join Session Callback Functions: listens for specific session events, such as destroy session or join session.

Implementing the Matchmaking flow

Implementation follows the high level flow described in our overview of the matchmaking flow.

Start matchmaking

In the function you plan to initiate matchmaking from, you need to acquire the Session Interface from the AGS OSS. Be aware, there is a custom AGS Session Interface of type FOnlineSessionAccelBytePtr you will be using to request matchmaking.

const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
if (!ensure(Subsystem != nullptr))
{
return;
}

const FOnlineSessionAccelBytePtr SessionInterface = StaticCastSharedPtr<FOnlineSessionV2AccelByte>(Subsystem->GetSessionInterface());
if (!ensure(SessionInterface.IsValid()))
{
return;
}
tip

It is recommended you store the Session Interface as a class member, as you will need to access it later on to handle callback results.

You need to acquire the Player ID for the game client so that you can later use it to pass the Player Controller ID as part of the request to start matchmaking. The Player Controller ID will also be used later on when handling callback results.

const FUniqueNetIdPtr LocalPlayerId = GetUniquePlayerId();
if (!ensure(LocalPlayerId.IsValid()))
{
return false;
}

You need to create a Session Search Handle FOnlineSessionSearch to be used to store the session result created during the matchmaking process if a match is found successfully. When creating a Session Search Handle, you will need to set the query settings with the following parameters:

  • Player Controller ID: the OSS ID of the player that the match ticket will be submitted for during matchmaking.
  • Match Pool Session Settings: the OSS session settings that will be used during matchmaking: SETTING_SESSION_MATCHPOOL. By passing this in, it will be updated with the Match Pool Name you provide as the next parameter.
  • Match Pool Name: the name of the match pool you defined as part of the prerequisites for the specified game mode. This will inform AGS Matchmaking to add the newly-created match ticket to that match pool for evaluation.
  • Comparison Operator: the OSS operator to be used as part of the session search, in this case: EOnlineComparisonOp::Equals.
// Create a new search handle instance for matchmaking. IMPORTANT: you will need to set the match pool that you are searching for with SETTING_SESSION_MATCHPOOL, as shown below.

TSharedRef<FOnlineSessionSearch> MatchmakingSearchHandle = MakeShared<FOnlineSessionSearch>();
MatchmakingSearchHandle->QuerySettings.Set(SETTING_SESSION_MATCHPOOL, FString(TEXT("YOUR_MATCHPOOL_NAME_GOES_HERE")), EOnlineComparisonOp::Equals);
tip

It is recommended you store a Session Search Handle as a class member and ensure it retains its type, and then assign the returned results after a match is found successfully.

You need to register a FOnMatchmakingCompleteDelegate callback delegate to listen for the match results, which will trigger when matchmaking completes.

  • Session Name: the name of the game session created if there was a successful match.
  • Success Value: a boolean value indicating whether matchmaking successfully found a match.
// Bind a function that has a return type of void and these parameters:
// FName SessionName, bool bWasSuccessful
const FOnMatchmakingCompleteDelegate OnMatchmakingCompleteDelegate = /* Bind to lambda or class method */;
SessionInterface->AddOnMatchmakingCompleteDelegate_Handle(OnMatchmakingCompleteDelegate);

Once you have the above, you can call StartMatchmaking() from the Session Interface to request matchmaking to start. This function takes the following parameters:

  • Player Controller ID: the OSS ID of the player that the match ticket will be submitted for during matchmaking.
  • Session Name: the name of the session created if matchmaking is successful. Typically, you will pass NAME_GameSession.
  • Session Settings: the session settings that should be used by AGS Session to create the game session: FOnlineSessionSettings(). In this case, you will leave it up to AGS Session to assign the settings by passing in an empty object.
  • Session Search Handle: the Session Search Handle you created earlier in this section to store the game session that will be created if matchmaking is successful.

Once the request to StartMatchmaking() is made, assuming the call returns true, you should bind the Session Search Handle you defined earlier in this section to a class member for future access. If StartMatchmaking() returns false, that indicates that there was an issue calling into AGS Matchmaking.

if (SessionInterface->StartMatchmaking(USER_ID_TO_MATCHMAKING_USER_ARRAY(LocalPlayerId.ToSharedRef()), NAME_GameSession, FOnlineSessionSettings(), MatchmakingSearchHandle, OnStartMatchmakingCompleteDelegate))
{
// Update the current search result handle class member with the search handle passed to AGS Matchmaking.
CurrentMatchmakingSearchHandle = MatchmakingSearchHandle;
}
note

With OnlineSubsystemV2, if you are in a party, the StartMatchmaking call will automatically send the PartyID to be attached as part of the match ticket during creation.

Join a game session

Once matchmaking completes, the OnMatchmakingCompleteDelegate you bound prior to starting matchmaking will fire and you can process the results by following these instructions.

You need to retrieve the game session result stored in the SearchResults member array of the Session Search Handle. If the array is empty, refer to Troubleshooting.

// Ensure that you have a valid session search result in the array before continuing
if (!CurrentMatchmakingSearchHandle->SearchResults.IsValidIndex(0))
{
return false;
}

FOnlineSessionSearchResult MatchResult = CurrentMatchmakingSearchHandle->SearchResults[0];
EOnlineSessionTypeAccelByte SessionType = SessionInterface->GetSessionTypeFromSettings(MatchResult.Session.SessionSettings);
if (SessionType != EOnlineSessionTypeAccelByte::GameSession)
{
return false;
}

You need to check if the player is already in a game session, if so, you will need to destroy it so they can join the new session returned through matchmaking. You can register a FOnDestroySessionCompleteDelegate callback delegate to listen for the result.

// Check if you already have a game session. If so, destroy it to join this one.
if (SessionInterface->GetNamedSession(NAME_GameSession) != nullptr)
{
const FOnDestroySessionCompleteDelegate OnDestroySessionForJoinCompleteDelegate = FOnDestroySessionCompleteDelegate::CreateUObject(this, &UOSSDemoGameSessionSubsystem::OnDestroySessionForJoinComplete, Session);
return SessionInterface->DestroySession(NAME_GameSession, OnDestroySessionForJoinCompleteDelegate);
}

You will need to register a FOnJoinSessionCompleteDelegate callback delegate to listen for the join result, which will trigger when the join request completes. At which point you can request to join the game server. Refer to the Joining a Host Server section of this article for sample code for the callback function.

// Register a delegate for joining the specified session
const FOnJoinSessionCompleteDelegate OnJoinSessionCompleteDelegate = FOnJoinSessionCompleteDelegate::CreateUObject(this, &UOSSDemoGameSessionSubsystem::OnJoinSessionComplete);
JoinSessionDelegateHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegate);

Once you have the above, you can call JoinSession() from the Session Interface to join the returned session. This function takes the following parameters:

  • Player Controller ID: the OSS ID of the player that the match ticket will be submitted for during matchmaking.
  • Session Name: the name of the game session created if matchmaking is successful.
  • Session: the OSS Session object that AGS Session will assign the game session info to for later access.
SessionInterface->JoinSession(LocalPlayerId.ToSharedRef().Get(), NAME_GameSession, Session);
note
  • If a player leaves or drops from a session, they cannot rejoin the same session through AGS Matchmaking. The player will need to use the Join API from AGS Session. If the session has its joinability set to OPEN or is set to INVITE_ONLY, they can call Session::JoinGameSession to rejoin the session.
  • If a player is still in the DISCONNECTED status, their status will turn back to CONNECTED once they reconnect to AGS Lobby. In this case, the player can get the game session details by using Session::GetMyGameSessions or Session::GetGameSessionDetails if they have the game ID.

Here is a full example of a callback function you can use as the delegate to listen for OnMatchmakingComplete results:

void OnMatchmakingCompleteDelegate(FName SessionName, bool bWasSuccessful)
{
if (SessionName != NAME_GameSession)
{
return;
}

EOnlineSessionTypeAccelByte SessionType = SessionInterface->GetSessionTypeFromSettings(MatchResult.Session.SessionSettings);
if (SessionType != EOnlineSessionTypeAccelByte::GameSession)
{
return false;
}

// Ensure that you have a valid session search result in the array before continuing
if (!CurrentMatchmakingSearchHandle->SearchResults.IsValidIndex(0))
{
return false;
}

FOnlineSessionSearchResult MatchResult = CurrentMatchmakingSearchHandle->SearchResults[0];

// Check if you already have a game session that you are in. If so, destroy it to join this one.
if (SessionInterface->GetNamedSession(NAME_GameSession) != nullptr)
{
const FOnDestroySessionCompleteDelegate OnDestroySessionForJoinCompleteDelegate = FOnDestroySessionCompleteDelegate::CreateUObject(this, &UOSSDemoGameSessionSubsystem::OnDestroySessionForJoinComplete, Session);
return SessionInterface->DestroySession(NAME_GameSession, OnDestroySessionForJoinCompleteDelegate);
}

// Register a delegate for joining the specified session
const FOnJoinSessionCompleteDelegate OnJoinSessionCompleteDelegate = FOnJoinSessionCompleteDelegate::CreateUObject(this, &UOSSDemoGameSessionSubsystem::OnJoinSessionComplete);


JoinSessionDelegateHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegate);

const FUniqueNetIdPtr LocalPlayerId = GetUniquePlayerId();
if (!ensure(LocalPlayerId.IsValid()))
{
return false;
}

return SessionInterface->JoinSession(LocalPlayerId.ToSharedRef().Get(), NAME_GameSession, Session);
}

Join a game host

If joining the game session completes, the OnJoinSessionComplete you bound prior to requesting to join the session will fire and you can process the results by following these instructions.

You will need to check the results enum passed into the delegate to determine if the join request completed successfully and you have joined the game session.

if (Result != EOnJoinSessionCompleteResult::Success)
{
return;
}

If successful, you have joined the game session and can extract the associated host connection from the Session Interface by calling GetResolvedConnectString(). The function takes the following parameters:

  • Session Name: the name of the game session created if matchmaking is successful.
  • TravelURL: will contain the connection string, if the session is connected to the host.
  • Game Port: the host port for the connection.

After requesting the connection string, the TravelURL will either contain a valid connection string or will be empty. If it contains a valid connection string, that means the game session has been connected and now you can use the string to connect the player to the host.

If it is empty, then the game session is still waiting to connect and you will need to register a OnSessionServerUpdate callback delegate to listen for the host join result and wait for the game session to successfully connect to the host.

FString TravelUrl{};

if (SessionInterface->GetResolvedConnectString(SessionName, TravelUrl, NAME_GamePort) && !TravelUrl.IsEmpty())
{
// If you successfully got a connect string from the session, then this is where you would connect to it
}
else
{
// Otherwise, bind a delegate to OnSessionServerUpdate to wait for a server to be spun up.
const FOnSessionServerUpdateDelegate OnSessionServerUpdateDelegate = FOnSessionServerUpdateDelegate::CreateUObject(this, &UOSSDemoGameSessionSubsystem::OnSessionServerUpdate);

SessionInterface->AddOnSessionServerUpdateDelegate_Handle(OnSessionServerUpdateDelegate);
}

Here is a full example of a callback function you can use as the delegate to listen for OnJoinSessionComplete results:

void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
if (SessionName != NAME_GameSession)
{
return;
}

if (Result != EOnJoinSessionCompleteResult::Success)
{
return;
}

const FOnlineSessionAccelBytePtr SessionInterface = GetSessionInterface();
check(SessionInterface.IsValid());

// Remove your delegate handler for join session. You will rebind if you join another session
SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionDelegateHandle);
JoinSessionDelegateHandle.Reset();

FNamedOnlineSession* Session = SessionInterface->GetNamedSession(SessionName);
check(Session != nullptr)

TSharedPtr<FOnlineSessionInfoAccelByteV2> SessionInfo = StaticCastSharedPtr<FOnlineSessionInfoAccelByteV2>(Session->SessionInfo);
check(SessionInfo.IsValid());

FString TravelUrl{};
if (SessionInterface->GetResolvedConnectString(SessionName, TravelUrl, NAME_GamePort) && !TravelUrl.IsEmpty())
{
// If you successfully got a connect string from the session, then this is where you would connect to it
}
else
{
// Otherwise, bind a delegate to OnSessionServerUpdate to wait for a server to be spun up. Once this
// delegate is fired, you may call the same GetResolvedConnectString to get the server address to
// connect to.
const FOnSessionServerUpdateDelegate OnSessionServerUpdateDelegate = FOnSessionServerUpdateDelegate::CreateUObject(this, &UOSSDemoGameSessionSubsystem::OnSessionServerUpdate);
SessionInterface->AddOnSessionServerUpdateDelegate_Handle(OnSessionServerUpdateDelegate);
}
}

Additional Matchmaking calls

Cancel matchmaking

If you need to cancel matchmaking before the process completes or timeout occurs, you can call FOnlineSessionV2Accelbyte::CancelMatchmaking(), passing in the Player Controller ID, the Session Name, and the NAME_GameSession you provided when you requested matchmaking to start.

Add version number to match ticket

Neither the AGS Game SDK or OSS automatically add a client_version to the match ticket when it is sent.

For the SDK, the MatchmakingV2::CreateMatchTicket API takes in an Optionals struct of type FAccelByteModelsV2MatchTicketOptionalParams. Within that is a field named Attributes where you can add the client_version field. See the code snippet below as an example:

FAccelByteModelsV2MatchTicketOptionalParams Optionals;

Optionals.Attributes->SetStringField(TEXT("client_version"), TEXT("desired_override_string");

ApiClient->MatchmakingV2.CreateMatchTicket(MatchPool, OnStartMatchmakingSuccessDelegate, OnStartMatchmakingErrorDelegate, Optionals);

For the OSS, see the code snippet below as an example:

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

This then gets passed into the StartMatchmaking call of the session interface. You would just need to use one of these snippets depending on which one you are using to initiate matchmaking in your game.

Once you've done the above, using whatever value is meaningful to your game for the client version, you should add the same value as a claim key on the fleet config(s) that you want that version to match to. Leave the claim keys in the session template empty unless you have other fleets you always want to try to get a dedicated server from first before trying to get one from the fleets matching your client_version.

For more context, the high-level expectation is that developers (or CI scripts) create and configure fleets with one or more claim keys (which can be anything, including strings matching a client version). When the Session service asks for a dedicated server, it includes an ordered list of regions and an ordered list of claim keys to match in the request. The set of claim keys it uses for the request is built from the claim keys listed in the session template, plus the ClientVersion. If you're using AccelByte Multiplayer Servers (AMS), it will find a matching dedicated server in one of the provided regions from fleets that match any of the provided claim keys (in order), as described in the Dedicated server claim flow.

Supported Notifications

As part of the matchmaking process, the service will send the following notifications to the game client through AGS Lobby and will be viewable in the log files.

NotificationDetails
OnSessionInvitedNotification that is sent to the Game Client indicating that there is a pending session invite.
OnSessionJoinedNotification that is sent to the Game Client indicating that it has successfully joined a session.
OnSessionKickedNotification that is sent to the Game Client indicating that it has been kicked out of a session.
OnSessionEndedNotification that is sent to the Game Client indicating that a session they were in has ended.
OnSessionMembersChangedNotification that is sent to all connected Game Clients informing them there has been a change in membership, player has connected or disconnected.
OnSessionDataChangedNotification that is sent to all connected Game Clients informing them there has been a change to the session information.
dsNotifNotification that is sent to all connected Game Clients providing them with the Dedicated Server status.
OnMatchmakingTicketExpiredNotification that is sent to the Game Client indicating that the request for matchmaking has exceeded the defined period in the Match Pool and has timed out.

Troubleshooting

In this section, you can find common errors and issues that may occur when using the service, along with recommendations on how to resolve them.

OnMatchmakingTicketExpired notification from AGS Lobby

If you receive the OnMatchmakingTicketExpired notification from AGS Lobby, that means that the ticket reached the timeout limit and the player has been removed from matchmaking. This often happens if the timeout defined in the match pool configuration is too short or the match ruleset doesn't allow enough rule flexing to enable the player to match with others.

  • Review the match pool configuration to determine if the match or backfill ticket timeout is too short.
  • Review the match ruleset to make sure if the minimum player or team requirements have been set correctly.

OnMatchmakingComplete SearchResults array is empty

When listening for the match result to notify you that matchmaking is complete, you may encounter an empty SearchResults array in the Session Search Handle. This likely means that the handle hasn't been saved as TSharedRef, which will prevent you from retrieving the matchmaking session result.

  • Be sure the Session Search Handle maintains type TSharedRef throughout.