Introduction

GameSparks MatchMaking is a very flexible and powerful feature. It incorporates a number of complex features like Real-Time servers and Matchmaking Scripts. These features may not be available for alternative platforms, so in this topic we are going to deal with two fundamental components needed to transition your existing Matchmaking feature.

Thresholds

The basic Matchmaking config in GameSparks consists of a min and max set of players you would like to match with and a set of thresholds.

These thresholds are used to create conditions upon which matchmaking decisions can be made.

In GameSparks, these thresholds are pretty simple and are controlled by a single parameter called “skill” which is passed in through the MatchMakingRequest. Alternative platforms have similar functionality for their Matchmaking, though with different setup and API calls to enter a player into Matchmaking.

Matchmaking API

Oftentimes Matchmaking needs additional functionality in order for the feature to comply with the designer’s needs. There are a number of ways to do this discussed in the following section, but one way is with the SparkMatch API.

SparkMatch allows developers to control an existing instance of a match, add or remove players or edit the match-data payload. This is an extremely powerful tool to create custom match features however, it is not present for many alternative platforms. Where possible we will demonstrate workarounds for this.

Other Features

As mentioned above, the SparkMatch API can be used to control and extend the matchmaking feature, but it is also possible to add custom context to the match outside of the “skill” value passed into the MatchMakingRequest. You can do this with the “participantData” field, for example, matching only players from a specific region.

{
  "@class": ".MatchmakingRequest",
  "participantData": {
    "region" : "eu"
  },
  "skill": 0
}

This may not be possible in all alternative platforms but we will cover it where possible.

Matchmaking Scripts

Where an even more complex set of Matchmaking rules is required you might be using matchmaking scripts to manipulate player and match data during matchmaking. This is a very complex feature that is not common on other platforms. Where there is some overlap between this feature and the destination platforms matchmaking offering we will show examples, however, something to keep in mind is that where these platforms offer a “cancel matchmaking” API, you can do a lot of this work client-side by setting up custom timers that will cancel a matchmaking request after a given period of time and then issue another matchmaking request with different params

GameLift FlexMatch

If these alternative platforms do not offer a solution to your existing Matchmaking configuration it is worth checking out AWS FlexMatch. FlexMatch offers a matchmaking service with a high-degree of configurability. In comparison with GameSparks, matchmaking criteria are designed through a script which allows you to add custom data to the match-instance and the player’s individual matchmaking ticket. You can also change criteria over time as with GameSparks thresholds and matchmaking scripts.

You can get more information on FlexMatch here and you can see an example of how to create a serverless implementation of the service here.

Beamable

Beamable has a relatively straightforward matchmaking feature compared to GameSparks, however, there is a good deal of custom-code in order to set it up, so we will cover that in this topic.

Currently, Beamable only offers a basic matchmaking service. You cannot match by specific values or thresholds, like you can with GameSparks. Instead, everyone who is looking for a particular type of match is grouped together by default.

There is a guide here on their matchmaking functionality and this is also demonstrated in their example game here. We will cover the basics needed anyway so you have further information as to how this feature compares to GameSparks.

Note - This feature is still in development by Beamable and is actively being worked on to add features like thresholds and skill matching like you find in GameSparks. Check with Beamable to see what updates have been made to this feature since we completed this topic.

Game-Types

The first thing we need to start with is creating a new Game-Type content object. You can do this by going to the Content Manager window and right-clicking on the “game_types” menu option.

For this example the configuration is pretty simple, we don't need any rewards or leaderboard updates for this game-type, we just need a max-players (2 in the case of our 1v1 match) and a max wait duration after which the matchmaking will stop.

This SimGameType content object is going to act as the matchmaking options for the matches we want to create. We will use these in the next section.

You can see a few other attributes on this game-type object. “Min Players To Start” is the same as the minPlayers attribute in GameSparks matches. “Wait After Min Reached Secs” acts like the “Accept Min. Players” option for GameSparks thresholds. This is the number of seconds after which the match will revert to accepting the minimum number of players if there are no other players found.

