Matchmaker #

The Nakama matchmaker feature enables users to search for, and be matched with, other users - whether as teammates or opponents - to form matches, groups, or participate in any other social features built into your project.

Matchmake to find friends, groups, teammates, or opponents
Matchmake to find friends, groups, teammates, or opponents

The matchmaker maintains a pool of users and their matchmaking requests (“tickets”), and places them together whenever a good match is found. The definition of a “good match” for any particular user is based upon the criteria defined in their matchmaker ticket. The length of time before a match is found is highly variable, depending on how narrowly defined the criteria is and the amount of users currently in the matchmaking pool. If matchmaking duration is too long, or matches are not being found at all, consider expanding the criteria.

Note that matchmaking is for active players only - users with an open socket connection. Once they have submitted a matchmaker ticket - adding themselves to the matchmaker pool of available players - users remain there until a match is found or they cancel their request. If a user disconnects, any pending matchmaking requests they had are also cancelled.

Offline Matchmaking
If your use case requires offline matchmaking, please check the following section for assistance.

Matchmaking is distinct from Nakama’s match listing feature. Where the matchmaker is used to place users together to start a new match, match listing is used to show users existing matches that they can join immediately. Deciding between using matchmaking or match listing is a design consideration (rather than a technical one) based on your project goals and requirements.

Configuration #

There are several parameters available in your Nakama configuration that affect how the matchmaker functions.

You can prevent users from submitting abusive amounts of tickets, and never cancelling old ones, by setting a maximum number of concurrent tickets a user can have at any one time.

By setting the time interval the matchmaker attempts to find a user’s “ideal” (size) match, and the number of intervals before allowing a less ideal (in size) match, you can adjust how long your users are waiting and balance finding the ideal match versus getting into a match more quickly.

The reverse matching precision and threshold flags are used to dictate if, and for how long, reverse matching precision is active. If enabled (rev_precision set to true), the matchmaker will validate matches bidirectionally (i.e. when Player A matches with Player B, it will also check that Player B matches with Player A). If the match is not bidirectional, the matchmaker will continue to search for a bidirectional match for the duration of the rev_threshold intervals. If no bidirectional match is found within number of intervals (1 by default), the matchmaker will return to unidirectional matching.

Matchmaking criteria #

To begin matchmaking users add themselves to the matchmaking pool. As part of their matchmaker ticket there are optional criteria that can be included to describe the desired match: Properties, Minimum count and Maximum count, Count multiple, and a Query.

Matchmaker Override
In addition to any criteria defined in the matchmaker ticket, you can also register a matchmaker override function to further refine the matchmaking process.

Properties #

Properties are key-value pairs, either string or numeric, that describe the user submitting a matchmaking ticket. Some common examples of what can be provided in properties include the user’s game rank/level, their skill rating, the connecting region, or selected match types (e.g. Free for All, Capture the Flag, etc.).

Client
1
2
3
4
5
6
7
const stringProperties = {
  region: "europe"
};

const numericProperties = {
  rank: 8
};
Client
1
2
3
4
5
6
7
var stringProperties = new Dictionary<string, string>() {
    {"region", "europe"}
};

var numericProperties = new Dictionary<string, int>() {
    {"rank", 8}
};
Client
1
2
let stringProperties = ["region": "europe"]
let numericProperties: [String:Double] = ["rank": 8]
Client
1
2
3
4
5
6
const stringProperties = {
  'region': 'europe',
};
const numericProperties = {
  'rank': 8,
};
Client
1
2
3
4
5
NStringMap stringProperties;
NStringDoubleMap numericProperties;

stringProperties.emplace("region", "europe");
numericProperties.emplace("rank", 8.0);
Client
1
2
3
4
5
6
7
Map<String, String> stringProperties = new HashMap<String, String>() {{
    put("region", "europe");
}};

Map<String, Double> numericProperties = new HashMap<String, Double>() {{
    put("rank", 8.0);
}};
Client
1
2
var string_properties = { "region": "europe" }
var numeric_properties = { "rank": 8 }
Client
1
2
var string_properties = { "region": "europe" }
var numeric_properties = { "rank": 8 }
Client
1
2
local string_properties = { region = "europe" }
local numeric_properties = { rank = 8 }

Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.

These properties are submitted by the user when they begin the matchmaking process, and can be different for each ticket. The server merges all included properties to form the overall properties that are part of the matchmaker ticket.

When matchmaking completes these properties are visible to all matched users. You can store extra information without affecting the matchmaking process itself if it’s useful to clients - just submit properties that aren’t queried for as part of the matchmaking process.

You can also authoritatively control the properties using a before hook when adding the user to the matchmaker:

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var (
	errInternal = runtime.NewError("internal server error", 13)
)

func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
	initializer.RegisterBeforeRt("MatchmakerAdd", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *rtapi.Envelope) (*rtapi.Envelope, error) {
		message, ok := in.Message.(*rtapi.Envelope_MatchmakerAdd)
		if !ok {
			return nil, errInternal
		}

		// If the string properties contains a region value of "europe", modify it to "europe-west"
		if value, ok := message.MatchmakerAdd.StringProperties["region"]; ok && value == "europe" {
			message.MatchmakerAdd.StringProperties["region"] = "europe-west"
		}

		return in, nil
	})

	return nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function InitModule(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  initializer.registerRtBefore("MatchmakerAdd", beforeMatchmakerAdd)
}

