【PUN2】ネットワーク管理にPhotonを使ってみる 〜導入からルーム・ロビー管理まで〜

Unity
マイケル
マイケル
どうもみなさんこんにちは!
マイケルです!
エレキベア
エレキベア
クマ〜〜〜
マイケル
マイケル
今日はこれまで作っていた3Dゲームに
ネットワーク機能をつけてマルチプレイできるようにしようとしていたのですが・・・

※過去記事はこちら!
【Unity】オンライン3Dアクションゲームの作り方をまとめる! (前編)

スクリーンショット 2020 04 08 0 36 18

Unityゲーム開発 オンライン3Dアクションゲームの作り方

マイケル
マイケル
参考にして進めていた書籍の情報が古すぎて全く使えませんでした・・・
エレキベア
エレキベア
2014年に書かれた本だから仕方ないクマ・・・
マイケル
マイケル
そんなわけで現在はどのように実装するのがいいのか調べてみたところ、
PhotonUnityNetworking(以下、PUN)というサービスを使うのがよさそうだということが分かりました!
今回はPUNの導入とルーム管理機能について調べてまとめます!
エレキベア
エレキベア
PUNって何なんクマ?
マイケル
マイケル
いい質問だね!
それじゃまずはPUNの概要についてみていこう!
スポンサーリンク

PUNとは

マイケル
マイケル
まずはPUNとは何か、なぜ選んだかについて!

〜PUNとは〜
Photon Unity Networking の略で、
マスターサーバやルーム管理などマルチプレイ実装に必要な機能を
提供してくれるUnityパッケージである。
・・・ Photon公式サイトはこちら

エレキベア
エレキベア
これを使うとマルチプレイ機能が簡単に実装できるというわけクマね
マイケル
マイケル
そういうことさ!
こういったサービスを使わなかったら、サーバの準備などハード面の問題NATパンチスルーなど通信問題等、考慮して組み込まないといけないんだけどその辺の面倒くさい機能を一律提供してくれるんだ!
マイケル
マイケル
ただ、20人までの接続は無料だけど、それ以上は有料プランになってしまうので注意です
エレキベア
エレキベア
すごいクマ〜〜〜!
PUNの他にも色々あるクマか??
マイケル
マイケル
まずUnityに標準で組み込まれているUNetというのがあったんだけど、これはUnity2019で廃止されたみたいだ。
Photonと同じようなサービスとしては他にMUN、GameLift、Strix Unity SDKというのもあるみたいだよ!
マイケル
マイケル
今回はその中でも導入例が多くて機能数も多いPUNを使用したというわけさ!
エレキベア
エレキベア
そんなにたくさんあるクマか

導入方法

マイケル
マイケル
導入方法は以下の手順になります!

1. Photon公式サイトでアカウント登録をし、アプリIDを取得する。
Photon公式サイト

スクリーンショット 2020 04 26 11 07 16

2. AssetStoreから「PUN2」をインポートする。

スクリーンショット 2020 04 26 11 07 57

3. Window > Photon Unity Networking > PUN Wizard > SetUp ProjectでアプリIDを設定する。
※再起動しないとメニューに出てこない場合があります。

スクリーンショット 2020 04 26 11 08 23

4. Assets/Photon/PhotonNetworking/Resources配下にあるPhotonServerSettingsの「Fixed Region」に「jp」と入力する。

スクリーンショット 2020 04 26 11 09 07

マイケル
マイケル
以上で導入は完了です!
エレキベア
エレキベア
かんたん4ステップクマ

実装方法について

マイケル
マイケル
それでは実装に移っていきます
マイケル
マイケル
まず、Photonのルーム管理までの流れは下記の図のようになります!

スクリーンショット 2020 04 26 11 59 32

エレキベア
エレキベア
Photonサーバの中でロビーやルームを管理してくれてるクマね
マイケル
マイケル
このイラストの矢印部分の

1. Photonサーバ接続・切断
2. ロビー入退室
3. ルーム作成・入退室

に沿って実装方法を見ていくよ!

Photonサーバ接続・切断


// コールバック機能を使うためMonoBehaviourPunCallbacksを継承
public class PhotonManager : MonoBehaviourPunCallbacks
{
・・・略・・・

