# 匹配程序

**URL:** https://heroiclabs.com/docs/zh/nakama/concepts/multiplayer/matchmaker/
**Summary:** 用户可以借助 Nakama 匹配程序查找比赛、群组和其他活动的对手和队友。匹配程序维护着一个目前正在寻找对手的用户池，并根据定义的标准，在可能的情况下将他们放在一起。

---


# 匹配程序

Nakama 匹配程序功能可以让用户搜索其他用户和与其匹配 – 可作为队友，也可以是对手，从而组织比赛、[群组](../../groups/)，或参与您项目中的任何其他社交功能。

![进行匹配，找到好友、群组、队友或对手]({{< fingerprint_image "/images/pages/nakama/concepts/multiplayer/matchmaker.jpg" >}})

匹配程序维护一个用户池，包含他们的匹配请求（“门票”），并在找到合适比赛时让他们聚在一起。对于任何特定用户，“合适比赛”都是根据其匹配程序门票中定义的[条件](#matchmaking-criteria)来定义的。找到比赛所用时间变化很大，这取决于条件定义的范围有多窄以及匹配池中当前的用户数量。如果匹配时间太长，或完全找不到比赛，请考虑[扩展条件](#expanding-criteria)。

请注意，匹配**仅适用于活跃玩家** – 具有开放套接字连接的用户。用户提交匹配门票后（将自己添加到匹配程序现有玩家池中），用户会一直停留其中，直到找到比赛或他们取消请求。如果用户断开连接，他们的待处理匹配请求也会被取消。

{{< note "important" "Offline Matchmaking" >}}
如果您的用例需要离线匹配，请[与 Heroic Labs 联系](mailto:support@heroiclabs.com)，寻求帮助。
{{< / note >}}

匹配与 Nakama 的[比赛列表](../match-listing/)功能不同。匹配程序用于将用户聚在一起开始新的比赛，比赛列表则用于向用户显示可以立即加入的现有比赛。使用匹配还是比赛列表，是基于您的项目目标和要求的_设计_考虑（而不是_技术_决定）。

## 配置

Nakama [配置](../../../getting-started/configuration/#matchmaker)中有多个参数，可影响匹配程序的工作方式。

通过设置用户随时可拥有的并存门票[最大数值](../../../getting-started/configuration/#matchmaker.max_tickets)，可防止用户提交过多的门票又不取消旧的门票。

设置匹配程序尝试为用户查找“理想”（规模）的比赛的[时间间隔](../../../getting-started/configuration/#matchmaker.interval_sec)，以及准许次优（论规模）比赛之前的[间隔数](../../../getting-started/configuration/#matchmaker.max_intervals)，可调整用户的等候时间长短，并在找到理想的比赛与快速进入比赛之间取得平衡。

反向匹配的[精度](../../../getting-started/configuration/#matchmaker.rev_precision)和[阈值](../../../getting-started/configuration/#matchmaker.rev_threshold)标志用于决定反向匹配精度是否有效以及持续多久。如果启用（`rev_precision`设置为`true`），匹配器将双向验证匹配（即当玩家A与玩家B匹配时，它也将检查玩家B是否与玩家A匹配）。如果匹配不是双向的，匹配器将继续在`rev_threshold'间隔的时间内搜索双向匹配。如果在间隔时间内没有找到双向匹配（默认为1），匹配器将返回单向匹配。

## 匹配条件

要开始匹配用户，请将他们添加到匹配池中。用户可在其匹配程序门票中加入可选的条件，描述其所需的比赛：**[属性](#properties)**、**[最小数和最大数](#minimum-and-maximum-count)**、**[计数倍数](#count-multiple)**和**[查询](#query)**。

### 属性

属性为键值对，为字符串或数值，描述_提交_匹配门票的用户。可在属性中提供的特点的常见例子包括用户的游戏级别、其技能评级、上网地区或选定的比赛类型（例如全部免费、夺标等等）。 

{{< code type="client" >}}
```javascript
const stringProperties = {
  region: "europe"
};

const numericProperties = {
  rank: 8
};
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var stringProperties = new Dictionary<string, string>() {
    {"region", "europe"}
};

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

{{< code type="client" >}}
```cpp
NStringMap stringProperties;
NStringDoubleMap numericProperties;

stringProperties.emplace("region", "europe");
numericProperties.emplace("rank", 8.0);
```
{{< / code >}}

{{< code type="client" >}}
```java
Map<String, String> stringProperties = new HashMap<String, String>() {{
    put("region", "europe");
}};

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

{{< code type="client" framework="godot3" >}}
```gdscript
var string_properties = { "region": "europe" }
var numeric_properties = { "rank": 8 }
```
{{< / code >}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
local string_properties = { region = "europe" }
local numeric_properties = { rank = 8 }
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

用户在开始匹配过程时提交这些属性，每张门票可以有不同的属性。服务器将所有包含的属性合并，组成匹配程序门票中的总体属性。

匹配完成后，所有匹配的用户均可以看到这些属性。如果对客户有帮助的话，您可以在不影响匹配过程本身的情况下存储额外信息 – 只需提交匹配过程中没有查询的属性。

在将用户添加到匹配程序时，还可以使用 before 挂钩以权威方式控制属性：

{{< code type="server" >}}
```go
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
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
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;
}
```
{{< / code >}}

{{< code type="server" >}}
```lua
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")
```
{{< / code >}}

### 最小和最大数

在提交匹配程序请求时，用户必须指定最小和最大数，其中 `minCount` 表示可接受的最小比赛规模，`maxCount` 表示可接受的最大比赛规模，两者都包括提交请求的玩家。 

匹配程序将始终尝试以提供的最大数匹配进行匹配。如果没有足够的用户，就会返回与最大数最接近的规模作为匹配 – 只要它大于最小数。

例如，如使用 `2` 作为最小数和 `4` 作为最大数，匹配程序将尝试找到 3 个其他用户与用户匹配。如没有 3 个匹配的玩家，匹配程序将尝试匹配 2 个其他用户，最后，如果没有 2 个，就匹配 1 个其他用户。

如果没有满足最小数的足够多的用户，则不返回比赛，用户继续停留在池中。

{{< code type="client" >}}
```javascript
const query = "*";
const minCount = 2;
const maxCount = 4;
var ticket = await socket.addMatchmaker(query, minCount, maxCount);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var query = "*";
var minCount = 2;
var maxCount = 4;
var matchmakerTicket = await socket.AddMatchmakerAsync(query, minCount, maxCount);
```
{{< / code >}}

{{< code type="client" >}}
```java
String query = "*";
int minCount = 2;
int maxCount = 4;
MatchmakerTicket matchmakerTicket = socket.addMatchmaker(query, minCount, maxCount).get();
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
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 >}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
local query = "*"
local min_players = 2
local max_players = 4

local ticket = socket.matchmaker_add(min_players, max_players, query)
```
{{< / code >}}

{{< missing type="client" lang="cpp" />}}
{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

提交的最小数和最大数相同时，用户可以搜索精确数量的对手：

<{{< code type="client" >}}
```javascript
const query = "*";
const minCount = 4;
const maxCount = 4;
var ticket = await socket.addMatchmaker(query, minCount, maxCount);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var query = "*";
var minCount = 4;
var maxCount = 4;
var matchmakerTicket = await socket.AddMatchmakerAsync(query, minCount, maxCount);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
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);
```
{{< / code >}}

{{< code type="client" >}}
```java
String query = "*";
int minCount = 4;
int maxCount = 4;

MatchmakerTicket matchmakerTicket = socket.addMatchmaker(query, minCount, maxCount).get();
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
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 >}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
local query = "*"
local min_players = 4
local max_players = 4

local ticket = socket.matchmaker_add(min_players, max_players, query)
```
{{< / code >}}

{{< missing type="client" lang="cpp" />}}
{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

在将用户添加到匹配程序时，还可以使用 before 挂钩以权威方式控制最小数和最大数：

{{< code type="server" >}}
```go
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
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
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;
}
```
{{< / code >}}

{{< code type="server" >}}
```lua
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")
```
{{< / code >}}

### 计数倍数

如果希望可接受的比赛规模为特定数的倍数（例如结果必须为 5 的倍数），可以使用 `countMultiple` 参数。

例如以下请求：

{{< code type="client" >}}
```javascript
const query = "*";
const minCount = 5;
const maxCount = 25;
const countMultiple = 5;
var ticket = await socket.addMatchmaker(query, minCount, maxCount, countMultiple);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var query = "*";
var minCount = 5;
var maxCount = 25;
var countMultiple = 5;
var matchmakerTicket = await socket.AddMatchmakerAsync(query, minCount, maxCount, countMultiple);
```
{{< / code >}}

{{< code type="client" >}}
```java
String query = "*";
int minCount = 5;
int maxCount = 25;
int countMultiple = 5;
MatchmakerTicket matchmakerTicket = socket.addMatchmaker(query, minCount, maxCount, countMultiple).get();
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
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 >}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
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 >}}

{{< missing type="client" lang="cpp" />}}
{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

匹配程序只会返回匹配玩家总数为 5 的倍数的结果，首先尝试返回最大数 25，然后是20、15，依此类推。即使有 23 个匹配玩家，也只返回 20 个玩家的结果。

在将用户添加到匹配程序时，还可以使用 before 挂钩以权威方式控制计数倍数：

{{< code type="server" >}}
```go
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
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
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;
}
```
{{< / code >}}

{{< code type="server" >}}
```lua
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")
```
{{< / code >}}

### 查询

`properties` 描述_搜索_其他玩家的用户，`query` 描述他们_正在搜索_的其他用户的属性。

可以通过 `properties` 前缀查询每个用户的匹配程序属性。您可以根据具有精确匹配数或值范围的属性筛选器组合查找对手。 

请参阅[查询语法](../query-syntax/)，进一步了解可在查询中使用的语法和运算符。

此示例搜索**必须**在 `europe` 且**必须**有介于 5 和 10（含 5 和 10）的 `rank` 的对手：

{{< code type="client" >}}
```javascript
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);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
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);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
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);
```
{{< / code >}}

{{< code type="client" >}}
```java
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();
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
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 >}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
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 >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

可以使用通配符查询`"*"`，忽略对手的属性并与任何人匹配：

{{< code type="client" >}}
```javascript
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);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
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);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
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);
```
{{< / code >}}

{{< code type="client" >}}
```java
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();
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
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 >}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
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 >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

在将用户添加到匹配程序时，还可以使用 before 挂钩以权威方式控制查询：

{{< code type="server" >}}
```go
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
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
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;
}
```
{{< / code >}}

{{< code type="server" >}}
```lua
nk.register_rt_before(function(context, payload)
  -- Force the matchmaking request to use the * query
  payload.matchmaker_add.query = "*"

  return payload
end, "MatchmakerAdd")
```
{{< / code >}}

### 扩展条件

{{< note "error" "Matchmaking Duration" >}}
**无法**保证匹配请求的成功或查找比赛所需的时间长短，因为这都取决于匹配程序中的活动用户池以及搜索的比赛的具体条件。 

重复提交完全一致的请求并不产生不同的结果。因此，不建议对匹配程序请求加以人为的时间限制。
{{< /note >}}

根据活跃用户的数量和匹配中使用的相应条件，有时很难或不可能找到所需的精确匹配。

为了有效地“放宽”正在使用的条件，玩家应该提交多张门票，每张门票的查询都比之前更宽松。 

例如，如果一名玩家希望与本地区具有完全相同技能水平的其他玩家匹配，但没有获得任何结果，那么之后的门票可以扩展到包括其他地区，并允许有一个与该玩家接近的技能水平_范围_。

{{< code type="client" >}}
```javascript
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);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
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);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
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);
```
{{< / code >}}

{{< code type="client" >}}
```java
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();
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
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 >}}