There are two other options, Leaderboard Updates and Rewards. These are used by Beamable’s multiplayer service and can be ignored for matchmaking in this case.

Matchmaking API The next step is to create a matchmaking API which can control our matches. This is pretty simple. It need to be able to:

  1. Take the match-type (the SimGameType we created above)
  2. Take a callback for match updates
  3. Take a callback for match complete
  4. Request matchmaking
  5. Cancel matchmaking
  6. Get a list of all other players in the match upon completion

This is roughly the same flow as GameSparks, but in Beamable it requires some customization.

GSMatchResult

The first thing we need is to create a class to represent the data we need out of a successful match. In GameSparks this is pretty simple, we need a matchId and a list of players and their Ids. In this example we aren't going to create a matchId because this is not automatically generated by Beamable, however, you could create a temporary group or room out of the match and give the match that ID if necessary. The other two attributes that can be useful is the target number of players and the remaining time.

Using this information you can match useful decisions as the match progresses.

/// <summary>
/// This is the object that will be returned from the match
/// </summary>
public class GSMatchResult
{
  /// <summary>
  /// List of playerIds for the players in the match
  /// </summary>
  public List<long> PlayersIds = new List<long>();
  /// <summary>
  /// The number of players required to match a match
  /// </summary>
  public int TargetPlayerCount;
  /// <summary>
  /// Remaining seconds in match
  /// </summary>
  public int SecondsRemaining;

  /// <summary>
  /// Creates a new instance of a match response used throughout matchmaking
  /// </summary>
  /// <param name="targetPlayerCount">The target number of players to complete a match</param>
  public GSMatchResult(int targetPlayerCount)
  {
     TargetPlayerCount = targetPlayerCount;
  }
}

Note - We are creating a simplified version of the code-examples in the example game here. Check that example out for more details.

GSMatch

Now we can create the class that is going to handle all the matchmaking and callbacks. This is not too difficult but there are several parts that need to be considered.

  1. We need a loop which can keep checking for match updates at regular intervals. We have set this interval to 1 second which is usually fast enough for most games. This update process is going to be a new thread.
  2. We need to be able to cancel this thread and therefore cancel matchmaking for our player.
  3. We need to be able to raise a callback for updates (like new players joining the match), matchmaking completed (we found all the players we need) or the matchmaking process timed out.
/// <summary>
/// This is the match object where you can start the matchmaking process or cancel it
/// </summary>
public class GSMatch
{
  // Event callbacks
  public event Action<GSMatchResult> OnProgress;
  public event Action<GSMatchResult> OnComplete;
  public event Action<GSMatchResult> OnTimeout;

  private GSMatchResult _matchResult;
  private GSMatchResult _gsMatchResult;
  private MatchmakingService _matchmakingService;
  private SimGameType _simGameType;
  private CancellationTokenSource _matchmakingOngoing;

  /// <summary>
  /// Creates a new instance of the match
  /// </summary>
  /// <param name="matchmakingService"></param>
  /// <param name="simGameType"></param>
  public GSMatch(MatchmakingService matchmakingService, SimGameType simGameType)
  {
     _matchmakingService = matchmakingService;
     _simGameType = simGameType;
     _gsMatchResult = new GSMatchResult(_simGameType.maxPlayers);
  }