    // Photonサーバ接続処理
    public void ConnectPhoton(bool boolOffline)
    {
        if (boolOffline)
        {
            // オフラインモードを設定
            PhotonNetwork.OfflineMode = true; // OnConnectedToMaster()が呼ばれる
            return;
        }
        // Photonサーバに接続する
        PhotonNetwork.ConnectUsingSettings();
    }

    // Photonサーバ切断処理
    public void DisConnectPhoton()
    {
        PhotonNetwork.Disconnect();
    }

    // コールバック:Photonサーバ接続完了
    public override void OnConnectedToMaster()
    {
         base.OnConnectedToMaster();
        }
    }

    // コールバック:Photonサーバ接続失敗
    public override void OnDisconnected(DisconnectCause cause)
    {
        base.OnDisconnected(cause);
    }
・・・略・・・
}
マイケル
マイケル
Photonサーバへの接続はConnectUsingSettingメソッド
切断時はPhotonNetwork.Disconnectメソッドを使用します。

使用するとコールバックで
OnConnectedToMasterメソッド(接続完了時)
OnDisconnectedメソッド(失敗時)

が呼び出されるので接続結果に対しての処理はこちらに記述しましょう!

マイケル
マイケル
コールバック関数を定義するため、継承クラスはMonoBehaviourPunCallbacksに変更するのも忘れずに行って下さい
エレキベア
エレキベア
さっき設定したPhotonServerSettingsを使って接続するクマね
マイケル
マイケル
飲み込みが早いですね
マイケル
マイケル
また、オフラインで接続したい場合には、PhotonNetwork.OfflineModeをtrueに設定します。
その場合、trueにした時点でコールバック処理が呼び出されるため注意しましょう!
エレキベア
エレキベア
数行かけば済むから簡単クマね〜〜

ロビー入退室

    // コールバック:Photonサーバ接続完了
    public override void OnConnectedToMaster()
    {
    	・・・略・・・
        // ロビーに接続
        PhotonNetwork.JoinLobby();
    }

    // コールバック:ロビー接続完了
    public override void OnJoinedLobby()
    {
        base.OnJoinedLobby();
    }

    // コールバック:ルーム一覧更新処理
    // (ロビーに入室した時、他のプレイヤーが更新した時のみ)
    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        base.OnRoomListUpdate(roomList);
    }
マイケル
マイケル
ロビーへの入室はJoinLobbyメソッドを使用します!

こちらも同じく入室完了後にコールバックとしてOnJoinedLobbyメソッドが呼び出されます。

エレキベア
エレキベア
Photonサーバ接続と同じような感じクマね
マイケル
マイケル
また、ロビー入室時に上記OnRoomListUpdateメソッドが呼び出されて
ロビー内のルーム一覧が取得できるのですが、これが中々曲者で・・・
マイケル
マイケル
コールバックされるタイミングは
・ロビー入室時
・他のプレイヤーがルームを更新した時
だけで、ルーム更新時には更新したルームリストしか引数で渡されてこないんです。

    // コールバック:ルーム一覧更新処理
    // (ロビーに入室した時、他のプレイヤーが更新した時のみ)
    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        base.OnRoomListUpdate(roomList);
        // ルーム一覧更新
        foreach (var info in roomList)
        {
            if (!info.RemovedFromList)
            {
                // 更新データが削除でない場合
                roomDispList.Add(info);
            }
            else
            {
                // 更新データが削除の場合
                roomDispList.Remove(info);
            }
        }
    }
マイケル
マイケル
そのため上記のように渡されてきたルームが削除かどうかを調べて表示するリストを更新しなければならないようです。
マイケル
マイケル
その他にも自動入室するAuto-Join Lobyや、ルーム一覧を取得するgetRoomListなんて機能もあったらしいですが、どうやらPUN2で廃止されたらしく、自分で実装するしかなさそうです・・・

リンク先(参考):https://doc.photonengine.com/en-us/pun/v2/getting-started/migration-notes

エレキベア
エレキベア
カナシマシマシクマ・・・・