const beforeMatchmakerAdd : nkruntime.RtBeforeHookFunction<nkruntime.EnvelopeMatchmakerAdd> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, envelope: nkruntime.EnvelopeMatchmakerAdd) : nkruntime.EnvelopeMatchmakerAdd | void {
  const region = envelope.matchmakerAdd.stringProperties["region"];

  // If the string properties contain a region value of "europe", modify it to "europe-west"
  if (region && region == "europe") {
    envelope.matchmakerAdd.stringProperties["region"] = "europe-west";
  }

  return envelope;
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
nk.register_rt_before(function(context, payload)
    local region = payload.matchmaker_add.string_properties["region"]

    -- If the string properties contain a region value of "europe", modify it to "europe-west"
    if region == "europe" then
        payload.matchmaker_add.string_properties["region"] = "europe-west"
    end

    return payload
end, "MatchmakerAdd")

Minimum and maximum count #

When submitting a matchmaker request users must specify both a minimum and maximum count, where the minCount represents the smallest acceptable match size and maxCount represents the largest acceptable match size, with both being inclusive of the player submitting the request.

The matchmaker will always try to match at the provided maximum count. If there aren’t enough users, then the closest possible size to the maximum count will be returned as the match so long as it is above the minimum count.

For example, if using a minimum count of 2 and maximum count of 4, the matchmaker will try to find 3 other players to match the user with. If there are not 3 matching players available, the matchmaker will try to match 2 others, and finally just 1 other player if 2 are not available.

If there are not enough available users to meet even the minimum count, no match is returned and the user remains in the pool.

Client
1
2
3
4
const query = "*";
const minCount = 2;
const maxCount = 4;
var ticket = await socket.addMatchmaker(query, minCount, maxCount);
Client
1
2
3
4
var query = "*";
var minCount = 2;
var maxCount = 4;
var matchmakerTicket = await socket.AddMatchmakerAsync(query, minCount, maxCount);
Client
1
2
3
4
let query = "*"
let minCount = 2
let maxCount = 4
let ticket = try await socket.addMatchmaker(query: query, minCount: minCount, maxCount: maxCount)
Client
1
2
3
4
5
6
7
8
9
const query = '*';
const minCount = 2;
const maxCount = 4;
final ticket = await socket.addMatchmaker(
  query,
  minCount: minCount,
  maxCount: maxCount,
);

Client
1
2
3
4
String query = "*";
int minCount = 2;
int maxCount = 4;
MatchmakerTicket matchmakerTicket = socket.addMatchmaker(query, minCount, maxCount).get();
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var query = "*"
var min_count = 2
var max_count = 4

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = yield(
    socket.add_matchmaker_async(query, min_count, max_count),
    "completed"
)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var query = "*"
var min_count = 2
var max_count = 4

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = await socket.add_matchmaker_async(query, min_count, max_count)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
auto successCallback = [](const NMatchmakerTicket& ticket)
{
    std::cout << "Matchmaker ticket: " << ticket.ticket << std::endl;
};

int32_t minCount = 2;
int32_t maxCount = 4;
string query = "*";

rtClient->addMatchmaker(minCount, maxCount, query, {}, {}, successCallback);
Client
1
2
3
4
5
local query = "*"
local min_players = 2
local max_players = 4

local ticket = socket.matchmaker_add(min_players, max_players, query)

Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.

Users can search for an exact number of opponents by submitting the same minimum and maximum count:

Client
1
2
3
4
const query = "*";
const minCount = 4;
const maxCount = 4;
var ticket = await socket.addMatchmaker(query, minCount, maxCount);
Client
1
2
3
4
var query = "*";
var minCount = 4;
var maxCount = 4;
var matchmakerTicket = await socket.AddMatchmakerAsync(query, minCount, maxCount);
Client
1
2
3
4
let query = "*"
let minCount = 4
let maxCount = 4
let ticket = try await socket.addMatchmaker(query: query, minCount: minCount, maxCount: maxCount)
Client
1
2
3
4
5
6
7
8
const query = '*';
const minCount = 4;
const maxCount = 4;
final ticket = await socket.addMatchmaker(
  query,
  minCount: minCount,
  maxCount: maxCount,
);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
auto successCallback = [](const NMatchmakerTicket& ticket)
{
    std::cout << "Matchmaker ticket: " << ticket.ticket << std::endl;
};

int32_t minCount = 4;
int32_t maxCount = 4;
string query = "*";

rtClient->addMatchmaker(minCount, maxCount, query, {}, {}, successCallback);
Client
1
2
3
4
5
String query = "*";
int minCount = 4;
int maxCount = 4;

MatchmakerTicket matchmakerTicket = socket.addMatchmaker(query, minCount, maxCount).get();
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var query = "*"
var min_count = 4
var max_count = 4

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = yield(
    socket.add_matchmaker_async(query, min_count, max_count),
    "completed"
)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var query = "*"
var min_count = 4
var max_count = 4

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = await socket.add_matchmaker_async(query, min_count, max_count)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])
Client
1
2
3
4
5
local query = "*"
local min_players = 4
local max_players = 4

local ticket = socket.matchmaker_add(min_players, max_players, query)

Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.

You can also authoritatively control the minimum and maximum counts using a before hook when adding the user to the matchmaker:

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var (
	errInternal = runtime.NewError("internal server error", 13)
)

func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
	initializer.RegisterBeforeRt("MatchmakerAdd", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *rtapi.Envelope) (*rtapi.Envelope, error) {
		message, ok := in.Message.(*rtapi.Envelope_MatchmakerAdd)
		if !ok {
			return nil, errInternal
		}

		// Force min count to be 4 and max count to be 8
        message.MatchmakerAdd.MinCount = 4
        message.MatchmakerAdd.MaxCount = 8

		return in, nil
	})

	return nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function InitModule(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  initializer.registerRtBefore("MatchmakerAdd", beforeMatchmakerAdd)
}

const beforeMatchmakerAdd : nkruntime.RtBeforeHookFunction<nkruntime.EnvelopeMatchmakerAdd> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, envelope: nkruntime.EnvelopeMatchmakerAdd) : nkruntime.EnvelopeMatchmakerAdd | void {
  // Force min count to be 4 and max count to be 8
  envelope.matchmakerAdd.minCount = 4
  envelope.matchmakerAdd.maxCount = 8

  return envelope;
}
Server
1
2
3
4
5
6
7
nk.register_rt_before(function(context, payload)
  -- Force min count to be 4 and max count to be 8
  payload.matchmaker_add.min_count = 4
  payload.matchmaker_add.max_count = 8

  return payload
end, "MatchmakerAdd")

Count multiple #

The countMultiple parameter can be used when you need to enforce a specific multiplier for the acceptable match sizes (i.e. results must be in multiples of 5).

For example, take the following request:

