Custom Match Functions
warning
This documentation is now deprecated. If you are using Matchmaker Self Serve though UDash, please use the documentation here.
tip
Custom Match Functions may not be enabled for your Unity Matchmaker.
If you are interested in using Custom Match Functions, please contact your sales representative.
#
Description#
DevelopmentAuthoring a match function involves implementing a single ExecuteAsync
method that defines the IMatchFunction
interface. Expect this method to be called at a frequent but configurable cadence.
The match function is responsible for turning matchmaking tickets into match proposals, which are then used to allocate dedicated servers for the grouped players to use for their online game session. Each activation of the match function should go through the following phases:
- Query for submitted matchmaking tickets.
- Group tickets together by using custom logic that you write.
- Generate, score, and then return match proposals by using custom logic that you write.
Match functions output proposals, which represent a potential match to the matchmaker. A proposal contains the list of tickets that should play in a match together and properties about the proposal.
- Proposals contain a Score field, which a match function developer can use to indicate the importance of a proposal.
- Proposal properties contain a JSON string field that can include any other data that you want to assign to tickets, such as team arrangements or selected map. This data is also assigned to any other tickets that make it through the matchmaking process.
note
For complex scenarios or to error out bad tickets, you can use the ConnectionOverride and AssignmentError fields in the proposal properties to skip assignment functionality in the backend.
#
UploadingWhen zipping your function build from a csproj, ensure that the assemblies in the .zip file are flat and are not nested in different subdirectories. All function names will be lowercased when uploaded.
For more information, see the Command-line interface documentation.
#
Querying ticketsThe first step for any match function is to query for tickets to determine if there are any players who are waiting to get into a match. Tickets include developer-defined attributes, such as Quality of service (QoS) connection quality, or player preferences, such as opting to play a ranked or a casual game.
Pools generate the filters for querying tickets. Pools are specified in the match function config, which is detailed in the matchmaker API documentation. Pools are provided to the match function through the Pools
property of the FunctionContext parameter. This property acts as a default set of filters that query for tickets for your match function.
Your query can contain multiple filters. The returned list of tickets is the intersection of all tickets that match those filters.
- If there are no qualifying tickets, the match function returns and waits until the next activation.
- If there are tickets, use your custom match logic to group the tickets into proposals.
Developers can choose to ignore pools in the match function config and instead create their own pool or set of query filters. The TimeoutFunction ensures that there is exactly one pool with a filter on the created
attribute of the ticket, which is the timestamp for when the ticket was created. When the query runs, all tickets that match the filter are returned, at which point you can then group the tickets together by using your custom match logic.
note
The included TimeoutFunction sample demonstrates a match function that creates its own pool if none are present.
#
Scoring proposalsProposals include a Score
property to rate the quality or urgency of the proposal. Because it is possible (and even likely, due to the distributed and parallel nature of how match functions run) to have a ticket belong to several proposals at a time, the score decides which of those proposals to actually turn into a match. To get started, consider using an exponential priority on time-spent-waiting for the tickets in a proposal to ensure that players who have been waiting for a longer amount of time are prioritized.
note
The provided samples include an implementation of an RMSE (root mean squared error) on time-waiting.
#
Backfill V2The basic concept behind backfill v2 is that a dedicated game server (DGS) can represent its need for tickets as a special type of ticket (backfill ticket). These tickets can be created, queried and updated by match functions. The DGS is required to approve these tickets, or replace them with whatever its latest desired state is. There are some caveats to this that will be discussed below. The DGS may also create or delete backfill tickets at any point in time.
The main goal of this architecture is to remove the possibility of accidentally creating more servers than are required; all existing DGS with capacity can be represented and observable in match functions so that they can make the best decision about allocating new servers vs utilizing existing servers.
#
Backfill General OverviewBackfill differs from the general matchmaking case in that a typical matchmaking scenario is some set of players and servers with no current association that are paired together based on various custom algorithms. Backfill has the implication that a match of this sort has already been created, but for some reason, has been rendered incomplete due to a loss of players or a failure of an assigned player to connect. In any event, a key difference is that for the backfill case, a server has already been selected to host a match, and is now expressing a need to get new players.
The following image details methods for how backfill is typically used:
For more information, see the Backfill API documentation.
The following sections detail some backfill use cases by category.
#
Backfill to keep target player countA server might backfill to keep target player count. This could happen for a variety of reasons, such as the following scenarios:
- To replace any players that drop during a match
- To ensure full utilization of a server to reduce costs
- To ensure a target number of players have connected before beginning a match (for example, consider a battle royale where players connect to a holding area while all players are given time to connect or be replaced by backfill before the match starts)
#
Backfill to balance teamsIn team-based modes, when one team gains a large lead over another team, it is common for players to disconnect before a match is complete. However, many games are designed so gameplay cannot continue fairly or at all without the correct number or type of players. This is especially true if players are tied to specific roles (for example, in class-based game modes).
Example use cases of this scenario include the following events:
- Replace players who drop without rebalancing teams
- Replace dropped players as fast as possible
- Replace dropped players on the losing team with higher skilled players to compensate for the winning team's lead
- Rebalance players between teams, and then replace players
- For example, if there is a team imbalance of 8 to 4, the server could move 2 players so the teams are 6 to 6, and then perform backfill to find comparably skilled players until teams are full again
- Pause the game while waiting for backfill to complete or time out
- For example, role-based games that require specific pairings (such as a driver and shooter pair) where the game cannot continue fairly or at all if a required role is not filled
#
Backfill V2 Lifecycle OverviewThe below diagram outlines the flow of a BackfillTicket:
In backfill v2, tickets have the following lifecycle:
- Created
- Observed by a match function and associated with a backfill ticket
- Tickets wait for the backfill ticket to be approved by the DGS
- If tickets time out, they go back into the matchmaker pool. This occurs if the DGS does not approve the BackfillTicket for various reasons.
- If the DGS rejects the backfill ticket (either by deleting it or updating the backfill ticket with the latest server state), the tickets are also released back into the matchmaker ticket pool
- If the DGS approves the backfill ticket, the tickets receive the assignment data associated with them when they were created and are given the server connection and assigned just like the normal ticket-assignment flow
#
Match Function ExampleThe following diagram illustrates the steps a match function and DGS go through to allocate tickets via a backfill request:
In the example above a match function executes and finds 5 players available, and no existing backfill tickets. The match function can assume “no other server exists at the moment that handles this type of ticket” and make a new backfill ticket associated with the 5 players.
At that point, a new server allocation request associated with that backfill ticket will be made.
While that server is starting, the matchmaker may execute a few more cycles of matchmaking.
When match functions are executed again, they will observe the new backfill ticket and whatever state was written by the match function, in this case, the current and needed player counts.
The match function can then prioritize filling that backfill ticket by adding all 10 new tickets to a proposal associated with that backfill ticket.
The DGS finally starts and immediately calls the ApproveTicket API which assigns the 15 tickets associated with the backfill ticket to the DGS. The ApproveTicket ticket request returns the latest version of the backfill ticket to the DGS. At this point the DGS now has full access to any custom attributes/properties that were written by the match function.
The match function can continue to add tickets to the backfill ticket, until it is full. When the DGS observes that the backfill ticket state indicates its request for additional tickets has been satisfied, it should delete the ticket. Additionally, the server may at this point have lost players and either update its existing backfill ticket to reflect the new server state or create a new backfill request if one does not already exist. (Covered Below)
#
DGS Needs to Update Backfill TicketThe bellow chart shows what happens when the DGS makes an update to the backfill ticket via the update API
In the above example, the DGS updates the ticket to show that the server has lost 3 players, the next run of the match function picks up this state, and adds 3 more players, on the next few DGS approvals, these new tickets should show up.
NOTE: The match function may execute anywhere between .3 to 3 seconds depending on current load. Typically things run in 1 second or less, but that means the DGS may need to approve tickets multiple times before an updated ticket is observed.
#
DGS Updates Backfill Ticket Too FrequentlyThe below chart shows what happens when the DGS makes an update to the backfill ticket via the update API at the same time a match function is attempting to update the last version of the backfill ticket it observed.
The diagram above shows that at the same time the match function was about to add 5 tickets, the DGS performed an update that increased the number of tickets desired from 10 to 13. The backfill tickets are versioned to detect these types of stale read/write conflicts and the match function will always fail as it is not the authority on the state of the ticket!
In the example above, the match function tries again, but the same thing happens again!
There is in fact no guarantee things will ever progress if the DGS continues to update tickets faster than the match function can execute!
caution
To avoid this situation, have the DGS hold non critical updates until the next approval observes that the match function has made a change, or at least longer than the maximum matchmaker cycle of 3 seconds. This eliminates the possibility of getting stuck in a state where the DGS is updating tickets and the matchmaker is constantly proposing changes to stale tickets.
#
Changes to ProposalsProposals have been updated to have a BackfillTicket property. This property can be set in the match function to either associate it with an existing backfill ticket or create a new backfill ticket.
#
Create a New BackfillTicket and Server AllocationIf the BackfillTicket property is set to a newly created backfill ticket (a BackfillTicket without a backfill ticket ID) it will be considered a new server allocation and create an associated backfill ticket.The matchmaker will execute the following steps:
- Create a new backfill ticket and send its ID to the session data that will be given to the DGS
- Associate the tickets from the proposal with the backfill ticket
#
Assign Tickets to Existing BackfillTicketWhen an existing backfill ticket is queried, it will come in with an ID already set. Everything but the id is updatable (the attributes, and properties). The match function can do things like add ticket ids, properties, update counters (for instance, “players needed”). Creating a proposal and setting the BackfillTicket property to be this backfill ticket will cause the matchmaker to run the proposal through the evaluator (to ensure each backfill ticket only creates one proposal), then once again associate the additional tickets with the backfill ticket.
#
How Tickets Get Assigned or ReleasedAt this point, for new or updated backfill tickets, the following actions and inactions will update, assign, or release the tickets back into the matchmaking pool:
- The minimum reservation time for a ticket assigned to a backfill server expires, releasing all tickets into the pool. This prevents tickets from being captured permanently by non responsive servers.
- The server accepts the backfill ticket, which assigns all the tickets.
- The server updates/deletes the backfill ticket, which releases all tickets
#
Backfill V2 API#
Contracts / APIBackfill V2 Introduces a new API and contracts, For more information, see the Backfill V2 API documentation and Backfill V2 Contracts.
#
SDK Support for backfill V2Make sure you have the latest version of the SDK downloaded.
#
BackfillTicketThe sdk contains the new backfill ticket (which also appears as json in the backfill API):
The Id, Created, and RecordVersion properties are read-only and should be kept at whatever value they have when a backfill ticket is received via the api or the GetBackfillTickets method. Attempts to set RecordVersion may cause updates to fail, attempts to update the others will simply be ignored.
#
Attributes and PropertiesBackfill tickets look a lot like normal tickets with respect to attributes and properties. However, backfill ticket attributes and properties are fully mutable.
When creating a backfill ticket, take care to give it attributes that reflect the filter used to query the normal matchmaking tickets, as they are intimately related.
An example strategy for writing the attributes for a backfill ticket could just be: loop over the list of filters that was given to the match function, and create attributes that reflect the min value of each filter. This is not a rule, and care should be taken to give backfill tickets attributes that will be matched by as broad or as narrow a scope of queries as desired.
#
ProposalThe proposal class has a new field for backfill tickets (explained in the overview section).
Setting a proposal's BackfillTicket property to a new backfill ticket (with null Id field) will cause a new server to be created if the proposal is approved. An Id for the BackfillTicket will be populated, and the Id will be passed to the server when it is allocated (via session properties).
Setting a proposal's BackfillTicket property to an existing backfill ticket (e.g., one returned via the QueryBackfillTicketsAsync method) will associate the proposal with the existing backfill ticket. If the proposal is approved, the backfill ticket will be updated with the new tickets in the proposal. See section on calling api
#
MatchPropertiesThe MatchProperties class has a new field for backfill ticket id.
When the DGS starts, it should fetch its session data record from the session data service to see what the match function has written there. If the backfill ticket id is not null, this means the DGS was created via a backfill ticket and there are currently tickets waiting for the DGS to approve them. The typical best course of action here would be to immediately call approve on that backfill ticket to reduce the time to match for those tickets. See section on Calling the Session API from the DGS for more details.
#
Example functionsThe matchmaking service comes with several example match functions, which are detailed in the following list. You can use these functions as-is, or you can use them as the basis for your own custom functions.
- SimpleFunction - Groups players together based on the
playercount
attribute on the ticket. Because a single ticket can represent groups of players, this function provides an example of how to group teams into a proposal by grouping team tickets together. - TimeoutFunction - Sets an
AssignmentError
on tickets that have gone unassigned in the configured amount of time. You can use this function as a catch-all to expire tickets and prevent users from waiting indefinitely for a ticket to be grouped into a proposal. - BackfillFunction - A special function for matchmaking directly from the dedicated game server (DGS). You can use this function to backfill players who have lost connection or have quit during the middle of a session, and then fill those spots with new players while the game is running. For more information, see Calling the Backfill API.
- TeamQosFunction - Shows how to use Quality of service (QoS) data from the Multiplay QoS service by using a “top three” method for finding acceptable regions for tickets. This function also includes simple team and party functionality.
#
Building a function in a new .csprojTo build a function in a new .csproj, extract the external
directory from the functions sample file, and then add the files as assembly references to your project.
note
If you build with Newtonsoft.Json, to guarantee that the function loads correctly, compile against 12.0.2 and ensure that you exclude the function from your build output by using <Private>false</Private>
.
#
APIThe Configs and Functions APIs are protected by administrative authorization on the project. To manage configs or functions, the caller must have either Manager or Owner permissions for the project in their Unity organization (for more information, see the Unity documentation on Organization roles. If you are using the command-line interface (CLI), this is prompted and handled for you.
Matchmaking configurations are the instructions for running matchmaking functions. They specify which function to run, which Multiplay server profile to use, and other match-specific parameters that are necessary to run multiple modes, playlists, and match configurations.
#
FunctionsMatch functions are implementations of IMatchFunction
. They are uploaded and deployed pieces of matchmaking logic that are called on during matchmaking cycles.
Match functions require developer authorization.
Function requests are also subject to rate-limiting.
GET /functions
List details about the currently deployed functions.
Response: 200 (FunctionList)
PUT /functions/{function-name}?implementation={impl-name}
Create or update a match function named {function-name}.
Body:
Response: 200, 400
IMPORTANT
When you update an existing function, matchmaking might experience a several-second pause in the current cycle while the new function is spun up.
There is a limit of 20 match functions. When this limit has been reached, an existing function must be deleted before you attempt to add a new function.
GET /functions/{function-name}
Get information about the function named {function-name}.
Response: 200 (Function), 400, 404
GET /functions/{function-name}/log?seconds=1800
Get logs about the currently deployed and running function named {function-name}
. You can set the amount of time that the logs cover by using the seconds
query parameter (this defaults to 1800 seconds).
Response: 200 (FunctionLogs), 400, 404
DELETE /functions/{function-name}
Deletes the function name {function-name}. This action is idempotent.
Response: 204
POST /functions/actions/restart
Schedule a list of functions to restart.
Body: 200 (FunctionNameList)
Response: 200 (FunctionDeployResult), 404
#
Contracts#
FunctionListField | Description | Type |
---|---|---|
functions | The list of functions that are currently deployed. | array(function ) |
function | An object that represents the state of a deployed function, status, and metadata. | Function |
#
FunctionField | Description | Type |
---|---|---|
name | The developer-defined name of the function, which is used in the targetFunction of a Config to run a particular function. | string |
implementation | The name of the class implementing ConfigurableMatchFunction. Can be null if no name was provided on upload | string |
md5Hash | The MD5 of the uploaded file for assurance purposes. | string |
created | The UTC time for when the function was uploaded. | datetime (ISO 8601) |
updated | The UTC time for when the function was updated. This is the same as the created value if it has not been updated. | datetime (ISO 8601) |
status | Status about the currently deployed function resource. | FunctionStatus |
#
FunctionStatusField | Description | Type |
---|---|---|
state | A description of the overall current capability of the function. Possible states include Running , Pending , Failed , Not Running , and Unknown (when the state cannot be determined). | string |
instances | An array of deployed function resources that can be scaled to achieve high-scale needs and zero-downtime deployments of functions. | array |
instance.state | The status of an individual instance. Possible states include Running , Terminated , and Waiting . An instance can be in the waiting state if the scheduling backend is busy or is scaling to meet traffic demands. | string |
instance.name | A unique identifier for keying the instance for debugging, support, and logging. | string |
#
FunctionLogsField | Description | Type |
---|---|---|
logs | A list of the instances of a match function and their associated logs. Note: Logs might contain formatting characters such as \n . | Dictionary<string, string> |
#
FunctionNameListField | Description | Type |
---|---|---|
functions | A list of function names to be restarted. | array(string ) |
#
FunctionDeployResultField | Description | Type |
---|---|---|
functions | A list of functions that have been scheduled for restart. | array(function ) |
function.name | The name of the function. | string |
function.state | The status of an individual restart. Possible states include Pending and Failed . | string |
#
ProblemDetailsWhen a request cannot be successfully completed, an error response is generated. Whenever possible, this response displays in the form of an RFC 7807 ProblemDetails response. For more information, see the IETF documentation on RFC 7807.
For example, requesting a ticket that does not exist returns a response similar to the following example:
A ProblemDetails response is generated in the following scenarios:
Request | Reason | HTTP code |
---|---|---|
Create Ticket | Ticket validation error (invalid ticket contents) | 400 |
Create Ticket | Invalid/missing content-type (Content-Type header must be application/json) | 415 |
Get Ticket | Ticket not found | 404 |
Create Config | Config validation error | 400 |
Update Config | Config validation error | 400 |
Create Function | Invalid function name | 400 |
Create Function | Missing file or zero-length file | 400 |
Create Function | Unsupported archive type | 400 |
Create Function | Exceeded maximum file length | 400 |
Create Function | Exceeded maximum number of match functions | 400 |
Get Function | Invalid function name | 400 |
Get Function | Function not found | 404 |
Get Function Logs | Invalid time range | 400 |
Get Function Logs | Invalid function name | 400 |
Get Function Logs | Function not found | 404 |
Delete Function | Invalid function name | 400 |
Any | Internal service error | 5xx |
Note that the details provided in a ProblemDetails response can be helpful for troubleshooting your issue if it is included in a support request.
#
CLIThe matchmaking function commands in the CLI can be used to simplify managing your match functions. Internally, the CLI uses the REST endpoints discussed above.
#
Beta SDK and samplesThe Match Function SDK is a set of C# assemblies that are included in the external match function samples.
note
The currently supported .NET runtime versions for match functions are 3.0 and 3.1.
The external match function samples include the following DLL files:
The Core SDK assemblies
- Unity.Services.Matchmaking.Functions.Contracts.dll
- Unity.Services.Matchmaking.Matchmaker.Contracts.dll
A helper assembly (see ConfigurableMatchFunction)
- Unity.Services.Matchmaking.Functions.Base.dll
A test simulation library to help with the authoring and testing of match functions
- Unity.Services.Matchmaking.TicketGenerator.dll
#
Thread-safetycaution
In order to maximize your resource utilization, it is possible that your match function is executed concurrently on multiple threads. It is therefore important that your function is written in a way that is thread-safe.
The following example is not thread-safe:
It is not thread-safe because in the case where a different thread is running ExecuteAsync
, both threads will be observing the same list of proposals (_proposals
), make changes to it and can ultimately send back the same exact list back to the matchmaker. In general, you should avoid accessing member variables in a non thread-safe way from async methods.
Here's a better way to write this logic to make each match function run fully independent:
In this example, each function run initializes its own list of proposals making sure no other concurrent thread has the ability to change the data.
#
Contracts#
Match Proposal#
ProposalProperties#
IMatchFunction and ConfigurableMatchFunctionIMatchFunction
is the core interface that needs to be implemented to define match function behavior. However, note that the following helper class can optionally inject and load operational configuration into the function, so it is recommended that you start with ConfigurableMatchFunction<TConfig>
.
#
SamplesThe following match function samples are included. Samples-download
includes a readme
with descriptions of each sample.
- AvoidParties
- ClanAlliance
- ClanAllianceExtended
- Simple
- SimpleBackfill
- Team-Qos
- Timeout
#
Csproj noteFor a function to be loaded properly during the upload process, the uploading .zip file should not contain the Core SDK libraries. For details on this workaround, see the matchmaker release notes.
#
Downloads#
Match Function SDK#
FAQ#
How matchmaker supports parties or groups of multiple playersMany multiplayer games implement methods for groups of players to enter matchmaking together. For example, a method for players to enter the same activity with each other, or a method for players to get grouped onto the same team in team-based games.
With the Multiplay matchmaker, you can support groups of players who are searching for a match together by passing in custom attributes and properties on a ticket and then using those values within a match function.
note
You can use attributes to query the ticket database. Properties are deserialized inside of your match function when you need additional information. For more information, see Tickets.
At a high-level, an example implementation might look like the following process:
- You implement a method for grouping players together (for example, a party service).
- You submit one ticket for the group.
- This is typically done through a service or a "leader" client.
- The ticket contains information about the group, which is stored in the ticket's attributes and properties.
- Your match function uses information in the group tickets to match groups with other groups and players.
- When your match function creates a match proposal, it writes any group-specific data to the proposal properties.
- When the ticket is assigned the match, the service or client that submitted the ticket consumes the match data.
- You communicate to all clients in the group the necessary information for connecting to the resulting match.
Note that during this process you define what a group is, and you create and manage groups outside of the matchmaker. Integrating your group solution with the matchmaker consists of you submitting whatever group data you want as part of your ticket, and then using that data within your match functions.
Figuring out which information is necessary to represent a group in your matchmaking system is an important design decision. As your match function logic becomes more complex, you should consider how that impacts the way that you represent player group data.
The following sections detail some common design concerns around representing groups with the matchmaker.
#
Number of playersIf your matchmaking algorithm is extremely simple, it might only need to understand the number of players on a single ticket. The following examples in the Match Function Samples detail this kind of implementation (encoding the number of players in a playercount
attribute):
- SimpleFunction
- TeamQosFunction
- ClanAlliance / ClanAllianceExtended
#
Group Quality of serviceIf you use client Quality of service (QoS) to determine the region where servers should be allocated from, you need to figure out the best way to determine QoS for a group of players or clients with unique QoS results that could be incompatible with each other.
Consider the scenario of a player in Los Angeles, who forms a group with their friend in London, and then attempts to find a match. These players will have very different QoS results, and could even be completely incompatible (for example, for one of the players, the latency is too high or the loss is too high).
One way to handle this is to perform QoS filtering and aggregation before submitting the group ticket to matchmaking. An example algorithm might look like the following process:
- Remove any completely incompatible or invalid results.
- Sort the remaining results by an algorithm that estimates the best results for the group.
- For example, the root mean square of latency values for each QoS endpoint.
- Include only the top X valid QoS results when submitting the group ticket.
This logic should be run by the player or service that is submitting the ticket to the matchmaker. Although you could put this logic inside of your match function, it would be far less performant than pre-calculating it once.
#
Player-specific dataIf your match function uses player-specific information, consider whether you should include that data for all players on the group ticket or if you can instead use aggregate values.
For example, when matching players by using a skill value, there are multiple aggregation strategies that you could implement:
- Submit the skill of each player in the group as property data
- Use an average skill attribute for the entire group
- Use a combination of min skill, max skill, average skill, and other related attributes for the group
- Use attributes to submit aggregate values, and use properties to submit per-player values
Another example is a class-based game where players can choose a class before starting matchmaking, and there are a limited number of combinations of classes that are allowed in a match. You might represent this with per-player information in your group ticket, or you could use aggregates for the number of players of each class (for example, DPS: 1, Tank: 2, Healer: 1).
Regardless of the data representation that you use, remember to consider the pros and cons of using attributes versus using properties:
- Attributes: Values that you want to use in matchmaking logic to query for tickets (currently limited to
double
values) - Properties: Detailed information about a ticket that helps to find the right match or enables a match function to decide match properties
For more information, see Tickets.
#
Player identityIf you configure your match function to make decisions based on per-player data or embed per-player data in match results, consider whether your group tickets and match function need to embed some kind of player identity.
For example, if your match function is responsible for assigning specific roles to players in a match, you might need a way to tell each player on a group ticket which role they should use.
#
Groups of oneDepending on your implementation, it might be easier to write your logic if you treat every player as if they belong to a group. In this model, single players belong to a group of size 1. Using this method can help you to avoid implementing significantly different logic and data fields for individuals versus groups.
#
Optional cross-platform playProviding an optional crossplay experience is a common requirement for platform holders. Although the matchmaking system enables several approaches for enabling crossplay (including configuring everything in a single match function for small populations), a recommended approach is to use the built-in matchmaker segmentation functionality to help with processing multiple combinations of platforms. This requires adding some attribute
and property
information to your matchmaking tickets.
To add platform information to your ticket, consider adding a platform
enum to your ticket attributes. This makes tickets searchable by platform during segmentation. In addition, a Boolean (currently represented by a double) for crossplay
enables an easier way to automatically isolate non-crossplay players during matchmaking with their respective pools.
The following example ticket has playerCount
, teamSkill
, mode
, crossplay
, and platform
attributes.
The goal is to create the following scenario during distinct match function runs:
- Match functions that run for crossplay-enabled tickets query regardless of platform, but only for tickets with crossplay enabled.
- Match functions that run for crossplay-disabled tickets exclusively query for tickets per platform, which generates several platform-specific match function runs, but includes tickets regardless of whether they have opted in to crossplay. This means that a Platform A ticket with crossplay enabled could still be matched into an exclusive Platform A game, but ensures that you are not splitting the player population into two separate groups.
The following section details some example configs that result in user-optional crossplay for multi-platform games. The examples include teamSkill
to show how other attributes combine with platform attributes.
#
Crossplay function runsThis config generates nine concurrent function runs: mode1_teamSkill0-1000
,mode1_teamSkill1000-2000
,mode1_teamSkill2000-3000
,mode2_teamSkill0-1000
,mode2_teamSkill1000-2000
, and so on.
The crossplay filter guarantees that only tickets that are opted in to crossplay are matched together. Note that the platform
attribute is ignored.
#
Platform-specific function runsThis config generates 54 concurrent function runs:
mode1_teamSkill0-1000-platform1
mode1_teamSkill0-1000-platform2
mode1_teamSkill0-1000-platform3
mode1_teamSkill0-1000-platform4
mode1_teamSkill0-1000-platform5
mode1_teamSkill0-1000-platform6
mode1_teamSkill1000-2000-platform1
mode1_teamSkill1000-2000-platform2
mode1_teamSkill1000-2000-platform3
mode1_teamSkill1000-2000-platform4
mode1_teamSkill1000-2000-platform5
mode1_teamSkill1000-2000-platform6
- ...
mode3_teamSkill2000-3000-platform5
mode3_teamSkill2000-3000-platform6
The platform segment filter guarantees that only tickets on the same platform are matched together. However, the lack of a crossplay filter means that this function's queries also include tickets that are technically opted in to crossplay. In this scenario, players without crossplay can still match with players that are able to use crossplay, which results in more possible matches for players who want to stay on their own platform.
#
Understanding and optimizing time-to-match"Time-to-match" is a metric that measures how quickly your players go from submitting a matchmaking ticket to getting a response back with a match assignment. It is a great measure of player experience, and can have an impact on player retention.
An ideal matchmaking system operating in ideal conditions can quickly create high-quality matches. However, under real-world conditions, there are many factors that can affect time-to-match and match quality. You must determine strategic ways to balance match quality and time-to-match.
The following sections detail factors that influence time-to-match.
#
The minimum round-trip through the matchmaking serviceThe following process details a simplified view of the parts of the matchmaking flow that impact time-to-match:
- A client submits a ticket (POST to tickets endpoint).
- A match function runs and creates a proposal with that ticket.
- The proposal is approved and a new server is allocated (or a backfilling server is selected).
- The client gets the completed match assignment (GET to tickets endpoint).
The real-world absolute minimum time-to-match for a ticket is generally around 1 second. Achieving this match time requires the following conditions to be met:
- The player has low latency to the datacenter that is hosting the matchmaker.
- A match can immediately be created for the player.
- The player is assigned to a backfilled match.
- The player polls the ticket's GET endpoint to pick up the match right after it is assigned.
Note that actual real-world match times can vary widely from this ideal, and generally take several seconds due to a number of variables, such as those detailed in the following list:
- The amount of time that it takes for your client to POST a ticket
- This is calulated as client latency to the datacenter + the system time to ingest the ticket
- The amount of time that it takes for your match function to run
- This is calulated as your function run time + ticket query time + system overhead
- Function run window time
- A variable time window where the system waits for all scheduled functions to run; the minimum time is currently ~300ms and the maximum is ~3000ms
- Tickets are cached between windows to ensure deterministic queries across functions, and new tickets must wait for the next window until they are included
- Whether there are enough tickets available to meet your function's requirements to immediately create matches
- For example, if you need 8 players, but only have 5, you have to wait until you have enough players to make a match
- The amount of time that it takes to assign a connection or server to the match after it is created
- Backfill takes ~0 seconds, and matches that are assigned to new servers take ~1-2 additional seconds to allocate a server for the match
- The amount of time that it takes for the client to get their match assignment after it is created
- This is also referred to as the delay between when the match is ready and when the client checks to see if the match is ready
You can control some of these variables, such as those detailed in the following list:
- Your match function's run time
- Your minimum requirements to create a match
- Your client's GET polling rate
Remember that you are generally not making matches in an ideal scenario, so you must consider how to balance your requirements versus your time-to-match. In certain scenarios, you might want to intentionally increase your minimum time-to-match.
#
Match function run timeA match function can take variable amount of time to run, depending on the complexity of your match function logic and how intensive the operations are that you perform within a match function. Match functions that take longer than ~250ms to run could impact on your time-to-match. Match functions that take longer than ~3000ms to run are assumed to be dead, are force stopped, and their results are ignored.
In general, match functions tend to have Big O performance between O(n) and O(n^2), where n is the number of tickets in the system. Keeping your match functions efficient under load is an important factor in keeping your match times low. For more information, see Optimizing function performance.
#
Minimum requirements for creating a matchFor most developers, the minimum requirements for creating a match is where the vast majority of your time-to-match is determined. The time impact of this part of matchmaking is defined by how easy it is to find the minimum number of compatible players to form a match.
In practice, this means that as the number of compatible players you can choose from (the player pool) increases, and the number of players that you need to form a match (the game size) decreases, the faster your matchmaking can be with real-world traffic.
If you have very narrow and specific criteria for how players can match with each other, it might take a long time to find a quality match. If the time-to-match is too long, your players might give up or they could time out. However, if you only have a few requirements for matching players together, your players might get frustrated at being placed into "poor quality" matches.
#
Example: A 4v4 game using QoS and player skillTo consider a real-world example, say that you have a very simple setup where you want to match players into games based on Quality of service (QoS) region and player skill, and you want to create 4v4 matches with "fair" teams. In this scenario, you could wait to create a perfect 4v4 game where all players are in their best QoS region and have skill ratings that will create a game with a roughly 50/50 chance of either team winning.
However, what if there aren't that many compatible players available? How long will you wait to create that match before giving up (and timing out)? How long before your players give up and quit? Should you put players in less optimal games if it means that they can at least play more quickly? How do you balance time-to-match versus match quality? These questions are some of the most important design questions when configuring matchmaking. How you design solutions to these issues has a large impact on match quality and time-to-match.
A common approach for handlign these scenarios is to change your requirements based on the time a ticket has spent in matchmaking. This could include switching from an "optimal" matchmaking strategy to a "non-optimal" strategy, depending on the age of a ticket. In the 4v4 example, consider the following factors:
- An "optimal" match might have criteria such as being in a ticket's preferred QoS region, being a full game, or having an excellent skill match.
- A "non-optimal" match might be outside of the ticket's preferred QoS region, could be partially full, or could be a bad skill match. You define what criteria are important.
This is a scenario where it is recommended that you use multiple match functions or multiple function configs to simultaneously run your optimal and non-optimal matchmaking algorithms. You can configure your "optimal" match function to build the best possible matches at all times, and configure your "non-optimal" function to query for tickets older than N seconds and then make non-optimal (but still acceptable) matches. When tickets overlap, you can use the Score field for your match proposals to ensure that the best proposal wins.
tip
There are many ways in which you can set up your match function or multiple match functions to balance the creation of optimal and non-optimal matches. Consider the ways in which you can go beyond a two-phase system. For example, you could add more phases, or change your match parameters at different rates from each other.
#
Client GET polling rateIt is important to keep the rate at which a client polls for a match assignment balanced. If you poll too quickly, you can overwhelm the system at scale or get rate-limited. If you poll too slowly, your time-to-match rises.
It is recommended that you consider the following poll rates:
- Wait 1-2 seconds after POSTing a ticket before you being polling (this is because it generally takes a minimum of one second for a match to be made after a ticket is submitted)
- Wait 1+ seconds between GET polls
In a worst-case scenario, in which the last poll missed the match assignment, time-to-match is increased by your approximate wait time between GET polls, and the average scenario should be approximately half the worst-case scenario. For example, if you poll once every 2 seconds, your worst-case scenario increases your time-to-match by ~2s, and your average scenario increases your time-to-match by ~2s.
#
Intentionally increasing your minimum match timeIn some scenarios, is can be beneficial to intentionally increase your minimum time-to-match.
For example, you might want to increase your minimum time-to-match to create the following scenarios:
- Drive more tickets to backfill instead of new matches
- Wait for enough players to create a full match
- Wait for enough compatible players to make a high-quality match
#
Waiting for a full matchIn scenarios where a full match is preferred, but it could take a long time to create one (such as when playing in low-popularity game modes or in developer or QA tests), you might want to implement ways to intentionally slow down matchmaking to allow these full games to form. Having configurable values for criteria such as minimum players required
, time before creating a minimum player match
, and time before ticket timeout
allows you to create matchmaking configurations that can wait much longer than normal for a full game to be ready.
#
Waiting for a high-quality matchIn some scenarios, a developer might value speed over quality, and in other scenarios, they might value quality over speed. Both of these strategies are valid. Sometimes, intentionally waiting a long time for a "good match" might be preferred or required.
Consider a game that supports both "unranked" PvP and "ranked" PvP modes. A developer might want to configure their unranked PvP matchmaking to be fast, and then start reducing match quality based on how long players are waiting in matchmaking to get players into a game faster. However, in their ranked PvP mode, the developer has decided that playing a fair match is the most important consideration, and they refuse to make matches that would be unfair. In this scenario, the ranked matches will probably take much longer to make than the unranked ones.
In addition, consider an official online tournament example, with prizes that have real-world value. In these types of events, cheating can be rampant, so spending extra time to ensure a good match can help to avoid player dissatisfaction.
#
Optimizing function performanceMany matchmaking algorithms tend to fall into a > O(n)
complexity, and can often hit O(n^2)
or worse.
For example, a very simple matchmaking algorithm might start by picking the first ticket in the list of all available tickets, and then iterate through the list, choosing tickets that are compatible with the first ticket until a full match has been made. This algorithm is O(n^2)
, because in a worst-case scenario, no tickets are compatible with each other, which results in comparing every ticket to every other ticket.
If your design requires an O(n^2)
algorithm to create the kinds of matches that you want, optimization becomes very important. The following list details tasks that you can perform to help reduce the real-world computational requirements:
- Reduce
n
as much as possible - Make your algorithm closer to
O(n)
by reducing the number of comparisons that are required - Reduce the overall complexity or amount of computation that is required per
n
For examples of optimization, see the match function sample project.
The following sections also provide more information about optimization methods.
#
Benchmark your functionsThe match function simulation project includes a basic test harness for generating a large amount of traffic and running your function against that traffic.
When developing your function, try testing it against different simulated traffic loads to see how the performance is impacted. For example, is it O(n)
or O(n^2)
? How many tickets are needed to consistently make your function take more than 2-3 seconds to run?
#
Scale test your matchmaker before you launchTesting locally is a good way to compare the performance of different approaches. However, your local PC test environment does not perfectly mirror your real matchmaker environment. The best way to ensure that you are ready to hit your scale targets is to work with the Multiplay team to run scheduled scale tests against your real matchmaker by using your custom match functions.
#
Bucket tickets by must-match attributes and propertiesReducing the number of tickets that each match function run operates on (the n
in O(n)
or O(n^2)
) is the quickest way to improve performance at scale.
Start by identifying must-match filters that can naturally be used to pre-bucket tickets into the smallest possible groups that can match together. A "bucket" is a group of tickets that all match some criteria. For example, you might bucket tickets by game mode, environment, and build. Depending on your design, you might be able to bucket by attributes, properties, or both.
If you have attributes that you can bucket on, and you know the buckets that you want to segment them into ahead of time, consider using the config segmentation system. This automatically performs bucketing for you by running your function multiple times, each with a different segment passed in. If you have multiple filters with defined segments, your function is run based on a matrix of all defined segments. For example, if you have four game modes, three environments, and two builds specified with segmentation, your function is run (4 3 2 = 24) times, which is one for each possible combination of segment values. Because the config segmentation system is a system-level optimization, using it produces a better parallelization of runs than when you manually segment within your match function code. However, if you need to discover the value of an attribute at runtime to bucket on it, you need to use your match function code. In addition, if you have buckets that you want to make from ticket properties, you have to use your match function code.
Note that there is still value in bucketing attributes and properties even if you cannot use the segmentation system, because it still creates a smaller search space (n
in O(n)
) for your match algorithm to work with.
#
Algorithm optimization strategiesBrute-forcing every single possible match for a set of tickets and determining which one is best is a computational worst-case scenario (O(n!)
). For example, if you have 100 unique tickets, and you want to evaluate every possible 8 player game, that results in 7,503,063,898,176,000 possible games. If you could evaluate 1 million combinations per second, it would take 238 years to try every possibility.
Because finding an optimal match by using brute force is not feasible, consider which kind of algorithms you can write that will work "good enough" to keep players happy. Some variance in the matchmaking process can keep the game experience more interesting than always playing a perfectly even "50/50" match.
When configuring matchmaking algorithms, consider the following best practices:
- Use what you know about your game to reduce your search space by bucketing tickets.
- Always bucket your tickets by must-match attributes and properties .
- Consider using additional buckets to group together tickets that share common features. For example, if your game has character classes and you need to create balanced teams with X players of each class, you probably want to bucket players by class so you can easily find only the players of the class you need at any given time.
- Consider whether pre-sorting your tickets by certain attributes can speed up your algorithm.
- For example, an
O(n log n)
sort followed by anO(n)
matching algorithm is faster thanO(n^2)
at scale.
- For example, an
- Use ranges or tolerances to define acceptable differences between players and teams.
- For example, "A match is acceptable as long as the skill difference between teams is < X".
- This allows you to focus on making "acceptable" games, which can be faster than making the "best possible" games.
#
Example: A simplified optimization strategy for a skill-based matching algorithm with tolerances- Initially bucket tickets by must-match attributes and properties, such as Quality of service (QoS) or game mode (
O(n)
). - Bucket the remaining tickets by their number of players (
O(n)
), and then sort those buckets by skill (O(n log n)
). - When building a match, start with the tickets with the highest number of players and highest skill, and then start trying to build matches. As you build matches, iterate through each sorted bucket of players, pulling in compatible players to the match that is being made (
O(n)
). - When a match can be completed, turn it into a proposal and then begin building the next match.
Although this algorithm is not perfect, it is an example of an O(n log n)
algorithm, because it performs an O(n log n)
sort followed by an O(n)
matching algorithm.
#
Limit logging in productionLogging is computationally expensive at scale, which is true regardless of the system or technology.
Verbose logging during development, testing, and small playtests can be useful, but it can become a major performance bottleneck when running against production-level ticket loads, especially if you are logging inside of O(n^2)
matchmaking logic.
Accordingly, it is recommended that you put your more verbose logs behind a flag that you can toggle in your function or function config.
A small number of logs per function run are generally harmless, but logging multiple times per ticket or ticket comparison (which can scale to O(n^2)
or worse) can dramatically affect your function performance and time-to-match.
#
Limiting the n in O(n)In addition to using bucketing strategies to reduce the number of tickets that your matchmaking algorithm has to process in any given run, you might want to put a hard limit on the number of tickets.
When you understand the approximate number of tickets where your function performance is no longer usable (for example, run time is consistently > 3000ms), you might want to put a hard limit on the number of tickets that the function can process.
#
Limiting your function run timeFunction results are discarded if the function run time is greater than 3000 ms. Because of this limit, you might want to add a timer inside of your function logic and use it to help ensure that you at least report the matches that you were able to generate in that time period. To do this, you should try setting your max function runtime to ~2500ms. Note that this only works well if you are able to consistently create at least some matches during that time frame.
#
How to use the Visual Studio 2019 debugger if you receive compile errors using the com.unity.ucg.qos packageSome users of the Unity Quality of service (QoS) package in combination with Visual Studio Tools for Unity and Visual Studio 2019 might get compile errors in Visual Studio 2019 that do not occur in the Unity editor. The specific error that might be seen is: CS8385 "The given expression cannot be used in a fixed statement"
.
This issue has been confirmed as a bug in the VS/C# compiler integration. The project successfully builds in the editor, but has this error in Visual Studio 2019. If you are seeing this issue, it is recommended that you use either of the following workarounds until the bug has been resolved:
- Use
Debug / Attach Unity Debugger
from Visual Studio 2019 instead of using thePlay
button. This does not require that the project builds successfully in Visual Studio 2019 before attaching to the debugger. - In Visual Studio 2019, set
Tools / Options / Tools for Unity / Disable the full build of project
tofalse
. In this scenario, the user still sees the error in the code, but the project should build successfully.