ルーム作成・入退室

    // ルーム作成・入室処理
    public void CreateRoom(string roomName)
    {
        PhotonNetwork.CreateRoom(roomName);
    }

    // ルーム入室処理
    public void ConnectToRoom(string roomName)
    {
        PhotonNetwork.JoinRoom(roomName);
    }

    // コールバック:ルーム作成完了
    public override void OnCreatedRoom()
    {
        base.OnCreatedRoom();
    }

    // コールバック:ルーム作成失敗
    public override void OnCreateRoomFailed(short returnCode, string message)
    {
        base.OnCreateRoomFailed(returnCode, message);
    }

    // コールバック:ルームに入室した時
    public override void OnJoinedRoom()
    {
        base.OnJoinedRoom();
    }
マイケル
マイケル
ルームの作成はCreateRoomメソッド
ルームの入室はJoinRoomメソッドを使用します。

こちらも他のメソッド と同じくコールバックとして
OnCreatedRoomメソッド (ルーム作成完了時)
OnCreateRoomFailedメソッド (ルーム作成失敗時)
OnJoinedRoomメソッド (ルーム入室時)
がそれぞれよばれます!

マイケル
マイケル
ただ一点注意なのは、ルームの作成時には自動で入室処理が行われるということです。
まあルームにはホストが必要なので当たり前といえばそうですが・・・
エレキベア
エレキベア
これでルーム入室まで出来たクマね
マイケル
マイケル
とりあえずここまでの機能で接続は試せそうだね。
早速これらを使って接続を試していこう!

接続テストプログラムの作成

マイケル
マイケル
接続やメソッド呼び出しを確認するため、テスト用の画面を作りました!
作った画面は下記になります。
スクリーンショット 2020 04 26 13 08 29
マイケル
マイケル
モード選択でONLINEModeを選ぶとPhotonサーバと接続すると・・・
スクリーンショット 2020 04 26 13 12 52
マイケル
マイケル
ルーム作成とロビーにあるルーム一覧が表示される!
確認する機能だけに絞った画面になります
マイケル
マイケル
下記にGUI表示も含めたソースは全部下に貼っておくので、
少し長いですが必要に応じて参考にしてください!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;

public class PhotonManager : MonoBehaviourPunCallbacks
{
    string mode;                 // モード(ONLINE, OFFLINE)
    string dispStatus;           // 画面項目:状態
    string dispMessage;          // 画面項目:メッセージ
    string dispRoomName;         // 画面項目:ルーム名
    List<RoomInfo> roomDispList; // 画面項目:ルーム一覧

    // 状態
    public enum Status
    {
        ONLINE,   // オンライン
        OFFLINE,  // オフライン
    };

    private void Start()
    {
        initParam();
    }

    // 変数初期化処理
    private void initParam()
    {
        dispRoomName = "";
        dispMessage = "";
        dispStatus = Status.OFFLINE.ToString();
        roomDispList = new List<RoomInfo>();
    }

    // Photonサーバ接続処理
    public void ConnectPhoton(bool boolOffline)
    {
        if (boolOffline)
        {
            // オフラインモードを設定
            mode = Status.OFFLINE.ToString();
            PhotonNetwork.OfflineMode = true; // OnConnectedToMaster()が呼ばれる
            dispMessage = "OFFLINEモードで起動しました。";
            return;
        }
        // Photonサーバに接続する
        mode = Status.ONLINE.ToString();
        PhotonNetwork.OfflineMode = false;
        PhotonNetwork.ConnectUsingSettings();
    }

    // Photonサーバ切断処理
    public void DisConnectPhoton()
    {
        PhotonNetwork.Disconnect();
        // 変数初期化
        initParam();
    }

    // コールバック:Photonサーバ接続完了
    public override void OnConnectedToMaster()
    {
        base.OnConnectedToMaster();
        if (Status.ONLINE.ToString().Equals(mode))
        {
            dispStatus = Status.ONLINE.ToString();
            dispMessage = "サーバに接続しました。";
            // ロビーに接続
            PhotonNetwork.JoinLobby();
        }
    }

    // コールバック:Photonサーバ接続失敗
    public override void OnDisconnected(DisconnectCause cause)
    {
        base.OnDisconnected(cause);
        dispMessage = "サーバから切断しました。";
        dispStatus = Status.OFFLINE.ToString();
    }

