Matchmaking #

The Matchmaking service compares available parties and combines them into matches. This service handles queue configuration and matchmaking logic. Custom behavior can be defined using the Matchmaking Plugin.

Parties enter the Matchmaking service from the Party service, and are placed into a queue and then matched with other parties. Once a match has been built and processed, it is sent to the Match Lifecycle service and assigned to a game server.

The Matchmaking service is structured to work at scale to support the world’s largest games. All matchmaking state is held in-memory upon entry from the Party service to improve performance by avoiding extraneous network hits. This allows the Matchmaking service to remain CPU-dependent and tailored to prioritize for match speed or quality; when more comparison data is used, the matchmaking is slower but the quality of the match is higher.

In the following sections, we’ll take a look at the basic matchmaking flow, some key concepts, and then dive into the specifics of the Matchmaking Plugin, including developer-defined points to allow for any custom matchmaking configuration.

Matchmaking Flow Overview #

At a very high level, there are just a few steps the Matchmaking service cycles through while intaking parties from the Party service and outputting approved matches to the Match Lifecycle service.

To enter matchmaking, a party indicates that it’s ready to be sent to the Matchmaking service when a party leader triggers the call to start matchmaking:

PlayerSession->Party().StartMatchmakingV1();
playerSession.Party.StartMatchmakingV1();
PartyRpc.startMatchmaking

At this point, players receive the SessionServicePB.SessionChangedV1Notification. The party is assigned a matchmaking ID, and it becomes a Potential Match. All parties entering matchmaking become Potential Matches.

Matchmaking compares and combines Potential Matches, as explained in the Building a Match section below; a Potential Match can contain multiple parties that have been combined. Once a full Potential Match group is confirmed as complete and marked readyToStart, the match is sent to the Match Lifecycle service to allocate a game server and place the party.

To leave matchmaking without finding a viable match, there are two possible scenarios.

  • A player client can request to leave matchmaking in a variety of expected ways, such as by having the leader remove the party.
  • A player client can accidentally leave matchmaking by disconnecting.

If one of the parties in a Potential Match leaves matchmaking, the other parties in that Potential Match retain their place in the queue, and the exiting party is removed from the Potential Match.

Key Concepts #

This section provides a baseline understanding of key components used in the Matchmaking Plugin.

The Plugin #

Most developer-defined logic can be implemented via the Matchmaking Plugin. By default, this plugin provides the most basic skeleton of a matchmaking flow, with two key functions: initialize and buildMatch. In the Matchmaking Plugin section, we’ll use these two functions to guide us through understanding all relevant fields and customization options for the Matchmaking service.

Match Type Objects #

Pragma Engine uses a few types of matches to support custom matchmaking flows.

The Potential Match object represents a party or group of parties attempting to create a match. They have not yet made it to a game server.

Matchable Parties represent a Potential Match that has parties that can be added to existing matches. Parties in Matchable Parties can be moved into another match.

Queues and Queue Keys #

Pragma Engine groups parties together into game queues so matchmaking logic can be set to apply only to one specific group.

Parties that enter the Matchmaking service are split by the Matchmaking Queue Key. Each distinct Matchmaking Queue Key represents a separate queue. Queues are a concept used to segment Matchable Parties to ensure only parties within the same queue can be matched together. Matchmaking Queue Keys contain the set of data defining which queue a party joins.

This key contains two pieces of information which are both set in the Party service: a GameServerVersion that splits queues by the version of the game server, and a custom ExtMatchmakingKey which provides a location for developers to define variables they want to use to split queues. Having a blank ExtMatchmakingKey and GameServerVersion causes all players to enter one global queue.

Example: Consider a game that has multiple game modes: 3v3 Ranked, 3v3 Casual, and Co-op Adventure. In this case, we configure the ExtMatchmakingKey proto to consider the game mode.

message ExtMatchmakingKey {
    GameMode game_mode = 1;
}

enum GameMode {
    Unspecified = 0;
    ThreeVsThreeRanked = 1;
    ThreeVsThreeCasual = 2;
    CoopAdventure = 3;
}