´
{{< code type="client" lang="lua" framework="defold" >}}
```lua
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 >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

## 匹配程序门票

每次将用户添加到匹配程序池时，他们都会收到一张门票，这是表示他们在匹配程序池中状态的唯一标识符。

一个用户在任何给定的时间可以有多张匹配程序门票，每票代表一组不同的条件。例如，一张门票寻找所有现有的全部免费比赛对手，另一张门票寻找当地参加夺标比赛的玩家。

服务器通知客户[匹配成功](#matchmaker-results)时，使用此门票。这可以区别同一用户可能有的多个匹配程序操作。对一张门票的成功匹配**并不**自动取消用户已经打开的任何其他门票。

在门票被执行前，用户可以随时[取消门票](#removing-tickets)。

## 删除门票

如果用户决定不再需要任何先前提交的匹配程序请求，他们可以取消对应的匹配程序门票：

{{< code type="client" >}}
```javascript
socket.removeMatchmaker(ticket);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
// "matchmakerTicket" is returned by the matchmaker.
await socket.RemoveMatchmakerAsync(matchmakerTicket);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
// "ticket" is returned by the matchmaker.
rtClient->removeMatchmaker(ticket, []()
{
    std::cout << "removed from Matchmaker" << std::endl;
});
```
{{< / code >}}

{{< code type="client" >}}
```java
// "matchmakerTicket" is returned by the matchmaker.
socket.removeMatchmaker(matchmakerTicket.getTicket()).get();
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
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 >}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
socket.matchmaker_remove(matchmaker_ticket.ticket)
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