    // コールバック:ロビー入室完了
    public override void OnJoinedLobby()
    {
        base.OnJoinedLobby();
    }

    // ルーム一覧更新処理
    // (ロビーに入室した時、他のプレイヤーが更新した時のみ)
    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        base.OnRoomListUpdate(roomList);
        // ルーム一覧更新
        foreach (var info in roomList)
        {
            if (!info.RemovedFromList)
            {
                // 更新データが削除でない場合
                roomDispList.Add(info);
            }
            else
            {
                // 更新データが削除の場合
                roomDispList.Remove(info);
            }
        }
    }

    // ルーム作成処理
    public void CreateRoom(string roomName)
    {
        PhotonNetwork.CreateRoom(roomName);
    }

    // ルーム入室処理
    public void ConnectToRoom(string roomName)
    {
        PhotonNetwork.JoinRoom(roomName);
    }

    // コールバック:ルーム作成完了
    public override void OnCreatedRoom()
    {
        base.OnCreatedRoom();
        dispMessage = "ルームを作成しました。";
    }

    // コールバック:ルーム作成失敗
    public override void OnCreateRoomFailed(short returnCode, string message)
    {
        base.OnCreateRoomFailed(returnCode, message);
        dispMessage = "ルーム作成に失敗しました。";
    }

    // コールバック:ルームに入室した時
    public override void OnJoinedRoom()
    {
        base.OnJoinedRoom();
        // 表示ルームリストに追加する
        roomDispList.Add(PhotonNetwork.CurrentRoom);
        dispMessage = "【" + PhotonNetwork.CurrentRoom.Name + "】" + "に入室しました。";
    }

    // ---------- 設定GUI ----------
    void OnGUI()
    {
        float scale = Screen.height / 480.0f;
        GUI.matrix = Matrix4x4.TRS(new Vector3(
            Screen.width * 0.5f, Screen.height * 0.5f, 0),
            Quaternion.identity,
            new Vector3(scale, scale, 1.0f));

        GUI.Window(0, new Rect(-200, -200, 400, 400),
            NetworkSettingWindow, "Photon接続テスト");
    }

    Vector2 scrollPosition;
    void NetworkSettingWindow(int windowID)
    {
        // ステータス, メッセージの表示
        GUILayout.BeginHorizontal();
        GUILayout.Label("状態: " + dispStatus, GUILayout.Width(100));
        GUILayout.FlexibleSpace();
        if (Status.ONLINE.ToString().Equals(dispStatus))
        {
            // サーバ接続時のみ表示
            if (GUILayout.Button("切断"))
                DisConnectPhoton();
        }
        GUILayout.EndHorizontal();
        GUILayout.Label(dispMessage);
        GUILayout.Space(20);

        if (!Status.ONLINE.ToString().Equals(dispStatus))
        {
            // --- 初期表示時、OFFLINEモードのみ表示
            // マスターサーバに接続する
            GUILayout.Label("【モード選択】");
            GUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            if (GUILayout.Button("ONLINE Mode"))
                ConnectPhoton(false);
            if (GUILayout.Button("OFFLINE Mode"))
                ConnectPhoton(true);
            GUILayout.EndHorizontal();
        }
        else if (Status.ONLINE.ToString().Equals(dispStatus))
        {
            // --- ONLINEモードのみ表示
            if (!(PhotonNetwork.CurrentRoom != null))
            {
                // ルーム作成
                GUILayout.Label("【ルーム作成】");
                GUILayout.BeginHorizontal();
                GUILayout.Label(" ルーム名: ");
                dispRoomName = GUILayout.TextField(dispRoomName, GUILayout.Width(150));
                GUILayout.FlexibleSpace();
                GUILayout.EndHorizontal();
                // 作成ボタン
                GUILayout.BeginHorizontal();
                if (GUILayout.Button("作成 & 入室"))
                {
                    CreateRoom(dispRoomName);
                }
                GUILayout.EndHorizontal();
                GUILayout.Space(20);

                // ルーム一覧
                GUILayout.Label("【ルーム一覧 (クリックで入室)】");
                // 一覧表示
                scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Width(380), GUILayout.Height(100));
                if (roomDispList != null && roomDispList.Count > 0)
                {
                    // 更新ボタン
                    GUILayout.BeginHorizontal();
                    GUILayout.FlexibleSpace();
                    if (GUILayout.Button("更新"))
                    {
                        // ロビーに入り直す
                        roomDispList = new List<RoomInfo>();
                        PhotonNetwork.LeaveLobby();
                        PhotonNetwork.JoinLobby();
                    }
                    // ルーム一覧
                    GUILayout.EndHorizontal();
                    foreach (RoomInfo roomInfo in roomDispList)
                        if (GUILayout.Button(roomInfo.Name, GUI.skin.box, GUILayout.Width(360)))
                            ConnectToRoom(roomInfo.Name);
                }
                GUILayout.EndScrollView();
            }
        }
    }
}