Client
1
2
3
4
5
const query = "*";
const minCount = 5;
const maxCount = 25;
const countMultiple = 5;
var ticket = await socket.addMatchmaker(query, minCount, maxCount, countMultiple);
Client
1
2
3
4
5
var query = "*";
var minCount = 5;
var maxCount = 25;
var countMultiple = 5;
var matchmakerTicket = await socket.AddMatchmakerAsync(query, minCount, maxCount, countMultiple);
Client
1
2
3
4
5
let query = "*"
let minCount = 5
let maxCount = 25
let countMultiple = 5
let ticket = try await socket.addMatchmaker(query: query, minCount: minCount, maxCount: maxCount, countMultiple: countMultiple)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const query = '*';
const minCount = 5;
const maxCount = 25;
const countMultiple = 5;
final ticket = await socket.addMatchmaker(
  query,
  minCount: minCount,
  maxCount: maxCount,
  countMultiple: countMultiple,
);
Client
1
2
3
4
5
String query = "*";
int minCount = 5;
int maxCount = 25;
int countMultiple = 5;
MatchmakerTicket matchmakerTicket = socket.addMatchmaker(query, minCount, maxCount, countMultiple).get();
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var query = "*"
var min_count = 5
var max_count = 25
var count_multiple = 5

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = yield(
    socket.add_matchmaker_async(query, min_count, max_count, count_multiple),
    "completed"
)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var query = "*"
var min_count = 5
var max_count = 25
var count_multiple = 5

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = await socket.add_matchmaker_async(query, min_count, max_count, count_multiple)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = [](const NMatchmakerTicket& ticket)
{
    std::cout << "Matchmaker ticket: " << ticket.ticket << std::endl;
};

int32_t minCount = 5;
int32_t maxCount = 25;
string query = "*";
int32_t countMultiple = 5;

rtClient->addMatchmaker(minCount, maxCount, query, {}, {}, countMultiple successCallback);
Client
1
2
3
4
5
6
local query = "*"
local min_players = 5
local max_players = 25
local count_multiple = 5

local ticket = socket.matchmaker_add(min_players, max_players, query, nil, nil, count_multiple)

Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.

The matchmaker will only return results containing a total number of matched players that is a multiple of 5, first trying to return the maximum count of 25, then 20, 15, and so forth. Even if there are 23 matching players available, the returned result will be 20 players.

You can also authoritatively control the count multiple using a before hook when adding the user to the matchmaker:

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var (
	errInternal = runtime.NewError("internal server error", 13)
)

func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
	initializer.RegisterBeforeRt("MatchmakerAdd", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *rtapi.Envelope) (*rtapi.Envelope, error) {
		message, ok := in.Message.(*rtapi.Envelope_MatchmakerAdd)
		if !ok {
			return nil, errInternal
		}

		// Force the count multiple to be in multiples of 5
		message.MatchmakerAdd.CountMultiple = &wrapperspb.Int32Value{Value: 5}

		return in, nil
	})

	return nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function InitModule(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  initializer.registerRtBefore("MatchmakerAdd", beforeMatchmakerAdd)
}

const beforeMatchmakerAdd : nkruntime.RtBeforeHookFunction<nkruntime.EnvelopeMatchmakerAdd> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, envelope: nkruntime.EnvelopeMatchmakerAdd) : nkruntime.EnvelopeMatchmakerAdd | void {
  // Force the count multiple to be in multiples of 5
  envelope.matchmakerAdd.countMultiple = 5;

  return envelope;
}
Server
1
2
3
4
5
6
nk.register_rt_before(function(context, payload)
  -- Force the count multiple to be in multiples of 5
  payload.matchmaker_add.count_multiple = 5

  return payload
end, "MatchmakerAdd")

Query #

Where properties describes the user searching for other players, the query describes what properties they are searching for in other users.

Every user’s matchmaker properties are available in queries under the properties prefix. You can find opponents based on a mix of property filters with exact matches or ranges of values.

See Query Syntax to learn about the grammar and operators available for your queries.

This example searches for opponents that must be in europe and must have a rank between 5 and 10, inclusive:

Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const query = "+properties.region:europe +properties.rank:>=5 +properties.rank:<=10";
const minCount = 2;
const maxCount = 4;

const stringProperties = {
  region: "europe"
};

const numericProperties = {
  rank: 8
};

var ticket = await socket.addMatchmaker(query, minCount, maxCount, stringProperties, numericProperties);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var query = "+properties.region:europe +properties.rank:>=5 +properties.rank:<=10";

var stringProperties = new Dictionary<string, string>() {
    {"region", "europe"}
};

var numericProperties = new Dictionary<string, int>() {
    {"rank", 8}
};

var matchmakerTicket = await socket.AddMatchmakerAsync(query, 2, 4, stringProperties, numericProperties);
Client
1
2
3
4
5
let query = "+properties.region:europe +properties.rank:>=5 +properties.rank:<=10"
let stringProperties = ["region": "europe"]
let numericProperties: [String:Double] = ["rank": 8]

let ticket = try await socket.addMatchmaker(query: query, minCount: 2, maxCount: 4, stringProperties: stringProperties, numericProperties: numericProperties)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const query = '+properties.region:europe +properties.rank:>=5 +properties.rank:<=10';

const stringProperties = {
  'region': 'europe',
};

const Map<String, double> numericProperties = {
  'rank': 8,
};

final ticket = await socket.addMatchmaker(
  query,
  minCount: 2,
  maxCount: 4,
  stringProperties: stringProperties,
  numericProperties: numericProperties,
  countMultiple: 5,
);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
auto successCallback = [](const NMatchmakerTicket& ticket)
{
    std::cout << "Matchmaker ticket: " << ticket.ticket << std::endl;
};

int32_t minCount = 2;
int32_t maxCount = 4;
string query = "+properties.region:europe +properties.rank:>=5 +properties.rank:<=10";
NStringMap stringProperties;
NStringDoubleMap numericProperties;

stringProperties.emplace("region", "europe");
numericProperties.emplace("rank", 8.0);

rtClient->addMatchmaker(minCount, maxCount, query, stringProperties, numericProperties, successCallback);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
String query = "+properties.region:europe +properties.rank:>=5 +properties.rank:<=10";
int minCount = 2;
int maxCount = 4;

