Heroic Labs Documentation

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, contact Heroic Labs 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.

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.

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
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 }

Code snippet for this language Defold 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.

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
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])

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Defold 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.

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
 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
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])

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Defold 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.

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
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])

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Defold 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.

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
 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])

Code snippet for this language Defold 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.

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
 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])

Code snippet for this language Defold 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.

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
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"
)

Code snippet for this language Defold 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.

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
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])

Code snippet for this language Defold 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.

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 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
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])

Code snippet for this language Defold 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.

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
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_matched_async(p_matched), "completed")

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

    print("Joined match: %s" % [joined_match])

Code snippet for this language Defold 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.

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 socket1.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
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_matched_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")

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Defold 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.

Related Pages