  /// <summary>
  /// Kicks off the matchmaking process
  /// Updates will be delivered using the event callbacks
  /// </summary>
  public async Task RequestMatch()
  {
     var handle = await _matchmakingService.StartMatchmaking(_simGameType.Id);
     try
     {
        _matchmakingOngoing = new CancellationTokenSource();
        var token = _matchmakingOngoing.Token;
        do
        {
           if (token.IsCancellationRequested) return;

           // Check if a new player has joined or left the match //
           if (handle.Status.Players.Count != _gsMatchResult.PlayersIds.Count)
           {
              _gsMatchResult.PlayersIds = handle.Status.Players;
              _gsMatchResult.SecondsRemaining = handle.Status.SecondsRemaining;
              OnProgress.Invoke(_gsMatchResult); // raise the progress update callback
           }

           // Tick down the matchmaking progress //
           MatchmakingUpdate update = new MatchmakingUpdate();
           update.players = handle.Status.Players;
           update.secondsRemaining = (handle.Status.SecondsRemaining-1);
           handle.Status.Apply(update);

           if (handle.Status.SecondsRemaining <= 0)
           {
              OnTimeout.Invoke(_gsMatchResult);
              await CancelMatchMaking();
              return;
           }

           await Task.Delay(1000, token);
        }
        while (!handle.Status.MinPlayersReached);
     }
     finally
     {
        _matchmakingOngoing.Dispose();
        _matchmakingOngoing = null;
     }
     // Invoke Complete //
     OnComplete.Invoke(_gsMatchResult);
  }

  /// <summary>
  /// Cancels the matchmaking process
  /// </summary>
  public async Task CancelMatchMaking()
  {
     await _matchmakingService.CancelMatchmaking(_simGameType.Id);
     _matchmakingOngoing?.Cancel();
  }
}

The important part of the class above is the RequestMatch() function. You can see where it is checking for a change in the player-count indicating someone has joined the match. It is also ticking down the match progress and checking if the match has ended.

Now that we have our request and response objects mocked up, we can look at an example of how to kick off matchmaking. For this example we will show an async method which you could call from a button click or anywhere else in your code.

async void StartMatchMaking()
{
   Debug.Log("Starting Matchmaking...");
   // get the game-type content for this match //
   var gameType = (SimGameType)await beamableAPI.ContentService.GetContent("game_types.1v1");
   Debug.Log($"GameType: {gameType.Id}...");
   // Now we can create our match object //
   GSMatch newMatch = new GSMatch(beamableAPI.Experimental.MatchmakingService, gameType);
   // Some examples of these matchmaking callbacks //
   // timeout callback //
   newMatch.OnTimeout += delegate(GSMatchResult result)
   {
       Debug.Log("Match Not Found...");
   };
   // progress callback - called when anything in the match is updated //
   newMatch.OnProgress += delegate(GSMatchResult result)
   {
       Debug.Log("Match Updated...");
       // How many players do we have atm //
       Debug.Log($" {result.PlayersIds.Count} / {result.TargetPlayerCount} Players...");
       Debug.Log($" {result.SecondsRemaining} Seconds Remaining...");
       foreach (long playerId in result.PlayersIds)
       {
          Debug.Log($"PlayerId: {playerId}");
       }
   };
   // Found Match callback //
   newMatch.OnComplete += delegate(GSMatchResult result)
   {
       Debug.Log("Match Found...");
       foreach (long playerId in result.PlayersIds)
       {
          Debug.Log($"PlayerId: {playerId}");
       }
       // >> use this player list to create a room or a game session //
   };
   await newMatch.RequestMatch(); // << Kick of matchmaking
}

You’ll need more than one player to test this process, but if you do kick off matchmaking for both players you should see the OnProgress and OnComplete callbacks being triggered and the logs appear in the console.

As mentioned already, from here you would want to do something with your player Ids like pass them to your multiplayer service.

Obviously if you are transitioning from GameSparks to Beamable you will have your own multiplayer implementations. Those playerIds should be sufficient to get your players connected but if you do need a common Id between players like a matchId you could consider creating a temporary group, or even a temporary stat which you can apply to all players using an OID generator.

AccelByte

AccelByte has a pretty robust matchmaking feature which allows you to create something like GameSparks’ thresholds along with more complex matchmaking rules.

Something that is important to explain about AccelByte’s matchmaking feature is that it is designed around their Lobby and Party feature. This can make it somewhat confusing when trying to understand which feature comes first in your implementation. We will cover these features in this topic with a simple player-to-player matchmaking example.

Something else to note is that AccelByte’s matchmaking feature uses party-to-party matching. In contrast to GameSparks, this means that you match groups of players to other groups of players instead of players to player.

