Azure PlayFabのとても便利なマッチメイキング（マッチング）機能を使ってみた

## まえがき
マッチメイキングの処理はオンラインゲームにはかかせないですよね。

でもマッチメイキングを実装しようとすると、やりたいことはプレイヤーに適切なルームやバトルのIDを渡したいだけなんですが、様々なルール（プレイヤーのレベルやレートの考慮、1vs1 or チームバトル or バトルロイヤル）や排他制御を考える必要があって大変です。

そんなマッチメイキングを PlayFab を使えば簡単に実装できると聞いたので試してみました。

※今回は複雑なルールは取り上げません。
※マッチメイキングは Public Preview の機能です。

## 作ってみたもの
![matchmakingsample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/791485e4-ef6f-5d01-8ca3-be90dc9c39de.gif)

## PlayFab を使ったマッチングの流れ
- 事前に GameManager でマッチング処理用のキューを作り、バトルの人数やマッチングする条件を定義します。
- Player が上記のキューに MatchmakingTicket というチケットを投げます。
- キューにバトルの人数分のチケットが貯まるとマッチングされます。
- Player が投げたチケットをポーリングしておけば MatchID が取得できるので、同じMatchID の Player を集めてバトルを開始したりします。

## 使用する PlayFab の API
- [CreateMatchmakingTicket - チケットを作ってキューに投げるときに使います](https://api.playfab.com/documentation/multiplayer/method/CreateMatchmakingTicket)
- [GetMatchmakingTicket - チケットのステータスををポーリングするときに使います](https://api.playfab.com/documentation/multiplayer/method/GetMatchmakingTicket)
- [CancelMatchmakingTicket - マッチングをキャンセルするときに使います](https://api.playfab.com/documentation/multiplayer/method/CancelMatchmakingTicket)

## 1. 事前準備（GameManager でマッチング処理用のキューを作っておく）
GameManager へログインし、マルチプレイヤー > マッチメイキング > 新しいキューをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/3870494f-72d6-b9b9-e46a-1d3d88322196.png)

キューの構成を変更してキューを作成します。
必須項目は `キュー名` と `対戦人数` です。
今回は 1vs1 のマッチングを作りますので、それっぽいキュー名を指定し、対戦人数は必ず2人なので 2to2 に設定しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/8eda0436-b530-518e-77de-e14f09d159ac.png)
 
キューができました。
GameManager での事前準備はここまでです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/8ce566a6-70e3-355a-a091-726b0f9fe6a1.png)

## 2. 最低限のコードでシンプルなマッチングを動かしてみる
Unity でこんなコードを書いてみました。
これを動かすと先に張ったサンプルのようにシンプルな1vs1のマッチングを行うことができます。