这只会取消指定的门票，不影响匹配池中用户可能会有的任何其他请求。 

记住如果用户在匹配中断开连接，任何打开的门票将被自动取消。无论用户多么迅速地重新建立连接，也无法恢复他们先前的门票。

## 匹配程序结果

匹配并非总是能够即时进行。匹配程序可能会花时间来完成，并异步返回对手结果列表，这随当前连接的用户数的多少而定。

客户应注册一个事件处理程序，当服务器向其发送匹配程序结果时，该事件处理程序将触发。

{{< code type="client" >}}
```javascript
socket.onmatchmakermatched = (matched) => {
  console.info("Received MatchmakerMatched message: ", matched);
  console.info("Matched opponents: ", matched.users);
};
```
{{< / code >}}

{{< code type="client" >}}
```csharp
socket.ReceivedMatchmakerMatched += matched =>
{
    Console.WriteLine("Received: {0}", matched);
    var opponents = string.Join(",\n  ", matched.Users); // printable list.
    Console.WriteLine("Matched opponents: [{0}]", opponents);
};
```
{{< / code >}}

{{< code type="client" >}}
```cpp
rtListener->setMatchmakerMatchedCallback([](NMatchmakerMatchedPtr matched)
{
    std::cout << "Matched! matchId: " << matched->matchId << std::endl;
});
```
{{< / code >}}