This is a great feature and opens up matchmaking to a lot more possibilities, however, it can come across as though you aren't able to perform simple player-to-player matchmaking. This is not the case, you can get around this by creating a party with a single player and starting the matchmaking process that way. We will show an example of this process in this topic.

Important - Before we continue it is important to note that while AccelByte has a very good matchmaking feature with a number of similarities to GameSparks, it is designed to be incorporated with their multiplayer service. It is not necessary to do this as you will see below, but it does mean that you cannot get a list of players in your match without some external tracking. This may rule AccelByte matchmaking out as a possible transition solution.

The Lobby Service

In Accelbyte, the Lobby Service is a set of APIs that integrates with a number of other social features. The name of this feature can be somewhat misleading as it assumes that a player can set up and join different lobbies where they can wait until a game-session starts and possibly also chat with other players while they wait.

The Lobby Service is actually global. When you connect to the Lobby Service it allows you to connect to or create parties and you can start Matchmaking from there.

The Lobby Service is also used as part of the Groups Service to get updates about a Group like when a player has joined or sent an invite. It is basically a way to subscribe to different components that need asynchronous updates or messages similar to GameSparks web-sockets. The Lobby Service itself is actually using web-sockets.

This is important because conceptually, what you would consider to be a “Lobby” is more closely comparable to AccelByte’s party feature. The Lobby Service is the service which facilitates all this asynchronous messages and updates like parties, group-notifications and matchmaking notifications, etc.

We therefore start with the Lobby Service before we can set anything else up.

Setup Lobby Config

We need to configure the Lobby Service before connecting to it. We can configure it from the Admin Portal.

In Admin Portal, we should go to Lobby Configuration under the Lobby and Matchmaking category as shown below.

As you can see, we are able to choose to auto-kick players when they are disconnected and specify the number of players that should be in a party.

And that’s it! We are ready to start using the Lobby Service.

Joining The Lobby Service

We can connect to AccelByte’s Lobby Service with just a simple API call as shown below.

private Lobby abLobby;

abLobby.Connect();

We have events to include some functionality when momething happens in AccelByte. For example, once we are able to connect to the Lobby service then create a party. We will see a sample example to demonstrate events.

abLobby.Connected += OnConnected;

private void OnConnected()
{
        Debug.Log("Connected to the AccelByte’s Lobby service");
}

Parties

As mentioned before, with AccelByte, we need to create parties in order to make use of the Matchmaking Service. We can create parties with the help of Lobby Service because we have already mentioned Lobby Service includes social features.

Creating A Party

Below is an example of how to create a party from the SDK.

private Lobby abLobby;
abLobby.Connect();

abLobby.CreateParty(CreatePartyCallback);