```C#
using PlayFab;
using PlayFab.ClientModels;
using PlayFab.MultiplayerModels;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class SampleSceneController : MonoBehaviour
{
    // 処理中のメッセージは雑に全部これに表示します。
    [SerializeField] Text textBox;

    public void Start()
    {
        textBox.text = "ログイン中...\n";

        // PlayFabにいつも通りログインします。
        var request = new LoginWithCustomIDRequest { CustomId = "MyCustomId", CreateAccount = true };
        PlayFabClientAPI.LoginWithCustomID(request, OnLoginSuccess, OnFailure);

        void OnLoginSuccess(LoginResult result)
        {
            textBox.text += "ログインしました！\n\n";

            // ログインできたので続けてマッチングの処理を呼びます。
            Matchmaking();
        }
    }

    private void Matchmaking()
    {
        textBox.text += "マッチメイキングチケットをキューに積みます...\n";

        // プレイヤーの情報を作ります。
        var matchmakingPlayer = new MatchmakingPlayer
        {
            // Entityは下記のコードで決め打ちで大丈夫です。
            Entity = new PlayFab.MultiplayerModels.EntityKey
            {
                Id = PlayFabSettings.staticPlayer.EntityId,
                Type = PlayFabSettings.staticPlayer.EntityType
            }
        };

        var request = new CreateMatchmakingTicketRequest
        {
            // 先程作っておいたプレイヤー情報です。
            Creator = matchmakingPlayer,
            // マッチングできるまで待機する秒数を指定します。最大600秒です。
            GiveUpAfterSeconds = 30,
            // GameManagerで作ったキューの名前を指定します。
            QueueName = "1vs1Battle"
        };

        PlayFabMultiplayerAPI.CreateMatchmakingTicket(request, OnCreateMatchmakingTicketSuccess, OnFailure);

        void OnCreateMatchmakingTicketSuccess(CreateMatchmakingTicketResult result)
        {
            textBox.text += "マッチメイキングチケットをキューに積みました！\n\n";

            // キューに積んだチケットの状態をマッチングするかタイムアウトするまでポーリングします。
            var getMatchmakingTicketRequest = new GetMatchmakingTicketRequest
            {
                TicketId = result.TicketId,
                QueueName = request.QueueName
            };

            StartCoroutine(Polling(getMatchmakingTicketRequest));
        }
    }

    IEnumerator Polling(GetMatchmakingTicketRequest request)
    {
        // ポーリングは1分間に10回まで許可されているので、6秒間隔で実行するのがおすすめです。
        var seconds = 6f;
        var MatchedOrCanceled = false;

        while (true)
        {
            if (MatchedOrCanceled)
            {
                yield break;
            }

            PlayFabMultiplayerAPI.GetMatchmakingTicket(request, OnGetMatchmakingTicketSuccess, OnFailure);
            yield return new WaitForSeconds(seconds);
        }

        void OnGetMatchmakingTicketSuccess(GetMatchmakingTicketResult result)
        {
            switch (result.Status)
            {
                case "Matched":
                    MatchedOrCanceled = true;
                    textBox.text += $"対戦相手が見つかりました！\n\nMatchIDは {result.MatchId} です！";
                    return;

                case "Canceled":
                    MatchedOrCanceled = true;
                    textBox.text += "対戦相手が見つからないのでキャンセルしました...";
                    return;

                default:
                    textBox.text += "対戦相手が見つかるまで待機します...\n";
                    return;
            }
        }
    }

    void OnFailure(PlayFabError error)
    {
        Debug.Log($"{error.ErrorMessage}");
    }
}

```

## 3. プレイヤーのレートやレベル差を考慮したマッチングを動かしてみる

実際のゲームではレートやレベル差によって、初心者と上級者のマッチングを分けたりしますよね。

PlayFab ではどのように制御すればよいのか試してみました。

### 3.1. キューにルールを追加する
先程キューを作ったときは最低限の内容しか定義しませんでした。
今回は以下のようなルールを追加してみます。

このルールを追加することで、Rate の差が 101 以上のプレイヤー同士はマッチングしなくなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/88c54faf-faf8-b9b0-acde-50f452b6d767.png)

### 3.2. プレイヤーの Rate 情報をチケットに含める
先程のコードではチケット内の MatchmakingPlayer には Entity 情報しか含んでいませんでした。
今回は Rate の情報をもたせるために Attributes を使用します。

```C#
        // マッチングさせるプレイヤーの情報を作ります。
        var matchmakingPlayer = new MatchmakingPlayer
        {
            // Entityは下記のコードで決め打ちで大丈夫です。
            Entity = new PlayFab.MultiplayerModels.EntityKey
            {
                Id = PlayFabSettings.staticPlayer.EntityId,
                Type = PlayFabSettings.staticPlayer.EntityType
            },
            // これ以下を追記
            Attributes = new MatchmakingPlayerAttributes
            {
                // このプレイヤーは Rate 900～1100 のプレイヤーとしかマッチングしない
                DataObject = new { Rate = 1000 }
            }
        };

```
これだけでレートやレベル差を考慮したマッチングを実装することができました。
これは楽で良いですね。

## あとがき
今回はマッチメイキングの基本の部分を触ってみました。

まだ[公式ドキュメント](https://docs.microsoft.com/ja-jp/gaming/playfab/features/multiplayer/matchmaking/)を読み切れていないのですが、PlayFab のマッチメイキング機能はもっと複雑なマッチングルールやチームバトルにも対応しているということですので、引き続き触ってみようと思います。