亚马逊AWS官方博客

HAQM GameLift 高阶使用技巧(一)- FlexMatch 多模式匹配的实现

在当今的对战游戏世界中,玩家之间的对抗不仅仅是技术的较量,更是策略与团队协作的综合展示。想象一下,在一场紧张刺激的比赛中,Radiant(近卫)和 Dire(天灾)两个队伍正准备展开殊死搏斗。为了确保每位玩家都能享受到最佳的游戏体验,匹配系统的设计显得尤为重要。理想的情况下,参与者之间应具备相似的技能水平、低延迟的网络连接,以及相同队伍的人数配置,这样才能让每场比赛都充满公平竞争与乐趣。

然而,现实中找到完美匹配并非易事。为了提升游戏体验,让玩家们快速匹配到合适的队友,公平决一胜负,匹配策略尤其重要。如何合理的设计规则,如何在找不到合适的人时通过设置动态扩展规则,逐渐放宽条件都是我们需要考虑的问题。随着游戏的发展,我们可能推出更多的玩法和模式,比如生存赛、练习赛和其他新奇的模式,不同的模式,对玩家的匹配要求也不完全一样。比如

  • 练习赛侧重的是让玩家熟悉操作和技能,匹配要求可以相对宽松。
  • 生存赛则强调竞技性和挑战性,需要更高的技能匹配,确保公平竞争。

面对这样的挑战,这时候我们需要一套更灵活、更强大的匹配解决方案。市面上已经有许多成熟的解决方案,比如 GameLift FlexMatch、OpenMatch 等,后续我们也会专门发布一篇对比文章,详细分析这些匹配方案的特点、优势和适用场景,帮助大家选择最适合你游戏的方案。本次主要基于 HAQM GameLift FlexMatch 方案进行介绍,它能根据不同的游戏模式、玩家需求和开发目标,帮助开发者打造更为出色的匹配系统,让每位玩家都能在公平竞争中尽情享受游戏带来的乐趣与挑战。

什么是 FlexMatch?

简单来说,FlexMatch 是 HAQM GameLift 提供的一个玩家匹配服务,它允许开发者根据游戏需求自定义匹配规则。它有几个主要特点:

  • 托管服务:可以和 GameLift 一起用,也能独立使用。
  • 玩家分队支持:能处理各种分队逻辑,比如不同阵营或队伍配置。
  • 动态扩展规则:匹配时间长了,规则会逐步放宽,提高匹配成功率。
  • 重连与掉线处理:玩家掉线了也可以重新回到匹配中。

 规则集是什么?

在 FlexMatch 里,规则集是一个非常强大的功能,允许开发者根据各种复杂的条件来定义和管理玩家匹配的逻辑。匹配规则是通过 JSON 文件来定义的。一个规则集通常包含:

  1. 自定义属性:比如技能等级(skill)、游戏模式等。
  2. 团队定义:比如两个阵营(Radiant vs Dire)。
  3. 匹配规则:确保技能水平相近、队伍人数平衡等。
  4. 规则扩展:等待时间太长时,可以逐步放宽匹配条件,比如技能差距从 10 扩大到 20 再到 50。

示例规则:

关于详细的规则集编写与使用可以参考文档建立 FlexMatch 规则集

随着游戏模式越来越多,匹配规则会变得越来越复杂。有些开发者把所有模式的规则都放在一个规则集中,而有些开发者则选择为每个模式单独创建规则集。接下来我们将基于一个具体的例子介绍在 FlexMatch 实现多模式匹配中,这两种方案的一些实用经验和注意事项。

游戏规则

假设我们需要创建一个游戏规则:两个游戏阵营 Radiant(近卫)和 Dire(天灾),游戏有三种模式:

  • 经典:每个阵营 4-6 人
  • 生存:每个阵营 1-3 人
  • 练习:每个阵营 2-8 人

玩家主要的游戏属性有三个:延迟(LatencyInMs)+技能值(skill)+游戏模式(GameMode)

Radiant-Dire-Survival [{'PlayerId': 'player-2085831', 'PlayerAttributes': {'skill': {'N': 703}, 'GameMode': {'SL': ['Survival']}}, 'LatencyInMs': {'us-east-1': 61}}]

Radiant-Dire-Practice [{'PlayerId': 'player-2239045', 'PlayerAttributes': {'skill': {'N': 912}, 'GameMode': {'SL': ['Practice']}}, 'LatencyInMs': {'us-east-1': 53}}]