Team Numbers #

Team numbers provide an optional way to modify the teams in a Potential Match during both the initialize and buildMatch processes. By default, all players start on team 0.

The following calls can be used to manage team numbers, either for an individual player or an entire party.

interface MatchableParties{
   fun assignPlayersToTeams(playerIdList: List<Fixed128>, teamNumber: Int)
   fun assignPartiesToTeams(partyIdList: List<UUID>, teamNumber: Int)
}
  • assignPlayersToTeam enables moving players in a party to different teams within the Potential Match. This allows for a scenario where a developer wishes to split a party’s players into opposing teams when necessary for MMR balancing.
  • assignPartiesToTeam enables keeping players within a party in the same team. Use this for parties that want to remain together regardless of skill. Use this for situations where an incoming party must stay together.

Matchmaking Plugin #

The Matchmaking Plugin provides two basic methods that allow for a wide variety of matchmaking implementations. These two methods are initialize and buildMatch. The initialize method handles initial steps for parties that enter the Matchmaking service, and the buildMatch method handles the match comparison and formation flows.

Once a party successfully makes its way through all the logic contained within these methods, they are sent to the Match Lifecycle service along with the other parties in their Potential Match via the startMatch method.

Initializing a match #

At this stage of the matchmaking flow, parties enter from the Party service. The initialize method is called every time a new party enters the Matchmaking service via PartyRpc.enterMatchmaking, which is called as part of PartyRpc.startMatchmaking.

interface MatchmakingPlugin {
   fun initialize(queueKey: MatchmakingQueueKey, potentialMatch: PotentialMatch) {
      // Custom logic to run before adding this potential match to a queue
   }
   ...
}

Each party that joins matchmaking triggers the creation of a Potential Match for that party. This Potential Match is like a train car, where there’s space for other parties to join until the car is full.

Exceptions on initialize cause the enterMatchmaking endpoint to log an error and the party is not added to the matchmaking queue, with the party leader receiving the PartyService_FailedToStartMatchmaking error.

As part of the initialize step, the Potential Match can be fast tracked to be started immediately, or it can be sent to the buildMatch step for matchmaking.

Fast Track Matches #

Any Potential Match method that doesn’t require multiple parties can happen during the initialize step, as long as it relies on data available to the party at that time.

This means that if a party contains all the players needed for a match, and the developer wants to enable fast-tracking the match and ignore any mismatches that would typically invalidate a matchup (such as skill disparity), startMatch can be called directly on this Potential Match. This skips the buildMatch step.

Example: Custom games for events like in-house tournaments may want to skip matchmaking and place players directly in a match regardless of skill level or other standard matchmaking data.

Building a match #

The primary method in the Matchmaking Plugin is buildMatch, which is used to perform matchmaking operations such as comparing players and moving them between matches. In the next sections, we’ll take a look at how to Compare Matches and how to Move Parties Between Matches.

This method must be implemented for matchmaking logic that isn’t fast tracking matches using initialize:

interface MatchmakingPlugin {
    ...
    fun buildMatch(queueKey: MatchmakingQueueKey, potentialMatch: PotentialMatch, matchable: MatchableParties) {
        // Custom matchmaking logic
    }
}

After each buildMatch call, there are a few possible outcomes for Potential Matches:

  • Start the match if the Potential Match has been marked readyToStart via the startMatch call.
  • Remove a Potential Match from the queue if it has been marked for removal, which can happen if the method throws an exception or if a Potential Match has no more players.

Compare Matches #

As part of the buildMatch method, matches are compared using a set of developer-defined matchmaking data, such as skill level or preferred game server zones. Only two match type objects can be compared at once. One object is always cast to the Matchable Parties class.

Example:

  • When comparing a Potential Match and a Potential Match, one is cast as a Matchable Party.

Within each queue, the oldest Potential Match is selected as the anchor match. All other Potential Matches in that queue are then classified as Matchable Parties, and each of these are compared to the anchor to check viability of combining parties into one match. The anchor Potential Match begins with the next oldest Matchable Party, and continues through all Matchable Parties in the queue until it reaches the accepted state for a full match and the startMatch function is called.