Map<String, String> stringProperties = new HashMap<String, String>() {{
    put("region", "europe");
}};

Map<String, Double> numericProperties = new HashMap<String, Double>() {{
    put("rank", 8.0);
}};

MatchmakerTicket matchmakerTicket = socket.addMatchmaker(query, minCount, maxCount, stringProperties, numericProperties).get();
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var query = "+properties.region:europe +properties.rank:>=5 +properties.rank:<=10"
var string_properties = { "region": "europe"}
var numeric_properties = { "rank": 8 }

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = yield(
    socket.add_matchmaker_async(query, 2, 4, string_properties, numeric_properties),
    "completed"
)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var query = "+properties.region:europe +properties.rank:>=5 +properties.rank:<=10"
var string_properties = { "region": "europe"}
var numeric_properties = { "rank": 8 }

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = await socket.add_matchmaker_async(query, 2, 4, string_properties, numeric_properties)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])
Client
1
2
3
4
5
6
7
local min_players = 2
local max_players = 4
local query = "+properties.region:europe +properties.rank:>=5 +properties.rank:<=10"
local string_properties = { region = "europe"}
local numeric_properties = { rank = 8 }

local matchmaker_ticket = socket.matchmaker_add(min_players, max_players, query, string_properties, numeric_properties)

Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.

The wildcard query "*" can be used to ignore opponents’ properties and match with anyone:

Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const query = "*";
const minCount = 2;
const maxCount = 4;

const stringProperties = {
  region: "europe"
};

const numericProperties = {
  rank: 8
};

var ticket = await socket.addMatchmaker(query, minCount, maxCount, stringProperties, numericProperties);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var query = "*";
var minCount = 2;
var maxCount = 4;

var stringProperties = new Dictionary<string, string>() {
    {"region", "europe"}
};

var numericProperties = new Dictionary<string, int>() {
    {"rank", 8}
};

var matchmakerTicket = await socket.AddMatchmakerAsync(query, minCount, maxCount, stringProperties, numericProperties);
Client
1
2
3
4
5
6
7
8
let query = "*"
let minCount = 2
let maxCount = 4

let stringProperties = ["region": "europe"]
let numericProperties: [String:Double] = ["rank": 8]

let ticket = try await socket.addMatchmaker(query: query, minCount: minCount, maxCount: maxCount, stringProperties: stringProperties, numericProperties: numericProperties)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const query = '*';
const minCount = 2;
const maxCount = 4;

const stringProperties = {
  'region': 'europe',
};

const Map<String, double> numericProperties = {
  'rank': 8,
};

final ticket = await socket.addMatchmaker(
  query,
  minCount: minCount,
  maxCount: maxCount,
  stringProperties: stringProperties,
  numericProperties: numericProperties,
  countMultiple: 5,
);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
auto successCallback = [](const NMatchmakerTicket& ticket)
{
    std::cout << "Matchmaker ticket: " << ticket.ticket << std::endl;
};

int32_t minCount = 2;
int32_t maxCount = 4;
string query = "*";
NStringMap stringProperties;
NStringDoubleMap numericProperties;

stringProperties.emplace("region", "europe");
numericProperties.emplace("rank", 8.0);

rtClient->addMatchmaker(minCount, maxCount, query, stringProperties, numericProperties, successCallback);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
String query = "*";
int minCount = 2;
int maxCount = 4;

Map<String, String> stringProperties = new HashMap<String, String>() {{
    put("region", "europe");
}};

Map<String, Double> numericProperties = new HashMap<String, Double>() {{
    put("rank", 8.0);
}};

MatchmakerTicket matchmakerTicket = socket.addMatchmaker(query, minCount, maxCount, stringProperties, numericProperties).get();
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var query = "*"
var min_count = 2
var max_count = 4
var string_properties = { "region": "europe" }
var numeric_properties = { "rank": 8 }

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = yield(
    socket.add_matchmaker_async(query, min_count, max_count, string_properties, numeric_properties),
    "completed"
)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var query = "*"
var min_count = 2
var max_count = 4
var string_properties = { "region": "europe" }
var numeric_properties = { "rank": 8 }

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = await socket.add_matchmaker_async(query, min_count, max_count, string_properties, numeric_properties)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])
Client
1
2
3
4
5
6
7
local min_players = 2
local max_players = 4
local query = "*"
local string_properties = { region = "europe"}
local numeric_properties = { rank = 8 }

local matchmaker_ticket = socket.matchmaker_add(min_players, max_players, query, string_properties, numeric_properties)

Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.

You can also authoritatively control the query using a before hook when adding the user to the matchmaker:

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var (
	errInternal = runtime.NewError("internal server error", 13)
)

func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
	initializer.RegisterBeforeRt("MatchmakerAdd", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *rtapi.Envelope) (*rtapi.Envelope, error) {
		message, ok := in.Message.(*rtapi.Envelope_MatchmakerAdd)
		if !ok {
			return nil, errInternal
		}

		// Force the matchmaking request to use the * query
		message.MatchmakerAdd.Query = "*"

		return in, nil
	})

	return nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function InitModule(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  initializer.registerRtBefore("MatchmakerAdd", beforeMatchmakerAdd)
}

const beforeMatchmakerAdd : nkruntime.RtBeforeHookFunction<nkruntime.EnvelopeMatchmakerAdd> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, envelope: nkruntime.EnvelopeMatchmakerAdd) : nkruntime.EnvelopeMatchmakerAdd | void {
  // Force the matchmaking request to use the * query
  envelope.matchmakerAdd.query = "*";

  return envelope;
}
Server
1
2
3
4
5
6
nk.register_rt_before(function(context, payload)
  -- Force the matchmaking request to use the * query
  payload.matchmaker_add.query = "*"

  return payload
end, "MatchmakerAdd")

Expanding criteria #

Matchmaking Duration

The success of a matchmaking request, or the length of time needed to find a match, cannot be guaranteed as both are dependent on the pool of users active in the matchmaker and the specific criteria sought for in a match.

Repeatedly submitting identical requests will not yield different results. For this reason, placing any artificial time limit on a matchmaker request is not advised.

Based on the number of active users and the respective criteria used in matchmaking, it can sometimes be difficult or impossible to find the exact match desired.

