Photon マッチメイキングガイド

# 内容
この記事は、Photon [マッチメイキングガイド](https://doc.photonengine.com/en-us/pun/current/lobby-and-matchmaking/matchmaking-and-lobby)を参考にしています。

## イントロダクション
ルームに入って誰かとプレイする方法は3つあります。

* マッチするルームをサーバーに探索させる方法
* フレンドについていってルームに入る方法
* ルームリストを取得してユーザーに選ばせる方法

## マッチメイキングのチェックリスト
* 同じRegionに接続しているか
* 全ての同じクライアントでAppVersionを使用しているか
* プレイヤーが異なるUserIDを保有しているか
* 名前でルームに参加する場合、ルームが作成されているか
* ランダムなルームに参加する場合にはロビーを使用するよう選択する
* ルームプロパティをフィルタとして使用し、ランダムマッチメイキングを行う場合、ルーム作成時にこれらのプロパティーキーをロビーから見えるよう設定する
* SQLフィルタでランダムマッチメイキングを行う場合、使用する予約済みのフィルタリングプロパティキーをロビーから見えるよう設定する。
* 非同期マッチメイキングを実装する場合は、適切な設定でWebhookを使用する。(AsyncJoinを有効にする)。AsyncRandomLobbyを使用する。

## クイックマッチ
### JoinRandomOrCreateRoom
ルームが見つかった場合はそれに参加し、それ以外の場合は新しいルームが作成されます。

```c#
private void QuickMatch()
{
  PhotonNetwork.NetworkingClient.OpJoinRandomOrCreateRoom(null, null);
}
```

### JoinRandomRoom または CreateRoom

* プレイヤー数が不足している場合は、待機します。その場合対戦相手を待っているk十を示す画面を表示する。
* 新しいプレイヤーを締め出すには、ルームを閉じます。

```c#

using Photon.Pun;
using Photon.Realtime;

public class QuickMatchExample : MonoBehaviourPunCallbacks
{
    [SerializeField]
    private maxPlayers = 4;

    private void CreateRoom()
    {
        RoomOptions roomOptions = new RoomOptions();
        roomOptions.MaxPlayers = maxPlayers;
        PhotonNetwork.CreateRoom(null, roomOptions, null);
    }

    private void QuickMatch()
    {
        PhotonNetwork.JoinRandomRoom();
    }

    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        CreateRoom();
    }

    public override void OnJoinedRoom()
    {
        // joined a room successfully
    }
}
```

## ランダムマッチメイキング
プレイヤーが、クイックマッチではなく、特定のマップまたはモードをプレイしたい場合は、カスタムルームプロパティを設定し、それらをJoinRandomRoomリスエストのフィルターとして利用することができる。

### ルーム設定の一部をロビーに公開する
カスタムルームプロパティーはルーム内の全てのプレイヤーに共有されます。これらのあたいは文字列との`Hashtable`にて扱われます。

基本的には、設定はルーム内でのみ共有されMaster Serverには共有されません。しかし。一部をLobbyに共有することができます。ランダムマッチメイキングのフィルターとして利用することが可能です。

```c#
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using Hashtable = ExitGames.Client.Photon.Hashtable;

public class CreateRoomWithLobbyPropertiesExample : MonoBehaviourPunCallbacks
{
    public const string MAP_PROP_KEY = "map";
    public const string GAME_MODE_PROP_KEY = "gm";
    public const string AI_PROP_KEY = "ai";

    private void CreateRoom()
    {
        RoomOptions roomOptions = new RoomOptions();
        roomOptions.CustomRoomPropertiesForLobby = { MAP_PROP_KEY, GAME_MODE_PROP_KEY, AI_PROP_KEY };
        roomOptions.CustomRoomProperties = new Hashtable { { MAP_PROP_KEY, 1 }, { GAME_MODE_PROP_KEY, 0 } };
        PhotonNetwork.CreateRoom(null, roomOptions, null);
    }

    public override void OnCreateRoomFailed(short returnCode, string message)
    {
        Debug.LogErrorFormat("Room creation failed with error code {0} and error message {1}", returnCode, message);
    }

    public override void OnCreatedRoom()
    {}

    public override void OnJoinedRoom()
    {
        // joined a room successfully, CreateRoom leads here on success
    }
}
```

* `Room.SetCustomProperties`を利用することで、プロパティーの値を変更することができます。
* `Room.PropertiesListedInLobby`は、ルーム作成後ロビーに表示されるプロパティーキーを変更することができます。
これらの変更は、ロビーにJoinすることなくRoom内で行うことが可能です。

### フィルタリングを設定する方法
`JoinRandomRoom`をする際、希望する設定を指定することができます。

```c#
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using Hashtable = ExitGames.Client.Photon.Hashtable;

public class RandomMatchmakingExample : MonoBehaviourPunCallbacks
{
    public const string MAP_PROP_KEY = "map";

    private void JoinRandomRoom(byte mapCode, byte expectedMaxPlayers)
    {
        Hashtable expectedCustomRoomProperties = new Hashtable { { MAP_PROP_KEY, mapCode } };
        PhotonNetwork.JoinRandomRoom(expectedCustomRoomProperties, expectedMaxPlayers);
    }

    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        Debug.LogErrorFormat("Join Random Failed with error code {0} and error message {1}", returnCode, message);
        // here usually you create a new room
    }

    public override void OnJoinedRoom()
    {
        // joined a room successfully, JoinRandomRoom leads here on success
    }
}
```

## 友達と同じルームに入る方法
* ルーム名を`friendName1 + friendName2 + randomInteger`のようなユニークな名前に設定します。
* `RoomOptions`の`InVisible`を`false`に設定します。

```c#
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;

public class PrivateRoomExample : MonoBehaviourPunCallbacks
{
    public void JoinOrCreatePrivateRoom(string nameEveryFriendKnows)
    {
        RoomOptions roomOptions = new RoomOptions();
        roomOptions.IsVisible = false;
        PhotonNetwork.JoinOrCreateRoom(nameEveryFriendKnows, roomOptions, null);
    }

    public override void OnJoinRoomFailed(short returnCode, string message)
    {
        Debug.LogErrorFormat("Room creation failed with error code {0} and error message {1}", returnCode, message);
    }

    public override void OnJoinedRoom()
    {
        // joined a room successfully, JoinOrCreateRoom leads here on success
    }
}
```
※友達を`FindFriends`で検索することも可能です。

## ルームでUserIDを公開する
`RoomOptions`の`PublishUserId`を`true`に設定することにより、サーバーはUserIDを提供し、クライアント上でのアクセスが可能になります。

ユーザーIDの設定方法

* ユーザーが接続前にIDを設定します。
* 外部の認証サービスにより与えられるIDを設定します。
* PhotonがIDを持たないユーザーのユーザーIDを設定します。

## 特定のユーザーのために枠を事前に確保する方法
引数に`UserID`の配列である`expectedUsers`を設定します。

```c#
// create room example
PhotonNetwork.CreateRoom(roomName, roomOptions, typedLobby, expectedUsers);
// join room example
PhotonNetwork.JoinRoom(roomName, expectedUsers);
// join or create room example
PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, typedLobby, expectedUsers);
// join random room example
PhotonNetwork.JoinRandomRoom(expectedProperties, maxPlayers, expectedUsers, matchmakingType, typedLobby, sqlLobbyFilter, expectedUsers);
```

また、ルーム内で`expected users`を編集することも可能です。`Room.ExpectedUsers`

## Lobby
Photonでは、Lobbyで全てのRoomを管理します。つまり、全てのルームはロビーに属しています。ロビーは`名前`と`タイプ`の二つの要素を持っています。
名前は文字列であり、タイプには`Default`,`SQL`,`Async`の3つの種類があります。

全てのユーザーは既存の`Default Lobby`に参加します。また、クライアントはロビーを作成することも可能です。
ロビーは次の関数が実行された時、自動的に作成されます`JoinLobby`,`CreateRoom`,`JoinOrCreateRoom`

ルームと同様、ロビーに参加したり離れたりすることが可能です。ロビー内では他のユーザーとコミュニケーションを取ることができず、参加したロビーのルームリストのみ取得できます。

#### `CreateRoom` or `JoinOrCreate`でルームを作成する場合

* ロビーに参加して、ルームを作成する場合、明示的にLobbyを設定しない場合は、作成したルームは現在参加しているロビーに追加されます。
* ロビーに参加せず、ルームを作成する場合、明示的にLobbyを設定しない場合は、Default Lobbyにそのルームは追加されます。

* ロビーに参加し、明示的にLobbyを設定し、ルームを作成する場合、LobbyNameが設定されているかどうかによって処理が変わります。
  * Lobby Name が設定されていない場合は、ルームは現在参加しているLobbyに追加されます。
  * 一方で設定されている場合は、ルーム作成リクエスト通りのLobbyに追加されます。

#### `JoinRandomRoom`でルームに参加する場合

* ロビーに参加して、ルームに参加する場合、明示的にLobbyを設定しない場合は、参加しているロビーに属するルームを検索します。

* ロビーに参加せず、ルームに参加する場合、明示的にLobbyを設定しない場合は、Default Lobbyに属するルームを検索します。

* ロビーに参加し、明示的にLobbyを設定し、ルームに参加する場合、LobbyNameが設定されているかどうかによって処理が変わります。

  * Lobby Name が設定されていない場合は、現在参加しているロビーに属するルームを検索します
  * 一方で設定されている場合は、ルーム作成リクエスト通りのLobbyに属するルームを検索します

### Defalt Lobby Type
ランダムマッチングにもっとも適しているタイプであり、一番使用されています。
Default Lobbyに参加すると、ルームリストを取得し、定期的にアップデートします。

それらのルームリストは次の条件に従い、ソートされます。
1. オープンで、人数に空きがある
2. 人数に空きはないが、クローズされていない
3. クローズされている
※ルームの数には限界があります。

```c#
using Photon.Pun;
using System.Collections.Generic;

public class RoomListCachingExample : MonoBehaviourPunCallbacks
{
    private TypedLobby customLobby = new TypedLobby("customLobby", LobbyType.Default);

    private Dictionary<string, RoomInfo> cachedRoomList = new Dictionary<string, RoomInfo>();

    public void JoinLobby()
    {
        PhotonNetwork.JoinLobby(customLobby);
    }

    private void UpdateCachedRoomList(List<RoomInfo> roomList)
    {
        for(int i=0; i<roomList.Count; i++)
        {
            RoomInfo info = roomList[i];
            if (info.RemovedFromList)
            {
                cachedRoomList.Remove(info.Name);
            }
            else
            {
                cachedRoomList[info.Name] = info;
            }
        }
    }

    public override void OnJoinedLobby()
    {
        cachedRoomList.Clear();
    }

    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        UpdateCachedRoomList(roomList);
    }

    public override void OnLeftLobby()
    {
        cachedRoomList.Clear();
    }

    public override void OnDisconnected(DisconnectCause cause)
    {
        cachedRoomList.Clear();
    }
}
```

Default Lobbyは名前が`null`であり、唯一名前が指定されていないのが許されるLobby Typeになります。
その他のLobby Typeでは全て名前をもつ必要があり、名前が指定されていない場合はLobby Typeを指定した場合でもDefault Lobbyになります。

#### 推奨される流れ
基本的に、ロビーに参加する過程はスキップしてください。もし、特定のロビーにルームを追加したい場合は、新しくルームを作成するときにロビーを指定してください。

Defalut LobbyにJoinするとき大量のルームリストを取得し、必ずしも良いとは限りません。

もし、マッチメイキングを細かく制御したい場合には、ランダムマッチメイキングのためのフィルターを利用してください。
複数のロビーは有用です。

### SQLロビータイプ
より高度なマッチメイキングフィルタリングを利用可能できるロビーになります。

```c#
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using Hashtable = ExitGames.Client.Photon.Hashtable;

public class RandomMatchmakingExample : MonoBehaviourPunCallbacks
{
    public const string ELO_PROP_KEY = "C0";
    public const string MAP_PROP_KEY = "C1";
    private TypedLobby sqlLobby = new TypedLobby("customSqlLobby", LobbyType.SqlLobby);

    private void CreateRoom()
    {
        RoomOptions roomOptions = new RoomOptions();
        roomOptions.CustomRoomProperties = new Hashtable { { ELO_PROP_KEY, 400 }, { MAP_PROP_KEY, "Map3" } };

        roomOptions.CustomRoomPropertiesForLobby = { ELO_PROP_KEY, MAP_PROP_KEY }; // makes "C0" and "C3" available in the lobby
        PhotonNetwork.CreateRoom(null, roomOptions, sqlLobby);
    }

    private void JoinRandomRoom()
    {
        string sqlLobbyFilter = "C0 BETWEEN 345 AND 475 AND C3 = 'Map2'";
        //string sqlLobbyFilter = "C0 > 345 AND C0 < 475 AND (C3 = 'Map2' OR C3 = \"Map3\")";
        //string sqlLobbyFilter = "C0 >= 345 AND C0 <= 475 AND C3 IN ('Map1', 'Map2', 'Map3')";
        PhotonNetwork.JoinRandomRoom(null, 0, MatchmakingMode.FillRoom, sqlLobby, sqlLobbyFilter);
    }

    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        CreateRoom();
    }

    public override void OnCreateRoomFailed(short returnCode, string message)
    {
        Debug.LogErrorFormat("Room creation failed with error code {0} and error message {1}", returnCode, message);
    }

    public override void OnJoinedRoom()
    {
        // joined a room successfully, both JoinRandomRoom or CreateRoom lead here on success
    }
}
```

クライアントはSQLのようなクエリを利用し、カスタムリストをリクエストすることも可能です。

```c#
using Photon.Pun;
using System.Collections.Generic;

public class GetCustomRoomListExample : MonoBehaviourPunCallbacks
{
    private TypedLobby sqlLobby = new TypedLobby("customSqlLobby", LobbyType.SqlLobby);

    private void GetCustomRoomList(string sqlLobbyFilter)
    {
        PhotonNetwork.GetCustomRoomList(sqlLobby, sqlLobbyFilter);
    }

    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        // here you get the response, empty list if no rooms found
    }
}
```

### Asynchronous Random Lobby Type
Defalut Lobby Typeと似ていますが、次の点が異なっています。

 * ルーム自体はロビーリストに削除後一時間生存しています。
 * ルームリストはクライアントに送信されません。

### その他
同時にルームに参加すると、別々のルームに割り当てられることがありますが、これは仕様です。有効な回避策としては、ルームへの参加時にランダムな遅延を挿入することで回避することが可能です。