{{< code type="client" >}}
```java
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());
    }
};
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
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 >}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
socket.on_matchmaker_matched(function(matched)
  pprint("Received MatchmakerMatched message: ", matched)
  pprint("Matched opponents: ", matched.users);
end)
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

匹配程序结果中将包含所有匹配的用户及其相应的 [`properties`](#properties)。 

结果中还包含令牌或比赛 ID，可用于加入这组匹配玩家的新比赛。比赛类型决定包含什么：对[客户端中继](../relayed/)比赛提供令牌，而对[服务器权威](../authoritative/)比赛则提供比赛 ID。

对于权威比赛，您可以使用服务器挂钩，在匹配程序结果返回时在服务器上创建新的比赛：

{{< code type="server" >}}
```go
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
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
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;
};
```
{{< / code >}}

{{< code type="server" >}}
```lua
nk.register_matchmaker_matched(function(context, matched_users)
    local match_id = nk.match_create("lobby", { invited = matched_users })
    return match_id
end)
```
{{< / code >}}

## 加入比赛

在[客户端中继多人游戏](../relayed/)中，通常使用匹配程序结果事件来加入与匹配对手的新比赛中。匹配的用户**并不**自动加入分配给他们的比赛中。

每个匹配程序结果事件都携带一个令牌（用于加入客户端中继比赛）或一个比赛 ID（用于加入权威比赛）。可用其与匹配对手一起加入比赛。 

在客户端中继多人游戏中，令牌使服务器能够知道这些用户想要一起玩游戏，并将为他们动态创建比赛。

令牌是短期的，必须尽快用于加入比赛。比赛令牌还用于防止无关用户尝试加入未与他们匹配的比赛。令牌过期后，将无法使用或刷新。

可以用标准客户端“比赛加入”操作来加入新比赛：

{{< code type="client" >}}
```javascript
socket.onmatchmakermatched = (matched) => {
  console.info("Received MatchmakerMatched message: ", matched);
  const matchId = null;
  socket.joinMatch(matchId, matched.token);
};
```
{{< / code >}}

{{< code type="client" >}}
```csharp
socket.ReceivedMatchmakerMatched += async matched =>
{
    Console.WriteLine("Received: {0}", matched);
    await socket.JoinMatchAsync(matched);
};
```
{{< / code >}}

{{< code type="client" >}}
```cpp
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;
    });
});
```
{{< / code >}}

{{< code type="client" >}}
```java
SocketListener listener = new AbstractSocketListener() {
    @Override
    public void onMatchmakerMatched(final MatchmakerMatched matched) {
        socket.joinMatchToken(matched.getToken()).get();
    }
};
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
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])
```
{{< / code >}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
socket.on_matchmaker_matched(function(matched)
  pprint("Received:", matched);
  socket.match_join(matched.token)
end)
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

## 派对匹配

Nakama 的[实时派对](../../parties/)让用户可以结成短期团队（只持续给定的一段时间），一起玩游戏。形成派对后，这些玩家可以一起匹配，确保他们最终都被分配到同一场比赛。

每个派对都有一个指定的组长，通常是创建该派对的用户。组长设置[条件](#matchmaking-criteria)，用于匹配，并将该派对加入匹配程序池：

{{< code type="client" >}}
```javascript
// 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);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
// 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, false, 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);
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
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")
```
{{< / code >}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
-- 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 >}}

{{< missing type="client" lang="cpp" />}}
{{< missing type="client" lang="java" />}}
{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

在匹配过程中，在任何返回的结果中，派对成员将始终在一起。派对既可以与其他派对也可以与个人用户匹配，最终组成比赛，匹配程序对两者没有偏好。 

例如，给出[最大数](#minimum-and-maximum-count) 10，5 人派对会与另一个 3 人派对匹配，然后与两个单独的用户匹配，组成完整的比赛。

匹配成功后，**所有**派对成员（而不只是组长）都会接收到[匹配程序结果](#matchmaker-results)回调。