Radiant-Dire-Classic [{'PlayerId': 'player-2857070', 'PlayerAttributes': {'skill': {'N': 1161}, 'GameMode': {'SL': ['Classic']}}, 'LatencyInMs': {'us-east-1': 100}}]

Radiant-Dire-All [{'PlayerId': 'player-4184596', 'PlayerAttributes': {'skill': {'N': 1487}, 'GameMode': {'SL': ['Practice', 'Classic']}}, 'LatencyInMs': {'us-east-1': 73}}]

设定的条件为:每个玩家的延迟在 80ms 以内,技能差值在 20 以内。如果超过一定的匹配时间,可以适当地放宽一些匹配条件。

分离规则集

常规的做法,按照游戏多少种模式,设计对应数目的规则集合。然后根据玩家的选择的游戏模式,向对应的规则匹配器发送匹配要求即可。比如按照前面的设计,我们会获得下面三种规则集合:

这种设计方法带来的挑战有:

  • 设计简单,但是多次匹配调用逻辑复杂
  • Gamelift 对应 API 的调用次数翻倍,有可能会被 aws api 的默认额度限速

还有一个所有的游戏设计者关心的问题:多个游戏规则,等价于玩家池子被拆分了,那么自然会考虑玩家匹配时间是否会被拉长。

单一规则集

一种做法是将所有的模式混合在一个大规则里面,这样做的好处是可以保证匹配的时候会有一个比较大的匹配池子。

然而,这种设计方法的也有如下挑战:

  • 多种规则混合的设计,对于掌握 flexmatch 语法规则有挑战
  • 复杂规则集合后期难于维护,会增加匹配的时间。
  • 由于 ruleset 中 player attribute 最多只有 10 个属性,而且一个 hardlimit,会导致添加新的玩法时候,发现 player attribute 不够用的时候。

这里还有一个隐含的问题:复杂规则会通常在多个模式种选择一个,会用到 or 和 and 的逻辑(compound 规则),由于 statement 通常会从左到右的进行评价,会不会经常在左边的条件就终止了,而导致后面的游戏模式较少的被选中?

{

      "name": "GameModeRule",

      "type": "compound",

      "statement": "or(and(Survival-Mode, FairTeamSkill-Survival), or(and(Classic-Mode,FairTeamSkill-Classic),and(Practice-Mode,FairTeamSkill-Practice)))"

  }

规则集验证

为了解决上面的疑惑,最好的方式就是进行 flexmatch 的压测。模拟大量的玩家在单一或者分离规则集合的不同情况下的表现。不过,一个真实的 gamelift + flexmatch 的环境通常会包含以下几个模块:

  • Builds/Scripts: 游戏服务器部分
  • Fleet: 匹配舰队,游戏会话放置的目的地
  • Queue: 匹配队列,按照价格,容量,延迟等因素来选择不同的 Fleet
  • Notification: 通知系统,一般通过 sns 的消息机制来完成
  • Ruleset: 匹配规则集合
  • Configuration:匹配设置,串联前面的各个模块

在这些模块中,queue 中选择 fleet 对应的机型,通常是很大的不可控因素。比如地理位置导致的延迟,供需关系引起的价格波动,甚至你使用的 fleet 数目都会对于端到端的匹配时间有很大的影响。

因此我们使用的是 flexmatch standalone 模式。在这种模式下,只需要考虑匹配器本身即可。来验证规则集合在不同模式下对于游戏匹配时间的影响。可以参考的架构图如下:

架构图

压测程序

为此,我们设计了一个压测程序,项目地址为:

主要是利用了 aws cli 的两个命令:start_matchmaking 和 describe_matchmaking。其设计思路很简单:通过按照一定规则程序产生的大量玩家数据,来模拟并发调用不同的规则集合,然后去监控前面返回的匹配票据状态即可。同时,可以开启匹配的 AcceptanceRequired,这样就需要同时再模拟调用 accept_match 的接口。

该程序支持 json 格式的配置,比如:

