Chapter 10

🤝 基本的なマッチメイキング

o8que
o8que
2021.04.11に更新

このチャプターでは、PUN2の機能を使用した基本的なマッチメイキングの実装方法について、サンプルプロジェクト(🎮)を例にして紹介していきます。

クイックマッチ

とにかく短時間でプレイヤー同士をマッチさせたいなら、PhotonNetwork.JoinRandomRoom()を使用すると良いでしょう。既に存在しているルームの参加人数に空きがあればそのルームに参加して、参加できるルームが存在しないなら、PhotonNetwork.CreateRoom()を使用して新規でルームを作成するようにします。


SampleScene.cs
 using Photon.Pun;
 using Photon.Realtime;
 using UnityEngine;

 public class SampleScene : MonoBehaviourPunCallbacks
 {
     private void Start() {
         PhotonNetwork.NickName = "Player";
         PhotonNetwork.ConnectUsingSettings();
     }

     public override void OnConnectedToMaster() {
+        // ランダムなルームに参加する
+        PhotonNetwork.JoinRandomRoom();
     }

+    // ランダムで参加できるルームが存在しないなら、新規でルームを作成する
+    public override void OnJoinRandomFailed(short returnCode, string message) {
+        // ルームの参加人数を2人に設定する
+        var roomOptions = new RoomOptions();
+        roomOptions.MaxPlayers = 2;
+
+        PhotonNetwork.CreateRoom(null, roomOptions);
+    }

     public override void OnJoinedRoom() {
         var position = new Vector3(Random.Range(-3f, 3f), Random.Range(-3f, 3f));
         PhotonNetwork.Instantiate("Avatar", position, Quaternion.identity);

         if (PhotonNetwork.IsMasterClient) {
             PhotonNetwork.CurrentRoom.SetStartTime(PhotonNetwork.ServerTimestamp);
         }
     }
 }

途中参加を許可しない方法

ルームの参加人数に空きがある状態でゲームを開始したり、ゲームが開始した後にプレイヤーがルームから退出してルームの参加人数に空きができたりすると、マッチメイキング中のプレイヤーはそのルームに途中参加できることになります。プレイヤーの途中参加を許可したくない場合は、ゲームを開始する時などに、PhotonNetwork.CurrentRoom.IsOpenからルームへの参加を不許可に設定しておくと良いでしょう。

SampleScene.cs
 using Photon.Pun;
 using Photon.Realtime;
 using UnityEngine;

 public class SampleScene : MonoBehaviourPunCallbacks
 {
     private void Start() {
         PhotonNetwork.NickName = "Player";
         PhotonNetwork.ConnectUsingSettings();
     }

     public override void OnConnectedToMaster() {
         PhotonNetwork.JoinRandomRoom();
     }

     public override void OnJoinRandomFailed(short returnCode, string message) {
         var roomOptions = new RoomOptions();
         roomOptions.MaxPlayers = 2;

         PhotonNetwork.CreateRoom(null, roomOptions);
     }

     public override void OnJoinedRoom() {
         var position = new Vector3(Random.Range(-3f, 3f), Random.Range(-3f, 3f));
         PhotonNetwork.Instantiate("Avatar", position, Quaternion.identity);

         if (PhotonNetwork.IsMasterClient) {
             PhotonNetwork.CurrentRoom.SetStartTime(PhotonNetwork.ServerTimestamp);
         }

+        // ルームが満員になったら、以降そのルームへの参加を不許可にする
+        if (PhotonNetwork.CurrentRoom.PlayerCount == PhotonNetwork.CurrentRoom.MaxPlayers) {
+            PhotonNetwork.CurrentRoom.IsOpen = false;
+         }
     }
 }

スキルベースマッチ

PhotonNetwork.JoinRandomRoom()は、ルームのカスタムプロパティの値で絞り込み条件を指定することができます。例えば、PhotonNetwork.CreateRoom()でルームのカスタムプロパティの初期値にルームを作成するプレイヤーのランクを設定して、PhotonNetwork.JoinRandomRoom()の第一引数で参加するプレイヤーのランクを指定することで、ルームを作成したプレイヤーと同じランクのプレイヤーだけがルームへ参加できるスキルベースマッチを実現できます。