To effectively “loosen” the criteria being used players should submit multiple tickets, each with a more permissive query than those before it.

For example, if a player wants to be matched with others that are in their region and at exactly the same skill level but is not getting any results, the subsequent tickets can expand to include other regions and allow for a range of skill levels close to the player’s own.

Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let query = "+properties.region:europe +properties.rank:5";
const minCount = 2;
const maxCount = 4;

const stringProperties = {
  region: "europe"
};

const numericProperties = {
  rank: 8
};

var ticket = await socket.addMatchmaker(query, minCount, maxCount, stringProperties, numericProperties);

// ... if no match is found within a certain time, request a new ticket with looser criteria
query = "+properties.region:europe +properties.rank:>=3 +properties.rank:<=7";
var newTicket = await socket.addMatchmaker(query, minCount, maxCount, stringProperties, numericProperties);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var query = "+properties.region:europe +properties.rank:5";

var stringProperties = new Dictionary<string, string>() {
    {"region", "europe"}
};

var numericProperties = new Dictionary<string, int>() {
    {"rank", 8}
};

var matchmakerTicket = await socket.AddMatchmakerAsync(query, 2, 4, stringProperties, numericProperties);

// ... if no match is found within a certain time, request a new ticket with looser criteria
query = "+properties.region:europe +properties.rank:>=3 +properties.rank:<=7";
var newMatchmakerTicket = await socket.AddMatchmakerAsync(query, 2, 4, stringProperties, numericProperties);
Client
1
2
3
4
5
6
7
8
9
var query = "+properties.region:europe +properties.rank:5"
let stringProperties = ["region": "europe"]
let numericProperties: [String:Double] = ["rank": 8]

let ticket = try await socket.addMatchmaker(query: query, minCount: 2, maxCount: 4, stringProperties: stringProperties, numericProperties: numericProperties)

// ... if no match is found within a certain time, request a new ticket with looser criteria
query = "+properties.region:europe +properties.rank:>=3 +properties.rank:<=7"
let newTicket = try await socket.addMatchmaker(query: query, minCount: 2, maxCount: 4, stringProperties: stringProperties, numericProperties: numericProperties)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var query = '+properties.region:europe +properties.rank:5';
const stringProperties = {
  'region': 'europe',
};
const Map<String, double> numericProperties = {
  'rank': 8,
};

final ticket = await socket.addMatchmaker(
  query,
  minCount: 2,
  maxCount: 4,
  stringProperties: stringProperties,
  numericProperties: numericProperties,
  countMultiple: 5,
);

// ... if no match is found within a certain time, request a new ticket with looser criteria
query = '+properties.region:europe +properties.rank:>=3 +properties.rank:<=7';
final newTicket = await socket.addMatchmaker(
  query,
  minCount: 2,
  maxCount: 4,
  stringProperties: stringProperties,
  numericProperties: numericProperties,
  countMultiple: 5,
);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
auto successCallback = [](const NMatchmakerTicket& ticket)
{
    std::cout << "Matchmaker ticket: " << ticket.ticket << std::endl;
};

int32_t minCount = 2;
int32_t maxCount = 4;
string query = "+properties.region:europe +properties.rank:5";
NStringMap stringProperties;
NStringDoubleMap numericProperties;

stringProperties.emplace("region", "europe");
numericProperties.emplace("rank", 8.0);

rtClient->addMatchmaker(minCount, maxCount, query, stringProperties, numericProperties, successCallback);

// ... if no match is found within a certain time, request a new ticket with looser criteria
query = "+properties.region:europe +properties.rank:>=3 +properties.rank:<=7";
rtClient->addMatchmaker(minCount, maxCount, query, stringProperties, numericProperties, successCallback);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
String query = "+properties.region:europe +properties.rank:5";
int minCount = 2;
int maxCount = 4;

Map<String, String> stringProperties = new HashMap<String, String>() {{
    put("region", "europe");
}};

Map<String, Double> numericProperties = new HashMap<String, Double>() {{
    put("rank", 8.0);
}};

MatchmakerTicket matchmakerTicket = socket.addMatchmaker(query, minCount, maxCount, stringProperties, numericProperties).get();

// ... if no match is found within a certain time, request a new ticket with looser criteria
query = "+properties.region:europe +properties.rank:>=3 +properties.rank:<=7";
MatchmakerTicket newMatchmakerTicket = socket.addMatchmaker(query, minCount, maxCount, stringProperties, numericProperties).get();
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var query = "+properties.region:europe +properties.rank:>=5 +properties.rank:<=10"
var string_properties = { "region": "europe"}
var numeric_properties = { "rank": 8 }

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = yield(
    socket.add_matchmaker_async(query, 2, 4, string_properties, numeric_properties),
    "completed"
)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])

// ... if no match is found within a certain time, request a new ticket with looser criteria
query = "+properties.region:europe +properties.rank:>=3 +properties.rank:<=7";
var new_matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = yield(
    socket.add_matchmaker_async(query, 2, 4, string_properties, numeric_properties),
    "completed"
)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var query = "+properties.region:europe +properties.rank:>=5 +properties.rank:<=10"
var string_properties = { "region": "europe"}
var numeric_properties = { "rank": 8 }

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = await socket.add_matchmaker_async(query, 2, 4, string_properties, numeric_properties)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])

// ... if no match is found within a certain time, request a new ticket with looser criteria
query = "+properties.region:europe +properties.rank:>=3 +properties.rank:<=7";
var new_matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = await socket.add_matchmaker_async(query, 2, 4, string_properties, numeric_properties)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local min_players = 2
local max_players = 4
local query = "+properties.region:europe +properties.rank:>=5 +properties.rank:<=10"
local string_properties = { region = "europe"}
local numeric_properties = { rank = 8 }

local matchmaker_ticket = socket.matchmaker_add(min_players, max_players, query, string_properties, numeric_properties)

local new_query = "+properties.region:europe +properties.rank:>=3 +properties.rank:<=7"
local new_matchmaker_ticket = socket.matchmaker_add(min_players, max_players, new_query, string_properties, numeric_properties)

Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.

Matchmaker tickets #