The anchor Potential Match cycles through the queue until it builds a match or until the anchor is moved to the next Potential Match.

Behind The Scenes

If the anchor Potential Match makes it through the entire queue without forming a complete match, matchmakingFinally is invoked, causing this matchmaking loop to process one more time. If it still does not complete a match after the second loop, the next oldest Potential Match becomes the anchor match. If the previous anchor match was a Potential Match, it becomes a Matchable Party. The process continues indefinitely.

If the buildMatch method throws an exception, the Matchmaking service logs an error and removes both the anchor Potential Match and the Matchable Party from the queue to allow for debugging. Players are notified via a SessionChangedV1Notification. The anchor position is then reset to the oldest Matchable Party in the queue, and it becomes the anchor Potential Match.

For each comparison, the following information is available to the Matchmaking Plugin for consideration in your matchmaking logic. This data is stored on the Potential Match object.

Match Comparison Data
  • parties - list of matchmaking parties
    • ExtPartySelections
    • ExtHiddenPartyData
    • preferredGameServerZones
    • ExtEnterMatchmakingV2Request
    • startedQueueTimestampMillis
    • Player details for all players within that matchmaking party
  • players - list of all players in match parties
    • Pragma session key
    • displayName
    • teamNumber
    • ExtPlayerSelections
    • ExtPrivatePlayerSelections
    • ExtHiddenPlayerData
    • gameClientVersion
    • gameServerZoneToPing
  • playersByParty() - list of all players, grouped by matchmaking party
  • playersByTeam() - list of all players, grouped by team number
  • matchmakingKey - the ext data associated with the queue
  • gameServerVersion - the version of the game server the match would be played on

Move Parties Between Matches #

When the matchmaking logic that compares Potential Matches determines that parties should be moved from one match to another, they can be moved by invoking either of the following functions, depending on the developer’s preferred behavior.

interface MatchableParties{
   moveAllPartiesFrom(matchableParties: MatchableParties)
   movePartiesFrom(matchableParties: MatchableParties, partyIds: List<UUID>)
}
  • moveAllPartiesFrom(matchableParties: MatchableParties): Move everyone from one match to another one. Use this when all of the parties in a Matchable Party can be moved to the anchor Potential Match.
  • movePartiesFrom(matchableParties: MatchableParties, partyIds: List<UUID>): Move individual parties from one match to another. Use this when only a subset of parties in a Matchable Party should be moved to the anchor Potential Match.

Starting a match #

When startMatch is called on a Potential Match, it stores a GameServerDetails object on the Potential Match, which is then sent to the Match Lifecycle service. This object contains all the data needed for a match to be started on a game server by the Match Lifecycle service. The Potential Match is then removed from the queue.

The GameServerDetails class contains two pieces of information: gameServerZone and allocateMatchRequest.

data class GameServerDetails(
    var gameServerZone: String,
    var allocateMatchRequest: ExtCreateMatchV1Request
)
  • gameServerZone is used to allocate a match on a game server in the desired physical location. This is set by the Party service.

  • allocateMatchRequest includes custom data via the ExtCreateMatchV1Request payload. This payload contains anything a developer may want to communicate to the match, such as game mode or difficulty.

Sample Implementation #

Pragma Engine provides a single sample implementation which can be configured for use in playtesting purposes or extended for use in production.

The provided example does not perform skill-based matchmaking. We recommend either extending this example with skill-based matchmaking logic or implementing custom matchmaking plugins.
MatchmakingStrategyBehavior
WarmBodyMatchmakingPluginCreates matches with a configurable playersPerTeam players on a configurable numberOfTeams teams.

Configuration:

game:
  core:
    pluginConfigs:
        MatchmakingService.matchmakingPlugin:
          class: "pragma.matchmaking.WarmBodyMatchmakingPlugin"
          config:
            numberOfTeams: 2
            playersPerTeam: 3