Amazon GameLift FlexMatch のルールセットの書き方をステップバイステップで解説

[AWS & Game Advent Calendar 2020](https://qiita.com/advent-calendar/2020/aws-game) の 21 日目の記事になります。

Amazon GameLift FlexMatch でマッチングルールセットの記述方法を簡単な例をベースにスッテプバイステップで解説付きで記載します。
「GameLift FlexMatch を使ってみたいけど、どのようにルールを記述すればよいかわからない」といった方向けの記事となっています。

# GameLift FlexMatch とは？

GameLift FlexMatch の概要については、本アドベントカレンダーの[@oreo367](https://qiita.com/oreo367) さんによる[Amazon GameLift FlexMatch でマッチメイキング“だけ”動かしてみる](https://qiita.com/oreo367/items/059ea0cfd9ad04ef82da) に記載されているのでそちらをご参照ください。この記事にも記載されていますが、2020 年 11 月のアップデートにより、GameLift FlexMatch を Stand alone で利用することが可能になっています！！🎉👏
[AWS announces General Availability of Amazon GameLift Feature Update](https://aws.amazon.com/jp/blogs/gametech/gamelift-flexmatch-now-generally-available/)

# GameLift FlexMatchにおけるマッチング開始までの流れ

GameLift FlexMatch のルールを説明する前に、改めて GameLift FlexMatch におけるマッチングの説明を簡単に記載します。
![スライド1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/883923/1f008969-97c1-c568-4756-f2eb59a1dc91.png)

1. ゲームクライアントがクライアントサービスにゲームプレイリクエストを送信
1. クライアントサービスがマッチメイキングリクエストを GameLift FlexMatchに送信
1. GameLift FlexMatch がマッチングチケットを発行し、チケットIDをクライアントサービスに返信
1. GameLift FlexMatch の**マッチメイキング設定において指定されたルールセットに従って、チケットプール内のチケットでマッチング**を実施し、チケットステータスを更新

上記の流れに沿って、GameLift FlexMatch のマッチングが実施されます。この 4 のステップにおけるルールセットがマッチメイキングの肝となります。

#  FlexMatch のルールセットについて
GameLift FlexMatch のルールセットが行うことは基本的に以下の2点であり、様々なオプションを用いてルールセットをカスタマイズすることが可能です。

* マッチングのチーム構造とサイズを決定
* 可能な限り最良のマッチを見つけるために、プレーヤーをどのように評価するかを指示

このルールセットはJSON 形式で記述します。詳細なパラメータを省略した大まかな構造を記載してみました。ここまでシンプルにすると、かなりとっつきやすいのではないでしょうか。

```
{
    "name": "string", 
    "ruleLanguageVersion": "1.0",
    "playerAttributes":[{...}],
    "algorithm": {...},
    "teams": [{...}],
    "rules": [{...}],
    "expansions": [{...}]
    }]
}
```
それぞれのコンポーネントの内容を簡単に以下に記載します。スキーマの詳細を確認したい方は[FlexMatchルールセットスキーマ](https://docs.aws.amazon.com/ja_jp/gamelift/latest/developerguide/match-ruleset-schema.html)をご参照ください。

* **name** : ルールセットを説明するラベル  [**必須**]
* **ruleLanguageVersion** : ルール言語のバージョン(現時点で1.0のみ)  [**必須**]
* **<font color="DarkOrange">playerAttributes</font>** : プレイヤーデータの属性 
* **<font color="DarkOrange">algorithm</font>** : マッチメイキングプロセスのアルゴリズムをカスタマイズする設定
* **<font color="DarkOrange">teams</font>** : チームの構造とサイズ範囲  [**必須**]
* **<font color="DarkOrange">rules</font>** : マッチングのプレイヤーの評価方法
* **<font color="DarkOrange">expansions</font>** : マッチング完了できない場合の時間経過に伴うマッチング要件を緩和する設定

この内、ルールに直接作用する操作可能な項目は「**<font color="DarkOrange">playerAttributes</font>**」「**<font color="DarkOrange">algorithm</font>**」「**<font color="DarkOrange">teams</font>**」「**<font color="DarkOrange">rules</font>**」「**<font color="DarkOrange">expansions</font>**」の**5つ**のコンポーネントとなります。また「**teams**」のみ必須となっています。
では、実際にこの構造でどのようにルールを組み立てていけばいいのか、例をもとに順を追って説明します。

# ルールセットを1から作ってみる
今回、2-4 vs 2-4 のオンラインFPS対戦ゲームを例として、5つのコンポーネントを使って順番にルールを組み立てていきます。

## 1.teams の追加

まずは、必須条件となっている「**teams**」を追加します。このコンポーネントでは、チームの構造とサイズを設定します。今回は 2-4 vs 2-4 なので、以下のように記載することができます。また、各チームに Red / Blue という名前をつけています。

```json
{
    "name": "fpsgame",
    "ruleLanguageVersion": "1.0",
    "teams": [{
        "name": "Red",
        "maxPlayers": 4,
        "minPlayers": 2
    }, {
        "name": "Blue",
        "maxPlayers": 4,
        "minPlayers": 2
    }]
}
```

最低限のルールセットの内容としてはこれだけです。流石に寂しいので、このルールをベースに順を追って拡張していきましょう。


## 2.rules の追加

今回のチーム構造としては、最小チーム人数と最大チーム人数に差があるので、ゲーム開始時にチームに人数差が発生する可能性があります。そこで、今回の場合、Red チームのプレイヤー人数と Blue チームのプレイヤー人数が一致している場合のみ開始するように条件を設定したいと思います。
特定の条件を設定したい場合は、「**rules**」コンポーネントを利用します。FlexMatch の「**rules**」ではいくつかのルールタイプをサポートしています。値を比較して評価したい場合は、comparison というルールタイプを利用することで、記述可能です。例では、count 関数を使って、Red チームのプレイヤーと Blue チームのプレイヤー人数が一致しているかどうかを確認しています。このようにあらかじめ用意された関数等を利用して集計して評価することが可能です。
他のルールタイプや関数等について知りたい場合は、[FlexMatchルール言語](https://docs.aws.amazon.com/ja_jp/gamelift/latest/developerguide/match-rules-reference.html)のドキュメントを参照してください。

```json
"rules": [{
            "name": "EqualTeamSizes",
            "type": "comparison",
            "measurements": [ "count(teams[Red].players)" ],
            "referenceValue": "count(teams[Blue].players)",
            "operation": "="
         }]
```

## 3.playerAttributes (と rules )の追加

対戦型ゲームでは、試合が均衡するようにプレイヤーのスキルに基づいてマッチングを行うケースがあります。では、スキルがチーム間で均等になるように評価を行うルールを追加してみましょう。
プレイヤースキルといったプレイヤーの属性に基づいてルールを設定する場合、「**playerAttributes**」を利用します。この属性は、マッチメイキングリクエスト時に Gamelift FlexMatchに送信することが可能です。今回は、1000を基準とするユーザのスキルレートをもとにルールを設定しようと思いますので、「**playerAttributes**」として Skill を追加します。

```json
"playerAttributes": [{
        "name": "Skill",
        "type": "number",
        "default": 1000
}]
```

では、次にこのパラメータを使って、チームのプレイヤーのスキルが均等になるように「**rules**」を作成します。プレイヤーが持つスキルレベル間の差(距離)を評価したいので、distance というタイプを使用します。マッチングにおける各チームの平均スキルレベル(measurementsにて設定)とすべてのプレイヤー平均スキルレベル(referenceValueにて設定)が100以内に収まるようにルールを記述し、先程の EqualTeamSizes と同じように「rules」に追加します。

```json
"rules": [{
    "name": "FairSkill",   
    "type": "distance",
    "measurements": [ "avg(teams[*].players.attributes[Skill])" ],
    "referenceValue": "avg(flatten(teams[*].players.attributes[Skill]))",
    "maxDistance": 100
},{
    "name": "EqualTeamSizes",
    "type": "comparison",
    "measurements": [ "count(teams[Red].players)" ],
    "referenceValue": "count(teams[Blue].players)",
    "operation": "="
}]
```
このように 「**rules**」 では、複数のルールを設定することが可能ですが、それらは AND 条件として評価されます。そのため、マッチングはすべてのルールを満たした際に成立します。


## 4.expansions の追加

ここまでチーム人数・スキルレベル調整を「**rules**」に設定しましたが、場合によっては、条件を満たすプレイヤーが存在せず、マッチングが完了しない可能性があります。このような場合、マッチングの時間経過に伴って、マッチング要件を緩和する仕組みを「**expansions**」を使って記述することができます。
例として、FairSkill というスキルマッチを行うルールを緩和させる例を以下に記載します。この例では、30秒後に maxDistance を100 -> 200 に、40秒後に200 -> 300 に変更しています。

```json
"expansions": [{
        "target": "rules[FairSkill].maxDistance",
        "steps": [{
            "waitTimeSeconds": 30,
            "value": 200
        }, {
            "waitTimeSeconds": 40,
            "value": 300
        }]
}]
```

## 5.algorithm の追加
このコンポーネントを理解するためには、まず FlexMatch がどのようにマッチメイキングのプロセスをすすめるかを知る必要があります。Gamelift FlexMatch におけるデフォルトの FlexMatch マッチメイキングプロセスは次のとおりです。

1. すべてのオープンなマッチメイキングチケットとバックフィルチケットは、チケットプールに配置されます。
2. チケットはマッチングのために 1 つ以上のバッチにランダムにグループ化されます。チケットプールのサイズが大きすぎる場合のみ、マッチング最適化のために複数のバッチが生成されます。
3. 各バッチはチケットの古い順によってソートされます。
4. 最も古いチケットから順に、バッチ内のすべてのチケットが、マッチ条件を満たすか評価されます。

このデフォルトのアルゴリズムは、ほとんどのゲームにおいてマッチングリクエストを迅速かつ効率的に処理するために設計されています。しかしながら、状況やニーズによっては、このプロセスをカスタマイズしたい場合があります。そのような場合、プロセスのアルゴリズムを修正する方法として、「**algorithm**」 コンポーネントがあります。

それでは、この「**algorithm**」で設定できる例を一つ見てみましょう。今回のゲームでは、最小 2 vs 2 でゲームが開始します。ただし、4 vs 4 のように可能な限り最大人数でゲームを進行するように設定したいと思います。
この場合、マッチメイキング設定で自動バックフィルをオンにすることで、少ない人数で開始したゲームセッションの人数を自動的に埋めることが可能になります。さらに、ルールセットにて「**algorithm**」 を追加し、backfillPriority を high に設定することで、バックフィルを優先させることが可能になります。backfillPriority を有効にする場合は、strategy を exhaustiveSearch(デフォルト) に設定する必要があります。

```json
"algorithm": {
    "backfillPriority": "high",
    "strategy": "exhaustiveSearch"
}
```

最終的に完成したルールセットは以下のようになります。

```json
{
    "name": "fpsgame",
    "ruleLanguageVersion": "1.0",
    "playerAttributes": [{
            "name": "Skill",
            "type": "number",
            "default": 1000
        }],
    "algorithm": {
        "backfillPriority": "high",
        "strategy": "exhaustiveSearch"
    },
    "teams": [{
            "name": "Red",
            "maxPlayers": 4,
            "minPlayers": 2
        },{
            "name": "Blue",
            "maxPlayers": 4,
            "minPlayers": 2
        }],
    "rules": [{
            "name": "FairSkill",   
            "type": "distance",
            "measurements": [ "avg(teams[*].players.attributes[Skill])" ],
            "referenceValue": "avg(flatten(teams[*].players.attributes[Skill]))",
            "maxDistance": 100
        },{
            "name": "EqualTeamSizes",
            "type": "comparison",
            "measurements": ["count(teams[Red].players)"],
            "referenceValue": "count(teams[Blue].players)",
            "operation": "="
        }],
    "expansions": [{
        "target": "rules[FairSkill].maxDistance",
        "steps": [{
                "waitTimeSeconds": 30,
                "value": 200
                }, {
                "waitTimeSeconds": 40,
                "value": 300
            }]
        }]
}
```


# 設計方針のまとめ

例を踏まえた手順では5つのコンポーネントを追加して、ルールセットを作成しました。改めて一からルールを作成するなら個人的には「こういった形ですすめるかな」という例をまとめます。

1. 「**team**」 の構造を決定する
1. マッチング時にどのうような「**rules**」 を設定したいか検討
1. 「**rules**」で必要になる「**playerAttributes**」を定義
1. 「**rules**」 を実装
1. 「**expansions**」 による段階的なマッチング条件の緩和を検討
1. (オプショナル)「**algorithm**」のカスタマイズが必要かを検討

難しいところが、2-4 の実際にルールを組むステップだと思います。2.の検討段階で、以下の公式ドキュメントを参照して、予めどのようなルールタイプ・式・関数が利用できるのかを確認し、希望するルールの実現可能性を検討する形が良いかと思います。
[FlexMatchルールセットスキーマ](https://docs.aws.amazon.com/ja_jp/gamelift/latest/developerguide/match-ruleset-schema.html)
[FlexMatchルール言語](https://docs.aws.amazon.com/ja_jp/gamelift/latest/developerguide/match-rules-reference.html)

また、この記事で紹介したようなサンプルがいくつか以下のドキュメントにはありますので、こちらを踏まえて色々なパターンを確認すると良いかなと思います。
[FlexMatchルールセットの例](https://docs.aws.amazon.com/ja_jp/gamelift/latest/developerguide/match-examples.html)

補足ですが、上記の方針は前提として大規模でないマッチング(40人以下)が前提となります。大規模なマッチングを作成する場合は、より最適化したアルゴリズムに設定するために、algorithm の strategy や balancedAttribute といった設定を行うことが推奨されています。詳細はこちらをご参照ください。
[大規模なマッチングの一致アルゴリズムをカスタマイズする](https://docs.aws.amazon.com/ja_jp/gamelift/latest/developerguide/match-design-ruleset.html#match-design-rulesets-large-algorithm)

# 最後に
この記事を読んだ方が、GameLift FlexMatch でどのようにルールを作ればいいか、少しでもイメージできるようになれば嬉しいです。
また、GameLift FlexMatch のスタンドアローン版も提供開始され、触りやすくなったかと思います。ぜひこれを機に試してみると面白いかなと思います。
今後は「え？これできるの？」というようなパターンのルールセットを集めたサンプル集を作りたいです。