接続テスト結果

マイケル
マイケル
それじゃ作ったテストプログラムで接続を確認していくよ!
エレキベア
エレキベア
楽しみクマね〜〜
マイケル
マイケル
ネットワークテストで実行アプリが2つ以上必要だから、
ビルドして書き出したAPPとUnityシミュレータでの操作とビルドファイルでの操作が同期されていることを確認します!
スクリーンショット 2020 04 26 13 22 32
スクリーンショット 2020 04 26 13 22 42
マイケル
マイケル
まずUnityシミュレータ側でルームを作成して、、
スクリーンショット 2020 04 26 13 22 53

スクリーンショット 2020 04 26 13 23 06

マイケル
マイケル
ビルドしたAPPで接続するとUnityシミュレータ側で作ったルームが無事に一覧に表示されています!
どうやらうまく同期できたみたいです!
エレキベア
エレキベア
おめでとうクマ〜〜〜!!
マイケル
マイケル
ここまで確認出来ればあとはマルチプレイ機能をガンガン実装していくだけだ!
エレキベア
エレキベア
大きな一歩を踏み出せたクマね

まとめ

マイケル
マイケル
というわけで今回はPhotonを使用したルーム作成の確認でした!
マイケル
マイケル
ネット上も古い情報が多すぎて
これだけ調べるのでも丸一日かかってつかれたぜ・・・
エレキベア
エレキベア
おつかれさまクマ・・・
エレキベア
エレキベア
でも同期できた時は楽しかったクマね〜〜
マイケル
マイケル
そうだね!
これからゲームに組み込んでいくのが楽しみだ!
エレキベア
エレキベア
一緒にがんばっていくクマ〜〜〜!
マイケル
マイケル
心強いぜエレキベア!
それじゃ今回はこの辺で!アデュー!
エレキベア
エレキベア
クマ〜〜〜

【PUN2】ネットワーク管理にPhotonを使ってみる 〜導入からルーム・ロビー管理まで〜 〜完〜

コメント

  1. より:

    コメント失礼いたします。
    public override void OnCreatedRoom()
    {
    base.OnCreatedRoom();
    dispMessage = “ルームを作成しました。”;
    }

    とありますがこれにボタンの割り当てなどは必要ないのでしょうか?
    私も現在pun2でロビーを作成しているのですがルーム作成ボタンを押してもなにも起こらないのです。。。

    • マイケル より:

      コメントありがとうございます!

      「OnCreatedRoom()」メソッドは「PhotonNetwork.CreateRoom(roomName);」の処理でルームに入室後、
      コールバックとして呼び出されるので、ボタンへの割り当ては不要です。
      しかし、「OnCreatedRoom()」メソッドの直後に「OnJoinedRoom()」も呼ばれるため、
      メッセージが上書きされ画面上での確認はできないかと思います・・・。
      もし呼ばれているのを確認したければ、お手数ですがデバッグログを入れて動かしてみてください!

      そもそもロビー入室処理すら呼ばれないのであれば、考えられる原因としては
       ・MonoBehaviourPunCallbacks を継承していない
       ・メソッド名が誤っている
      あたりでしょうか・・・。

      僕もかなり記憶が曖昧ですが、お力になれれば幸いです!1