private void CreatePartyCallback(Result<PartyInfo> result)
{
        if (result.IsError)
        {
            Debug.Log($"Error. Code: {result.Error.Code}, Reason: {result.Error.Message});
        }
        else
        {
            Debug.Log("Successfully created a party");
        }
}

Therefore, if you wish to replicate something like a GameSparks MatchmakingRequest you can always create a single-user party immediately after connecting to the Lobby Service as shown below.

private Lobby abLobby;
abLobby.Connect();
abLobby.Connected += OnConnected;

private void OnConnected()
{
        Debug.Log("Connected to the AccelByte’s Lobby service");
        abLobby.CreateParty(CreatePartyCallback);
}

private void CreatePartyCallback(Result<PartyInfo> result)
{
        if (result.IsError)
        {
            Debug.Log($"Error. Code: {result.Error.Code}, Reason: {result.Error.Message});
        }
        else
        {
            Debug.Log("Successfully created a party");
        }
}

Addition Party Functionality

The AccelByte Party feature has more functionality than we will not cover in this topic. You can read about those here.

Below are the other API calls available for Party service in AccelByte.

These are similar to the functionality available from the Groups/Teams feature which we covered in a topic here, however, they are different and parties should be treated more like a lobby where players can be grouped together before starting a game-session.

Matchmaking

The next step in order to get these parties into matches is to configure the Matchmaking Service in the Admin portal or through REST calls. We are not going to cover REST calls in this section as it is easier to demonstrate this setup through the portal.

Before we do that, we need to have at least one Stat configured as a prerequisite to the Matchmaking service.

Matchmaking Statistics

The stat used by the Matchmaking Service is essentially like the “Skill” field that GameSparks uses for its matchmaking. The difference here is that the stat is not passed into the request, but is instead handled server-side as it exists on the player’s account.

We have already covered how to create a stat with Statistic service in the Leaderboard topic available here here.

We will use LEVEL as the stat for this example.

Matchmaking Configuration

From the Admin portal, navigate to the Matchmaking Ruleset section under the Lobby and Matchmaking category.

In the Matchmaking window, we need to click on the Add Configuration button on the right hand side of the window.

This will bring up a small popup window where we need to supply valid information regarding matchmaking configuration.

The form above is an example of how to configure this match for 1v1 matches, but you can have any different game modes you want.

Rulesets

The next step is to set up some Rules Sets.

Rule sets are similar to GameSparks thresholds as they define a set of rules which control what values of the stat are acceptable when forming the match.

Similar to thresholds, you can set multiple rules which can be configured to change over time. “Distance” defines the relative value of the stat, similar to GameSparks, however, that is the only option you have as there is no percentage option.

With AccelByte there are two rules-sets you can choose from. The Flexing Rules option at the bottom is configured the same as the Matchmaking Rules above it, however, the flexing rules can be configured to kick-in when the service is having trouble finding a match for the player.

Something important to note is that you need to include the StatCode that we have created earlier.

Starting The Matchmaking Process

Only a party leader can start the Matchmaking process. This is usually done after players join the party according to the game-mode configuration.

Or you can simply start the process with the help of the below API call.

abLobby.Connect();
var channelName = "1vs1";
abLobby.StartMatchmaking(channelName, StartMatchMakingCallback);

/// <summary>
/// Callback to know the status of the matchmaking process
/// </summary>
private void StartMatchMakingCallback(Result<MatchmakingCode> result)
{
        if(result.IsError)
        {
             Debug.Log($"Error. Code: {result.Error.Code}, Reason: {result.Error.Message});
        }
        else
        {
            Debug.Log($"MatchMakingCode : {result.Value.code}");
        }
}

Here, the channelName field is the name of the Ruleset we just created.

If we received the code “0” in the response enum it means our request is successfully received and passed to the matchmaking queue.

Below is an example of our 1v1 match config, along with the previous example of how to create a 1-player party so that we can immediately kick off matchmaking for a 1v1 game.

private Lobby abLobby;
abLobby.Connect();
abLobby.Connected += OnConnected;

/// <summary>
/// Callback when we have successfully connected to the Lobby Service
/// </summary>
private void OnConnected()
{
        Debug.Log("Connected to the AccelByte’s Lobby service");
        abLobby.CreateParty(CreatePartyCallback);
}

/// <summary>
/// Callback when we have successfully created a party
/// </summary>
 /// <param name="result">Party information like party leader, members. etc<param>
private void CreatePartyCallback(Result<PartyInfo> result)
{
      if (result.IsError)
      {
          Debug.Log($"Error. Code: {result.Error.Code}, Reason: {result.Error.Message});
      }
      else
      {
         var channelName = "1vs1";
         Debug.Log("Successfully created a party");
         abLobby.StartMatchmaking(channelName, StartMatchMakingCallback);
      }
}

/// <summary>
/// Callback to know the status of the matchmaking process
/// </summary>
private void StartMatchMakingCallback(Result<MatchmakingCode> result)
{
        if(result1.IsError)
        {
             Debug.Log($"Error. Code: {result.Error.Code}, Reason: {result.Error.Message});
        }
        else
        {
            Debug.Log($"MatchMakingCode : {result.Value.code} ");
        }
}

MatchFound Callback

We need some way of getting updates about the progress of the match. There is an optional callback we can configure for getting matchmaking results when the match has been found.

abLobby.Connect();
abLobby.MatchmakingCompleted += OnMatchMakingCompleted;

/// <summary>
/// Notification to inform matchmaking process is completed
/// </summary>
/// <param name="result"> matchmaking process status and matchId</param>
private void OnMatchMakingCompleted(Result<MatchmakingNotif> result)
{
      if (result.Value.status == "done")
      {
            Debug.Log("Match found");
      }
}

Using Matchmaking Callbacks

Ideally you can use these callbacks to perform some custom logic when a match has been found. In GameSparks this callback would contain the matchId along with a list of players or playerIds you can group together. In AccelByte you will get a matchId but you cannot get the list of players.

You will therefore need to create your own solution for getting these players and their Ids. This would need to be a custom solution so check out the tutorial on Cloud-Code here for more information.

Cancel Match

Finally we will show how a player can cancel the Matchmaking process once it has been started. We can simply call the below API call to do the same.

 var channelName = "1vs1";

 abLobby.CancelMatchmaking(channelName, result =>
{
     Debug.Log(string.Format("Cancel matchmaking response {0}",result.Value.code));
});

We have started the matchmaking process and immediately cancelled for demonstration purposes with the above call and below is the response received. When we receive a code ‘0’, generally, it means that the process is terminated.

Nakama

Nakama has a very flexible matchmaking system which allows you to make complex queries on your matchmaking parameters. This begins with a set of matchmaking Properties which are sent to the server using an equivalent to the GameSparks MatchmakingRequest.

Let's take a look at a GameSparks Match configuration. You can get a copy of your existing match configurations using the GameSparks REST API.

{
  "@id": "/~matches/bomber_man",
  "description": "bomber_man",
  "dontAutoJoinMatch": false,
  "dropInDropOut": false,
  "dropInDropOutExpireSeconds": null,
  "maxPlayers": 4,
  "minPlayers": 4,
  "name": "bomber_man",
  "playerDisconnectThreshold": null,
  "realtime": false,
  "realtimeScript": null,
  "script": null,
  "shortCode": "bomber_man",
  "~thresholds": [
   {
   "@id": "/~matches/bomber_man/thresholds/10",
   "acceptMinPlayers": false,
   "max": 1,
   "min": 10,
   "period": 10,
   "type": "ABSOLUTE"
   }
  ]
}

In the portal, this match would look something like this.

Let us take a look at how we could recreate this match config using Nakama’s Match Properties.

In GameSparks we create a match config through the portal and then use the match short code to let the server know the settings we want for the match. We then submit a matchmaking request along with the “skill” value.

In Nakama, the match config (Match Properties) are defined in the client and submitted in a matchmaking request.

The GameSparks example above in Nakama would look like this in Unity.

/// <summary>
/// Submits a matchmaking request to Nakama with the given values
/// </summary>
/// <param name="minPlayers"></param>
/// <param name="maxPlayers"></param>
/// <param name="skill"></param>
private async void SubmitSimpleMatchRequest(int minPlayers, int maxPlayers, int skill)
{
   Debug.Log($"Submitting Matchmaking Request | min:{minPlayers}, max:{maxPlayers}, skill:{skill}");
   string query = "+properties.skill:>=1 +properties.skill:<=10";
   Debug.Log($"Query: [{query}]");
     var numericProperties = new Dictionary<string, double>() {{ "skill", skill }};
   IMatchmakerTicket matchTicket = await sessionSocket.AddMatchmakerAsync(query, minPlayers, maxPlayers, null, numericProperties);
   Debug.Log($"MatchTicket: {matchTicket.Ticket}...");
}

Note - This code requires a socket to be created from your session. We will not cover that here as it is already covered in a number of other topics. Check out our topic on Achievements here for an example, or you can look at some examples in the Nakama documentation here.

Let us break down the example above.

Match Query

Match queries allow you to control what kind of matches you want for your player.

As you can see from our match query

"+properties.skill:>=1 +properties.skill:<=10"

We are setting the absolute value of the skill level to between 1 and 10, just like in the GameSparks example.

For this to work we need to add properties to our request which will include our skill. You can see that being set using the C# Dictionary in the above example.

Note - Nakama uses the Bleve search and indexing engine so you can check that out for more examples of the kinds of queries you can use here.

Match Properties

You can see from the example above that we have added a Dictionary called “numericProperties” in which we have included our player’s skill value. You can add multiple properties to this field depending on your requirements and they are referenced from the query by adding the “+properties.” prefix.

We can also add string properties to the matchmaking request. We’ll see an example of how to do that later in this topic.

Before we can test this matchmaking request we need to create a listener for our matchmaking messages and assign it to the socket created from our session.

For this example we will just print the details of the successful match message to the console.

sessionSocket.ReceivedMatchmakerMatched += matched =>
{
   Debug.LogFormat("Received: {0}", matched);
   foreach (IMatchmakerUser user in matched.Users)
   {
       Debug.Log($" UserName: {user.Presence.Username}, Id: {user.Presence.UserId}" );
   }
};

Note - We are using a minimum of 2 players, a maximum of 4 players and both players have a skill value of 5 for this example.

Note - Matchmaking with Nakama can take up to 30 seconds before you get a result so be patient and wait for the logs to appear to confirm your code is working correctly.

This example covers very basic matchmaking with the skill parameter, but what if we need to transition some more complex matchmaking from GameSparks like Thresholds or Participant Data?

Participant Data

We’ve already touched on how you can replicate this in Nakama using match properties but let's take a look at a simple example you might have in GameSparks where you want to match players via skill level but also by region or country.

In GameSparks, the MatchMakingRequest would look something like this…

{
 "@class": ".MatchmakingRequest",
 "customQuery": {"players.participantData.countryCode":"US"},
 "participantData": {"countryCode":"US"},
 "matchShortCode": "4v4",
 "skill": 5
}

With Nakama we can add string properties to the matchmaking request and include the country code to the matchmaking query.

string query = "+properties.skill:>=1 +properties.skill:<=10";
// add country property to query //
query += " +properties.country:" + countryCode;
Debug.Log($"Query: [{query}]");
var numericProperties = new Dictionary<string, double>() {{ "skill", skill }};
var stringProperties = new Dictionary<string, string>() {{ "country", countryCode }};
IMatchmakerTicket matchTicket = await sessionSocket.AddMatchmakerAsync(query, minPlayers, maxPlayers, stringProperties, numericProperties);

Remember that you can include multiple numeric and string properties to your request and add them to your query to construct more complex matchmaking.

Thresholds

Because matchmaking in Nakama is initiated from the client, in order to create thresholds which change matchmaking parameters over time, we need to use something like a Coroutine in Unity.

We will need to loop through a list of thresholds and create a new matchmaking request after each threshold has timed out. This is simple in Unity but it does require some preparation.

To begin with we are going to create a GSMatchConfig class. To keep this example simple, this class will have a min and max player attribute, along with an array of thresholds.

Thresholds will be a struct with a duration attribute and a string which will represent a Nakama matchmaking query.

public class GSMatchConfig
{
   public int maxPlayers { get; set; }
   public int minPlayers { get; set; }
   public Threshold[] thresholdQueries { get; set; }
   public struct Threshold
   {
       public Threshold(int _duration, string _query)
       {
           duration = _duration;
           query = _query;
       }
       public int duration;
       public string query;
   }
}

And now we can create an instance of this class and add our threshold details.

GSMatchConfig thresholdMatchConfig = new GSMatchConfig();
thresholdMatchConfig.minPlayers = minPlayers;
thresholdMatchConfig.maxPlayers = maxPlayers;
thresholdMatchConfig.thresholdQueries = new GSMatchConfig.Threshold[]
{
   new GSMatchConfig.Threshold(20, "+properties.skill:>=1 +properties.skill:<=10"),
   new GSMatchConfig.Threshold(20, "+properties.skill:>=1 +properties.skill:<=50"),
   new GSMatchConfig.Threshold(20, "+properties.skill:>=1 +properties.skill:<=100")
};

For this example we are just broadening the skill range over time but you can add more complex query strings using Bleve search parameters.

Next we need to create the Coroutine method which will actually run through each threshold. We want the process to wait until the threshold duration has passed before starting the next matchmaking request so this is why we are using Coroutine.

This will be started from the matchmaking request method where our GSMatchConfig object is defined.

/// <summary>
/// Iterates over an array of thresholds and creates new matchmaking requests for each threshold
/// </summary>
/// <param name="matchConfig"></param>
/// <param name="sessionSocket"></param>
/// <param name="skill"></param>
/// <returns></returns>
IEnumerator StartMatchmakingThresholds(GSMatchConfig matchConfig, ISocket sessionSocket, int skill)
{
   Task<IMatchmakerTicket> matchTicket = null;
   for (int i = 0; i < matchConfig.thresholdQueries.Length; i++)
   {
       var threshold = matchConfig.thresholdQueries[i];
       int duration = threshold.duration;
       string query = threshold.query;
       if (matchFound)
       {
           break;
       }
       if (i > 0)
       {
           Debug.Log($"Cancelling preview matchmaking request {matchTicket.Result.Ticket}");
           sessionSocket.RemoveMatchmakerAsync(matchTicket.Result);
       }
       Debug.Log($"Sending matchmaking request...");
       Debug.Log($"Query [{query}]");
       var numericProperties = new Dictionary<string, double>() {{ "skill", skill }};
       matchTicket = sessionSocket.AddMatchmakerAsync(query, matchConfig.minPlayers, matchConfig.maxPlayers, null, numericProperties);
       yield return new WaitForSeconds(duration);
   }

   if (!matchFound)
   {
       Debug.LogWarning("Match not found...");
       sessionSocket.RemoveMatchmakerAsync(matchTicket.Result);
   }
   else
   {
       Debug.Log("Match Found...");
   }
}

Let us break down what is happening in the flow above:

Now we can start this Coroutine from our matchmaking method.

/// <summary>
/// Starts the matchmaking process when using matchmaking thresholds
/// </summary>
/// <param name="minPlayers"></param>
/// <param name="maxPlayers"></param>
/// <param name="skill"></param>
private async void SubmitThresholdMatchRequest(int minPlayers, int maxPlayers, int skill)
{
   GSMatchConfig thresholdMatchConfig = new GSMatchConfig();
   thresholdMatchConfig.minPlayers = minPlayers;
   thresholdMatchConfig.maxPlayers = maxPlayers;
   thresholdMatchConfig.thresholdQueries = new GSMatchConfig.Threshold[]
   {
       new GSMatchConfig.Threshold(60, "+properties.skill:>=1 +properties.skill:<=10"),
       new GSMatchConfig.Threshold( 60, "+properties.skill:>=1 +properties.skill:<=50"),
       new GSMatchConfig.Threshold( 60, "+properties.skill:>=1 +properties.skill:<=100")
   };
   StartCoroutine(StartMatchmakingThresholds(thresholdMatchConfig, sessionSocket, skill));
}

You can run this code with a single player just to check the process is working. You should see each step in the console logs.

Note - remember to set the matchFound bool to ‘true’ in the ReceivedMatchmakerMatched listener.

There are several things to note about this process as it relates to Nakama.

As mentioned before, Nakama matchmaking can take much longer than what you expect from GameSparks matchmaking. Therefore short threshold durations may not be useful and you may need to increase your threshold durations in order for your matchmaking to be effective.

You can also choose not to cancel match-tickets at the start of each new threshold. This would keep your match ticket in the pool throughout all thresholds so that you can still get potential matches at lower thresholds before the duration of all thresholds has passed.

As you can see from what we have covered in this topic, Nakama’s matchmaking feature is very flexible and should be able to adapt to the vast majority of gameSparks matchmaking configurations.

For more information consult Nakama’s documentation on matchmaking here.