{

  "version": "1.0",

  "aws":{

    "region": "us-east-1"

  },

  // acceptance > 0 代表是否需要客户端接受匹配以及超时时间; =0 则无接受需求

  // name 为匹配器的名字

  // ruleset 为匹配器用到的匹配规则名字,需要提前写在Config的目录下

  "flexmatch":{

    "configurations": [{

      "name": "Radiant-Dire-All",

      "acceptance": 15,

      "ruleset":"RadiantDire-All"

    },{

      "name": "Radiant-Dire-Survival",

      "acceptance": 15,

      "ruleset":"RadiantDire-Survival"

    },{

      "name": "Radiant-Dire-Practice",

      "acceptance": 15,

      "ruleset":"RadiantDire-Practice"

    },{

      "name": "Radiant-Dire-Classic",

      "acceptance": 15,

      "ruleset":"RadiantDire-Classic"

    }]

  },

  "benchmark":{

    "ticketPrefix": "benxiwan-",

    "logs": "output.txt", // 日志名称

    "totalPlayers": 30, // 总共多少玩家参与匹配

    "gameModes": [ "Classic", "Practice", "Survival" ], // 游戏模式, ALL会随机选一种或多种

    "acceptance": {

      "rate": 0.9, // acceptance-rate: 客户端接受匹配的概率

      "timeout": 10 // acceptance-timeout: 客户端接受匹配的超时时间

    },

    // 玩家组队规模

    "teamSize": {

      "default": 5,

      "small": 2

    },

    // 玩家数据分布

    "playerData":{

      "latency": {

        "median": 70,

        "std_dev": 20

      },

      "skill": {

        "median": 1000,

        "std_dev": 400

      }

    }

  }

}

然后通过一条简单的命令参数即可更新和关联新的匹配规则。目前具体支持的命令行有:

Options:

-help: Show this help message

-json: Output json config

-flexmatch: Update flexmatch sets

-benchmark: Start a benchmark

批量更新配置

压测之前,我们可以通过 -flexmatch 命令来保证匹配器的设置为最新的

开启压测

注意:为了模拟玩家因为分开池子后的减少情况,我们会在非全匹配的模式中,加倍每个请求的随机等待时间。比如原来只有 1-3 秒,现在变成了 2-6 秒。

2000 个模拟玩家(没有开启 acceptance):

Matchmaking Monitor for [Radiant-Dire-All] Done!
Complete Tickets: 815, Average Time: 17.17 seconds
Failed Tickets: 20, Average Time: 120.13 seconds
Matchmaking Monitor for [Radiant-Dire-Practice] Done!
Complete Tickets: 823, Average Time: 16.74 seconds
Failed Tickets: 24, Average Time: 120.11 seconds
Matchmaking Monitor for [Radiant-Dire-Survival] Done!
Complete Tickets: 1658, Average Time: 14.08 seconds
Failed Tickets: 18, Average Time: 120.09 seconds
Matchmaking Monitor for [Radiant-Dire-Classic] Done!
Complete Tickets: 829, Average Time: 17.12 seconds
Failed Tickets: 11, Average Time: 120.11 seconds

2000 个模拟玩家(开启 acceptance):

Matchmaking Monitor for [Radiant-Dire-All] Done!

Complete Tickets: 175, Average Time: 36.08 seconds

Failed Tickets: 508, Average Time: 41.61 seconds

Matchmaking Monitor for [Radiant-Dire-Practice] Done!

Complete Tickets: 319, Average Time: 30.68 seconds

Failed Tickets: 342, Average Time: 34.14 seconds

Matchmaking Monitor for [Radiant-Dire-Classic] Done!

Complete Tickets: 311, Average Time: 34.03 seconds

Failed Tickets: 376, Average Time: 35.61 seconds

Matchmaking Monitor for [Radiant-Dire-Survival] Done!

Complete Tickets: 1028, Average Time: 24.03 seconds

Failed Tickets: 309, Average Time: 29.45 seconds

可以看到,规则拆分开以后,无论是否开启 acceptance,平均的匹配时间都是有一定的程度的缩短,并不会如之前想象的那样因为池子的变小导致了匹配时间变长。这里面根据分析得出来的结论有:

  • 较为复杂的匹配规则同样会更多的消耗 FlexMatch 服务本身的算力,除了平均时间变长以外,最大值和最小值之间的差距也会拉大。比如下面的这个规则明显就过于复杂了。
{

      "name": "GameModeRule",

      "type": "compound",

      "statement": "or(and(Survival-Mode, FairTeamSkill-Survival), or(and(Classic-Mode,FairTeamSkill-Classic),and(Practice-Mode,FairTeamSkill-Practice)))"

  }
  • 拆分了规则池子,看起来玩家池子变少了,可以用一些设计手段进行规避。通常,游戏设计者会让玩家选择多种游戏模式来加快匹配。利用这点,可以同时发送多个匹配到 flexmatch,然后以哪一个匹配最先撮合成功为标志,主动地拒绝其他匹配即可。这样的做法已经在很多客户实际案例上使用了。下图展示了一个真实的客户案例,当它们采用了分离式规则集合以后,端到端的匹配延迟(time-to-match)看到了明显的好转。

观察