Each time a user is added to the matchmaker pool they receive a ticket, a unique identifier representing their state in the matchmaker.

A user can have multiple matchmaker tickets at any given time, each representing a different set of criteria. For example one ticket seeking any available opponents for a Free for All match, and another seeking players in their local region to play in a Capture the Flag match.

This ticket is used when the server notifies the client on matching success. It distinguishes between multiple possible matchmaker operations for the same user. A successful match on one ticket does not automatically cancel any other tickets the user had open.

The user can cancel a ticket at any time before the ticket has been fulfilled.

Removing tickets #

If a user decides they no longer want any previously submitted matchmaker request, they can cancel that matchmaker ticket:

Client
1
socket.removeMatchmaker(ticket);
Client
1
2
// "matchmakerTicket" is returned by the matchmaker.
await socket.RemoveMatchmakerAsync(matchmakerTicket);
Client
1
2
// "ticket" is returned by the matchmaker.
try await socket.removeMatchmaker(ticket: ticket)
Client
1
2
// "ticket" is returned by the matchmaker.
await socket.removeMatchmaker(ticket);
Client
1
2
3
4
5
// "ticket" is returned by the matchmaker.
rtClient->removeMatchmaker(ticket, []()
{
    std::cout << "removed from Matchmaker" << std::endl;
});
Client
1
2
// "matchmakerTicket" is returned by the matchmaker.
socket.removeMatchmaker(matchmakerTicket.getTicket()).get();
Client
1
2
3
4
5
6
7
var removed : NakamaAsyncResult = yield(socket.remove_matchmaker_async(matchmaker_ticket.ticket), "completed")

if removed.is_exception():
    print("An error occurred: %s" % removed)
    return

print("Removed from matchmaking %s" % [matchmaker_ticket.ticket])
Client
1
2
3
4
5
6
7
var removed : NakamaAsyncResult = await socket.remove_matchmaker_async(matchmaker_ticket.ticket)

if removed.is_exception():
    print("An error occurred: %s" % removed)
    return

print("Removed from matchmaking %s" % [matchmaker_ticket.ticket])
Client
1
socket.matchmaker_remove(matchmaker_ticket.ticket)

Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.

This will only cancel the specified ticket and does not affect any other requests the user may have in the matchmaking pool.

Remember that if a user disconnects during matchmaking any open tickets are automatically cancelled. The user immediately reconnecting, no matter how quickly, does not restore their previous tickets.

Matchmaker override #

You can optionally further refine the matchmaker results - for example utilizing OpenSkill or your own custom logic - by registering a MatchmakerOverride function.

When RegisterMatchmakerOverride is used, the functioning of the matchmaker is altered as follows:

  • Using the provided criteria and existing matchmaker logic, the matchmaker compiles a list of all possible matches that could be formed, in order of preference, but does not yet create any matches.
  • This list is passed to the MatchmakerOverride function. Using any additional criteria or logic desired, this function returns a list of matches, in order of preference, that should be created.
  • The matchmaker then creates the matches in the order provided by the MatchmakerOverride function.

Keep in mind that the list of all potential matches will, by necessity, include matches that are not possible to create. For example, given a match size of 2 and a pool of 3 players, the list of all possible matches will include 3 matches, but only 1 of those matches can actually be created.

Matchmaker results #

Matchmaking is not always an instant process. Depending on the currently connected users the matchmaker may take time to complete and will return the resulting list of opponents asynchronously.

Clients should register an event handler that triggers when the server sends them a matchmaker result.

Client
1
2
3
4
socket.onmatchmakermatched = (matched) => {
  console.info("Received MatchmakerMatched message: ", matched);
  console.info("Matched opponents: ", matched.users);
};
Client
1
2
3
4
5
6
socket.ReceivedMatchmakerMatched += matched =>
{
    Console.WriteLine("Received: {0}", matched);
    var opponents = string.Join(",\n  ", matched.Users); // printable list.
    Console.WriteLine("Matched opponents: [{0}]", opponents);
};
Client
1
2
3
4
socket.onMatchmakerMatched = { matched in
    print("Received MatchmakerMatched message: \(matched)")
    print("Matched opponents: \(matched.users)")
}
Client
1
2
3
4
socket.onMatchmakerMatched.listen((MatchmakerMatched matched) {
  print('Received MatchmakerMatched message: $matched');
  print('Matched opponents: ${matched.users}');
});
Client
1
2
3
4
rtListener->setMatchmakerMatchedCallback([](NMatchmakerMatchedPtr matched)
{
    std::cout << "Matched! matchId: " << matched->matchId << std::endl;
});
Client
1
2
3
4
5
6
7
SocketListener listener = new AbstractSocketListener() {
    @Override
    public void onMatchmakerMatched(final MatchmakerMatched matched) {
        System.out.format("Received MatchmakerMatched message: %s", matched.toString());
        System.out.format("Matched opponents: %s", opponents.toString());
    }
};
Client
1
2
3
4
5
6
7
func _ready():
    # First, setup the socket as explained in the authentication section.
    socket.connect("received_matchmaker_matched", self, "_on_matchmaker_matched")

func _on_matchmaker_matched(p_matched : NakamaRTAPI.MatchmakerMatched):
    print("Received MatchmakerMatched message: %s" % [p_matched])
    print("Matched opponents: %s" % [p_matched.users])
Client
1
2
3
4
5
6
7
func _ready():
    # First, setup the socket as explained in the authentication section.
    socket.received_matchmaker_matched.connect(self._on_matchmaker_matched)

func _on_matchmaker_matched(p_matched : NakamaRTAPI.MatchmakerMatched):
    print("Received MatchmakerMatched message: %s" % [p_matched])
    print("Matched opponents: %s" % [p_matched.users])
Client
1
2
3
4
socket.on_matchmaker_matched(function(matched)
  pprint("Received MatchmakerMatched message: ", matched)
  pprint("Matched opponents: ", matched.users);
end)

Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.

The matchmaker result will include all of the matched users and their respective properties.

The result also includes either a token or match ID that can be used to join the new match for this group of matched players. Which is included depends on the type of match: for client relayed matches a token is provided, while for server authoritative matches a match ID is provided.