SampleScene.cs
 using ExitGames.Client.Photon;
 using Photon.Pun;
 using Photon.Realtime;
 using UnityEngine;

 public class SampleScene : MonoBehaviourPunCallbacks
 {
     private void Start() {
         PhotonNetwork.NickName = "Player";

+        // プレイヤーのカスタムプロパティに、ランダムなランクを設定する
+        PhotonNetwork.LocalPlayer.SetRandomRank();

         PhotonNetwork.ConnectUsingSettings();
     }

     public override void OnConnectedToMaster() {
+        var expectedProps = new Hashtable();
+        expectedProps.SetPlayerRank(PhotonNetwork.LocalPlayer);
+
+        // 自身と同じランクのプレイヤーが作成したルームへランダムに参加する
+        PhotonNetwork.JoinRandomRoom(expectedProps, 2);
     }

+    // ランダムで参加できるルームが存在しないなら、新規でルームを作成する
+    public override void OnJoinRandomFailed(short returnCode, string message) {
+        // ルームのカスタムプロパティの初期値に、自身と同じランクを設定する
+        var initialProps = new Hashtable();
+        initialProps.SetPlayerRank(PhotonNetwork.LocalPlayer);
+
+        // ルーム設定を行う
+        var roomOptions = new RoomOptions();
+        roomOptions.MaxPlayers = 2;
+        roomOptions.CustomRoomProperties = initialProps;
+        roomOptions.CustomRoomPropertiesForLobby = initialProps.KeysForLobby();
+
+        PhotonNetwork.CreateRoom(null, roomOptions);
+    }

     public override void OnJoinedRoom() {
         var position = new Vector3(Random.Range(-3f, 3f), Random.Range(-3f, 3f));
         PhotonNetwork.Instantiate("Avatar", position, Quaternion.identity);

         // ルームを作成したプレイヤーは、現在のサーバー時刻をゲームの開始時刻に設定する
         if (PhotonNetwork.IsMasterClient) {
             PhotonNetwork.CurrentRoom.SetStartTime(PhotonNetwork.ServerTimestamp);
         }
     }
 }

上記のスクリプト(SampleScene)では、プレイヤーとルームのカスタムプロパティにランクを設定・取得する拡張メソッドを定義して使用しています。

PlayerPropertiesExtensions.cs
using ExitGames.Client.Photon;
using Photon.Realtime;
using UnityEngine;

public static class PlayerPropertiesExtensions
{
    private const string RankKey = "Rank";

    private static readonly Hashtable propsToSet = new Hashtable();
    // プレイヤーのランクの配列
    private static readonly string[] ranks = { "A", "B", "C" };

    // プレイヤーのランクを取得する
    public static string GetRank(this Player player) {
        if (player.CustomProperties[RankKey] is string rank) {
            return rank;
        } else {
            return ranks[ranks.Length - 1];
        }
    }

    // プレイヤーのランクをランダムに設定する
    public static void SetRandomRank(this Player player) {
        propsToSet[RankKey] = ranks[Random.Range(0, ranks.Length)];
        player.SetCustomProperties(propsToSet);
        propsToSet.Clear();
    }
}
RoomPropertiesExtensions.cs
using ExitGames.Client.Photon;
using Photon.Realtime;

public static class RoomPropertiesExtensions
{
    private const string RankKey = "Rank";

    // Hashtableにプレイヤーのランクを設定する
    public static void SetPlayerRank(this Hashtable hashtable, Player player) {
        hashtable[RankKey] = player.GetRank();
    }

    // ロビーから取得できるカスタムプロパティのキーの配列を返す
    public static string[] KeysForLobby(this Hashtable hashtable) {
        return new[] { RankKey };
    }
}

アバターの頭上にはプレイヤー名とプレイヤーIDに加えてプレイヤーのランクを表示させて、同じランクのプレイヤー同士が正しくマッチしているか確認しやすくしておきます。