以下是我们对于不同规则集类型的优缺点对比的汇总表格,最终我们通过使用单一规则集同时发送多个匹配到 flexmatch 来综合两者的优点来实现。

维度 单一规则集 分离规则集
玩家匹配质量 所有 ticket 在一个池中,匹配质量更高。 ticket 被分成多个较小池,匹配质量下降。
属性需求 需要更多属性以对应不同模式的规则。 只需每种模式特定的属性,属性需求较少。
最小/最大人数约束 所有模式共享约束,规则定义更复杂。 每种模式有独立约束,简化了实现。
维护复杂度 需要 compound 组合大量 AND/OR 规则,易出错,维护复杂。 规则独立,简单易维护。
匹配耗时 单一规则集需判断更多复杂条件,时间更长。 每个规则集判断的条件较少,时间更短。

关于 acceptance timeout seconds 设置的建议,考虑到接受 event 的速度,响应调用 API 耗时,偶尔的网络抖动等原因,可以考虑先设置为 5-10 秒。更详细的设置可以考虑计算同一个 match id 的 PotentialMatchCreated 事件和 AcceptMatchCompleted 事件之间的时间差值,从而分析为什么系统未能按预期进行自动接受/超时。还可以使用相同的策略与 AcceptMatch 事件结合,计算 Ticket 响应接受匹配请求所需的时间。可以通过观察 MatchAcceptancesTimedOut 指标,观察到超时的匹配数量,而进行进一步的调整。

其他相关

  • flexmatch 中如何开启黑名单?

游戏中黑名单是一个常见需求,在 flexmatch 中我们通常会用 player attribute 的 list 属性来放置玩家名单。不过它会面临 100 个长度的上限。因此, 对于超过百人的名单,需要多个 player attribute 来配合。

// player attributes 部分

{

    "name": "black_list",

    "type": "string_list",

    "default": []

}  

// rule 部分....

{

    "name": "PlayerIdNotInBlackList",

    "type": "collection",

    "operation": "reference_intersection_count",

    "measurements": "flatten(teams[*].players.attributes[black_list])",

    "referenceValue": "flatten(teams[*].players[playerId])",

    "maxCount": 0

},
  • flexmatch 的 standalone 模式如何知道匹配结果?

在 standalone 模式中,由于不会真实的拉起服务器,因此是没有 GameSessionInfo 相关的信息。需要等待 SNS 的 MatchMakingSucceeded 的事件。比如下面的例子:

{

        "version":"0",

        "id":"840e11aa-f309-4995-bd66-1025fd4f7550",

... // other metadata

        "detail":{

                "tickets":[

... // tickets info

                ],

                "type":"MatchmakingSucceeded",

                "gameSessionInfo":{

                        "players":[

                                {

                                        "playerId":"player-2805118",

                                        "team":"Radiant"

                                },

...// other players

                                {

                                        "playerId":"player-4954051",

                                        "team":"Radiant"

                                }

                        ]

                },

                "matchId":"dd07e64c-fd5c-4557-a1b5-5fe2a3a5bdd2"

        }

}

结论

FlexMatch 是 GameLift 的一个强大且灵活的补充组件。它不仅可以适用于 10 人以内的小型匹配场景,也能支持上百人的大规模匹配。目前,全球有许多知名游戏公司已在其游戏中采用了这项服务。它不是强制性需求,可以根据需求选择接入,并编写出各种强大且公平的匹配方案。

然而,我们必须承认,前期的测试无法完全模拟真实的游戏环境。生产环境通常情况下是多种多样的,您需要时刻考虑机器容量规划、区域选择,甚至游戏玩法等因素。还有一个不容忽视的,就是有些游戏会把 AcceptMatch 当作匹配成功的必要条件。这些因素远比 FlexMatch 本身复杂且具体。但在这里,我们主要想向开发者展示一种优化方式:无需关注运营商和底层硬件设备,只需调整 ruleset 的编写方式就能达到不错的效果。

系列博客

HAQM GameLift 高阶使用技巧(二)- 使用 GameLift Container Fleet 运行 UE5 Dedicated Server

参考链接

本篇作者

刘幸园

亚马逊云科技客户技术经理。主要负责游戏、互联网行业客户的架构优化、成本管理、技术咨询等工作。拥有 10 年以上的数据库优化、项目管理与技术支持经验。

万曦

亚马逊云科技解决方案架构师,负责基于亚马逊云科技的云计算方案的咨询和架构设计。坚实的AWS Builder文化拥抱者。拥有超过12年的游戏研发经验,参与过数个游戏项目的管理和开发,对于游戏行业有深度理解和见解。