In the case of authoritative matches, you can use a server hook to create a new match on the server when matchmaker results are returned:

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var (
	errUnableToCreateMatch = runtime.NewError("unable to create match", 13)
)

func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
	if err := initializer.RegisterMatchmakerMatched(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, entries []runtime.MatchmakerEntry) (string, error) {
		matchId, err := nk.MatchCreate(ctx, "lobby", map[string]interface{}{"invited": entries})
		if err != nil {
			return "", errUnableToCreateMatch
		}

		return matchId, nil
	}); err != nil {
		logger.Error("unable to register matchmaker matched hook: %v", err)
		return err
	}

	return nil
}
Server
1
2
3
4
5
6
7
8
function InitModule(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  initializer.registerMatchmakerMatched(onMatchmakerMatched);
}

const onMatchmakerMatched : nkruntime.MatchmakerMatchedFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, matches: nkruntime.MatchmakerResult[]): string | void {
  const matchId = nk.matchCreate("lobby", { "invited": matches })
  return matchId;
};
Server
1
2
3
4
nk.register_matchmaker_matched(function(context, matched_users)
    local match_id = nk.match_create("lobby", { invited = matched_users })
    return match_id
end)

Join a match #

In client relayed multiplayer it is common to use the matchmaker result event as a way to join a new match with the matched opponents. The matched users do not automatically join the match they are assigned to.

Each matchmaker result event carries either a token, used to join a client relayed match, or a match ID, used to join an authoritative match. These can be used to join a match together with the matched opponents.

In the case of client relayed multiplayer, the token enables the server to know that these users wanted to play together and will create a match dynamically for them.

Tokens are short-lived and must be used to join a match as soon as possible. The match token is also used to prevent unwanted users from attempting to join a match they were not matched into. When a token expires it can no longer be used or refreshed.

The standard client-side “match join” operation can be used to join the new match:

Client
1
2
3
4
5
socket.onmatchmakermatched = (matched) => {
  console.info("Received MatchmakerMatched message: ", matched);
  const matchId = null;
  socket.joinMatch(matchId, matched.token);
};
Client
1
2
3
4
5
socket.ReceivedMatchmakerMatched += async matched =>
{
    Console.WriteLine("Received: {0}", matched);
    await socket.JoinMatchAsync(matched);
};
Client
1
2
3
4
5
6
7
socket.onMatchmakerMatched = { matched in
    print("Received MatchmakerMatched message: \(matched)")
    let matchId: String? = nil
    Task {
        try await self.socket.joinMatch(matchId: "matchId")
    }
}
Client
1
2
3
4
5
socket.onMatchmakerMatched.listen((MatchmakerMatched matched) {
  print('Received MatchmakerMatched message: $matched');
  final matchId = matched.matchId!;
  socket.joinMatch(matchId, matched.token);
});
Client
1
2
3
4
5
6
7
8
9
rtListener->setMatchmakerMatchedCallback([this](NMatchmakerMatchedPtr matched)
{
    std::cout << "Matched! token: " << matched->token << std::endl;

    rtClient->joinMatchByToken(matched->token, [](const NMatch& match)
    {
        std::cout << "Joined Match!" << std::endl;
    });
});
Client
1
2
3
4
5
6
SocketListener listener = new AbstractSocketListener() {
    @Override
    public void onMatchmakerMatched(final MatchmakerMatched matched) {
        socket.joinMatchToken(matched.getToken()).get();
    }
};
Client
1
2
3
4
5
6
7
8
9
func _on_matchmaker_matched(p_matched : NakamaRTAPI.MatchmakerMatched):
    print("Received MatchmakerMatched message: %s" % [p_matched])
    var joined_match : NakamaRTAPI.Match = yield(socket.join_match_async(p_matched), "completed")

    if joined_match.is_exception():
        print("An error occurred: %s" % joined_match)
        return

    print("Joined match: %s" % [joined_match])
Client
1
2
3
4
5
6
7
8
9
func _on_matchmaker_matched(p_matched : NakamaRTAPI.MatchmakerMatched):
    print("Received MatchmakerMatched message: %s" % [p_matched])
    var joined_match : NakamaRTAPI.Match = await socket.join_match_async(p_matched)

    if joined_match.is_exception():
        print("An error occurred: %s" % joined_match)
        return

    print("Joined match: %s" % [joined_match])
Client
1
2
3
4
socket.on_matchmaker_matched(function(matched)
  pprint("Received:", matched);
  socket.match_join(matched.token)
end)

Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.

Party matchmaking #

Nakama’s real-time parties enables users to band together into short-lived teams - lasting only as long as a given session - and play together. Once grouped into a party, these players can matchmake together, ensuring that they all are ultimately assigned to the same match.

Each party has a designated leader, typically the user that created the party. This leader sets the criteria that will be used for matchmaking and adds the party to the matchmaker pool:

Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Register the matchmaker matched handler (the party leader and party members should all do this)
socket.onmatchmakermatched = (matched) => {
  socket.joinMatch(null, matched.token);
};

// Create a party as the party leader
const party = await socket.createParty(true, 2);

// Accept any incoming party requests
socket.onpartyjoinrequest = (request) => {
  request.presences.forEach(presence => {
    await socket.acceptPartyMember(request.party_id, presence);
  });
};

// As the leader of the party, add the entire party to the matchmaker
const ticket = await socket.addMatchmakerParty(party.party_id, "*", 3, 4, null, null);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Register the matchmaker matched handler (the party leader and party members should all do this)
socket.ReceivedMatchmakerMatched += async matched => await socket.JoinMatchAsync(matched);

// Create a party as the party leader
var party = await socket.CreatePartyAsync(true, 2);

// Accept any incoming party requests
socket.ReceivedPartyJoinRequest += async request =>
{
    foreach (var presence in request.Presences)
    {
        await socket.AcceptPartyMemberAsync(request.PartyId, presence);
    }
};

// As the leader of the party, add the entire party to the matchmaker
var ticket = await socket.AddMatchmakerPartyAsync(party.Id, "*", 3, 4);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Register the matchmaker matched handler (the party leader and party members should all do this)
socket.onMatchmakerMatched = { matched in
    print("Received MatchmakerMatched message: \(matched)")
    print("Matched opponents: \(matched.users)")
}