AvatarNameDisplay.cs
 using Photon.Pun;
 using TMPro;

 public class AvatarNameDisplay : MonoBehaviourPunCallbacks
 {
     private void Start() {
         var nameLabel = GetComponent<TextMeshPro>();

+        // プレイヤー名とプレイヤーIDとプレイヤーのランクを表示する
+        var nickName = photonView.Owner.NickName;
+        var id = photonView.OwnerActorNr;
+        var rank = photonView.Owner.GetRank();
+        nameLabel.text = $"{nickName}({id})[{rank}]";
     }
 }

プライベートマッチ

ルームを非公開に設定すると、PhotonNetwork.JoinRandomRoom()を使ったランダムなルームへの参加対象から除外されて、ロビーからルーム情報を取得することもできなくなります。非公開のルームへ参加するにはPhotonNetwork.JoinRoom()などで正確なルーム名を指定する必要があるため、ルーム名をパスワードにすれば、パスワードを知っているプレイヤーのみが参加できるプライベートマッチが実現できます。

ここでは、以下のようなパスワードを入力するUIをサンプルプロジェクトに追加してみましょう。パスワードはアルファベットと数字の6桁として、6桁を入力した時のみ参加ボタンを押せるようにします。


MatchmakingView.cs
using Photon.Pun;
using Photon.Realtime;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class MatchmakingView : MonoBehaviourPunCallbacks
{
    [SerializeField]
    private TMP_InputField passwordInputField = default;
    [SerializeField]
    private Button joinRoomButton = default;

    private CanvasGroup canvasGroup;

    private void Start() {
        canvasGroup = GetComponent<CanvasGroup>();
        // マスターサーバーに接続するまでは、入力できないようにする
        canvasGroup.interactable = false;

        // パスワードを入力する前は、ルーム参加ボタンを押せないようにする
        joinRoomButton.interactable = false;

        passwordInputField.onValueChanged.AddListener(OnPasswordInputFieldValueChanged);
        joinRoomButton.onClick.AddListener(OnJoinRoomButtonClick);
    }

    public override void OnConnectedToMaster() {
        // マスターサーバーに接続したら、入力できるようにする
        canvasGroup.interactable = true;
    }

    private void OnPasswordInputFieldValueChanged(string value) {
        // パスワードを6桁入力した時のみ、ルーム参加ボタンを押せるようにする
        joinRoomButton.interactable = (value.Length == 6);
    }

    private void OnJoinRoomButtonClick() {
        // ルーム参加処理中は、入力できないようにする
        canvasGroup.interactable = false;

        // ルームを非公開に設定する(新規でルームを作成する場合)
        var roomOptions = new RoomOptions();
        roomOptions.MaxPlayers = 2;
        roomOptions.IsVisible = false;

        // パスワードと同じ名前のルームに参加する(ルームが存在しなければ作成してから参加する)
        PhotonNetwork.JoinOrCreateRoom(passwordInputField.text, roomOptions, TypedLobby.Default);
    }

    public override void OnJoinedRoom() {
        // ルームへの参加が成功したら、UIを非表示にする
        gameObject.SetActive(false);
    }

    public override void OnJoinRoomFailed(short returnCode, string message) {
        // ルームへの参加が失敗したら、パスワードを再び入力できるようにする
        passwordInputField.text = string.Empty;
        canvasGroup.interactable = true;
    }
}

入力フィールド(InputField)はインスペクターから、制限文字数を6、テキストコンテンツのタイプをAlphanumericにします。

そして最後に、SampleSceneのスクリプトから不要なコードを削除しておきましょう。


SampleScene.cs
 using Photon.Pun;
-using Photon.Realtime;
 using UnityEngine;

 public class SampleScene : MonoBehaviourPunCallbacks
 {
     private void Start() {
         PhotonNetwork.NickName = "Player";
         PhotonNetwork.ConnectUsingSettings();
     }

-    public override void OnConnectedToMaster() {
-        PhotonNetwork.JoinOrCreateRoom("Room", new RoomOptions(), TypedLobby.Default);
-    }

     public override void OnJoinedRoom() {
         var position = new Vector3(Random.Range(-3f, 3f), Random.Range(-3f, 3f));
         PhotonNetwork.Instantiate("Avatar", position, Quaternion.identity);

         if (PhotonNetwork.IsMasterClient) {
             PhotonNetwork.CurrentRoom.SetStartTime(PhotonNetwork.ServerTimestamp);
         }
     }
 }