亚马逊AWS官方博客

Amazon GameLift 高阶使用技巧(1)- flexmatch 多模式匹配的实现

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

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

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

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

什么是 FlexMatch?

简单来说,FlexMatch 是 Amazon 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}}]
PowerShell

设定的条件为:每个玩家的延迟在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)))"

  }
PowerShell

规则集验证

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

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

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

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

架构图

压测程序

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

  • https://github.com/aws-samples/sample-simulator-gamelift-flexmatch-matchmaking

主要是利用了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

      }

    }

  }

}
PowerShell

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

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

  }
PowerShell
  • 拆分了规则池子,看起来玩家池子变少了,可以用一些设计手段进行规避。通常,游戏设计者会让玩家选择多种游戏模式来加快匹配。利用这点,可以同时发送多个匹配到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

},
PowerShell
  • 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"

        }

}
PowerShell

结论

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

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

参考

本篇作者

刘幸园

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

万曦

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