// Create a party as the party leader
let party = try await socket.createParty(open: true, maxSize: 2)

// Accept any incoming party requests
socket.onPartyJoinRequest = { request in
    for presence in request.presences {
        Task {
          try await self.socket.acceptPartyMember(partyId: request.partyID, presence: presence.toUserPresence())
        }
    }
}

// As the leader of the party, add the entire party to the matchmaker
let ticket = try await socket.addMatchmakerParty(partyId: party.partyID, query: "*", minCount: 3, maxCount: 4)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Register the matchmaker matched handler (the party leader and party members should all do this)
socket.onMatchmakerMatched.listen((event) {
  print('Received MatchmakerMatched message: $event');
  print('Matched opponents: ${event.users}');
});

final party = await socket.createParty(open: true, maxSize: 2);

// Accept any incoming party requests
socket.onPartyJoinRequest.listen((event) async {
  for (var presence in event.presences) {
    await socket.acceptPartyMember(event.partyId, presence);
  }
});

// As the leader of the party, add the entire party to the matchmaker
final ticket = await socket.addMatchmakerParty(
  partyId: party.partyId,
  minCount: 3,
  maxCount: 4,
  query: '*',
);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func _ready():
  // Register the matchmaker matched handler (the party leader and party members should all do this)
  socket.connect("received_matchmaker_matched", self, "_on_matchmaker_matched")

  // Create a party as the party leader
  var party = yield(socket.create_party_async(true, 2), "completed")

  // Accept any incoming party requests
  socket.connect("received_party_join_request", self, "_on_party_join_request")

  // As the leader of the party, add the entire party to the matchmaker
  var ticket = yield(socket.add_matchmaker_party_async(party.id, "*", 3, 4), "completed");

func _on_matchmaker_matched(p_matched : NakamaRTAPI.MatchmakerMatched):
  var joined_match : NakamaRTAPI.Match = yield(socket.join_match_async(p_matched), "completed")

func _on_party_join_request(party_join_request: NakamaRTAPI.PartyJoinRequest):
  for presence in party_join_request.presences:
    yield(socket.accept_party_member_async(party_join_request.party_id, presence), "completed")
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func _ready():
  // Register the matchmaker matched handler (the party leader and party members should all do this)
  socket.received_matchmaker_matched.connect(self._on_matchmaker_matched)

  // Create a party as the party leader
  var party = socket.create_party_async(true, 2)

  // Accept any incoming party requests
  socket.received_party_join_request.connect(self._on_party_join_request)

  // As the leader of the party, add the entire party to the matchmaker
  var ticket = await socket.add_matchmaker_party_async(party.id, "*", 3, 4)

func _on_matchmaker_matched(p_matched : NakamaRTAPI.MatchmakerMatched):
  var joined_match : NakamaRTAPI.Match = await socket.join_match_async(p_matched)

func _on_party_join_request(party_join_request: NakamaRTAPI.PartyJoinRequest):
  for presence in party_join_request.presences:
    await socket.accept_party_member_async(party_join_request.party_id, presence)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
listener.setMatchmakerMatchedCallback([&rtClient](NMatchmakerMatchedPtr matchmakerMatched) {
    auto successCallback = [&rtClient](const NMatch match)
    {
        rtClient->createParty(true, 2);
    };

    rtClient->joinMatch(matchmakerMatched->matchId, {}, successCallback);
});

listener.setPartyJoinRequestCallback([&rtClient](const NPartyJoinRequest& joinRequest) {
    for (NUserPresence presence : joinRequest.presences)
    {
        rtClient->acceptPartyMember(joinRequest.partyId, presence);
    }
});
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- Register the matchmaker matched handler (the party leader and party members should all do this)
socket.on_matchmaker_matched(function(matched)
  pprint("Received:", matched);
  socket.match_join(matched.token)
end)

-- Accept any incoming party requests
socket.on_party_join_request(function(request)
  for i,presence in ipairs(request.presences) do
    socket.party_accept(request.party_id, presence)
  end
end)

-- Create a party as the party leader
local open = true
local max_players = 2
local party = socket.party_create(open, max_players)

-- As the leader of the party, add the entire party to the matchmaker
local min_players = 3
local max_players = 4
local query = "*"
local ticket = socket.party_matchmaker_add(party.party_id, min_players, max_players, query)

Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.

As part of the matchmaking process, the party members will always be kept together in any returned result. Parties can be matched both with other parties and individual users to ultimately form a match, there is no preference for either in the matchmaker.

For example, given a maximum count of 10, a party of 5 could be matched with another party of 3 and then two individual users to form a complete match.

Upon successful matchmaking, all party members receive the matchmaker result callback, not just the party leader.

Matchmaker Stats API #

To get the Matchmaker Stats via API, a request can be made to the endpoint /v2/matchmaker/stats (The API expects the client to be authenticated).

The API call returns a payload in the following format:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "ticket_count": 2,
  "oldest_ticket_create_time": "2024-07-30T16:00:42.390493Z",
  "completions": [
    {
      "create_time": "2024-07-30T16:00:42.390493Z",
      "complete_time": "2024-07-30T16:00:45.131689Z"
    },
    {
      "create_time": "2024-07-30T16:00:44.402601Z",
      "complete_time": "2024-07-30T16:00:45.131689Z"
    }
  ]
}

Timestamps are in ns precision.

Snapshot Latency
These timestamps are taken at a point in time that may not represent the exact status of the matchmaker by the time the response is received.

ticket_count: The number of tickets in the pool at the time of sampling.

oldest_ticket_create_time: The oldest ticket in the pool that hasn’t been matched yet.

completions: A collection of the latest 10 matches that the matchmaker has processed.

  • create_time: The timestamp of when the matchmaking ticket was added to the pool.

  • complete_time: The timestamp of when the matchmaking ticket was successfully fulfilled.

Client Use
These values should allow the client to calculate and present a rough estimate of matchmaking times.
Matchmaking Complexity
The completion times may vary widely depending on the complexity of the tickets criteria and number of tickets in the pool.

Related Pages