うら干物書き

ゲームを作っています。

【Unity】僕もPhotonを使いたい #13 isMessageQueueRunning

 さて前回、乱入型のゲームを目指してゲームを作り始めました。
 そしたら早くも問題にぶつかりました。

 今回は私がシーン切り替えの際に問題になったPhotonNetwork.isMessageQueueRunningについて調べてみました。


この記事にはUnity5.4.1f1及びPUN1.76を使用しています。

シーンを切り替えてオブジェクト生成する

 ゲームの基礎部分を作り始めした。
 ロビー用のシーンを作成し、そのシーンでネットワークに接続しロビーに入りルームに入ります。
 ルームに入室した後は、ゲーム用のシーンに遷移しプレイヤーが操作するオブジェクトを生成する。

 この辺りは今までの僕もPhotonを使いたいシリーズでやってきました。

www.urablog.xyz

 PhotonNetwork.Instantiate()を使ってプレイヤー用のオブジェクトを作れば、後からルームに入室プレイヤーにもオブジェクトが同期されるはずです。

using UnityEngine;
using UnityEngine.SceneManagement;

public class DemoNetwork : MonoBehaviour
{
    [SerializeField]
    private string  m_resourcePath  = "";
    [SerializeField]
    private float   m_randomCircle  = 4.0f;

    private const string ROOM_NAME  = "RoomA";
    private const string SCENE_NAME = "DemoB";

    void Start()
    {
        DontDestroyOnLoad( gameObject );

        // シーンの読み込みコールバックを登録.
        SceneManager.sceneLoaded += OnLoadedScene;

        PhotonNetwork.ConnectUsingSettings( Application.version );
    }

    public void JoinRoom()
    {
        PhotonNetwork.JoinOrCreateRoom( ROOM_NAME, new RoomOptions(), TypedLobby.Default );
    }

    void OnJoinedRoom()
    {
        // ルームに入ると同時にシーンを遷移させる.
        SceneManager.LoadScene( SCENE_NAME );
    }

    private void OnLoadedScene( Scene i_scene, LoadSceneMode i_mode )
    {
        // シーンの遷移が完了したら自分用のオブジェクトを生成.
        if( i_scene.name == SCENE_NAME )
        {
            Vector3 pos = Random.insideUnitCircle * m_randomCircle;
            PhotonNetwork.Instantiate( m_resourcePath, pos, Quaternion.identity, 0 );
        }
    }
} // class DemoNetwork

f:id:urahimono:20161002095619g:plain

 おかしいですね。
 ルームに後から入室したプレイヤー2のシーンにはプレイヤー1のオブジェクトがありません。
 前回調べたときはちゃんと他のプレイヤーにもオブジェクトが生成されたはずなのですが……。
 前回と違うのはシーンを切り替えたことぐらいです。
 では上記のスクリプトを少し改良して、ルームに入室後少し時間をあけてからシーンを切り替えるようにしましょう。

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;

public class DemoNetwork : MonoBehaviour
{
    [SerializeField]
    private string  m_resourcePath  = "";
    [SerializeField]
    private float   m_randomCircle  = 4.0f;

    private const string ROOM_NAME  = "RoomA";
    private const string SCENE_NAME = "DemoB";

    void Start()
    {
        DontDestroyOnLoad( gameObject );

        // シーンの読み込みコールバックを登録.
        SceneManager.sceneLoaded += OnLoadedScene;

        PhotonNetwork.ConnectUsingSettings( Application.version );
    }

    public void JoinRoom()
    {
        PhotonNetwork.JoinOrCreateRoom( ROOM_NAME, new RoomOptions(), TypedLobby.Default );
    }

    void OnJoinedRoom()
    {
        // 少し時間をおいてからシーンを遷移させる.
        StartCoroutine( LoadScene( 2.0f ) );
    }

    private void OnLoadedScene( Scene i_scene, LoadSceneMode i_mode )
    {
        // シーンの遷移が完了したら自分用のオブジェクトを生成.
        if( i_scene.name == SCENE_NAME )
        {
            Vector3 pos = Random.insideUnitCircle * m_randomCircle;
            PhotonNetwork.Instantiate( m_resourcePath, pos, Quaternion.identity, 0 );
        }
    }

    private IEnumerator LoadScene( float i_time )
    {
        // 一定時間経ってからシーンを読む.
        yield return new WaitForSeconds( i_time );
        SceneManager.LoadScene( SCENE_NAME );
    }
} // class DemoNetwork

f:id:urahimono:20161002095703g:plain

 Player2にはルームに入室した瞬間にはPlayer1のオブジェクトがあります。
 そして、シーンを切り替えたらオブジェクトが消えてなくなってしまいます。
 なぜなら生成するオブジェクトは、DontDestroyOnLoad()を呼んで設定していないから。
 なるほど、オブジェクトはちゃんと同期されていたけどシーンが切り替わったことでオブジェクトが破棄されているだけだったんですね!

 理由はわかりましたが、ちょっと困りますね。
 そもそもルームに入室瞬間に同期を取り始めるのではなく、シーンを切り替え終わってから同期をとってほしいものです。
 何か方法はあるのでしょうか。

PhotonNetwork.isMessageQueueRunning

 PhotonNetworkにはこんなプロパティがあります。isMessageQueueRunningです。
 リファレンスを見てみましょう。

Photon Unity Networking: PhotonNetwork Class Reference

PhotonNetwork.isMessageQueueRunning

受信イベント(RPCやインスタンス作成やその他受信するすべて)の送出を一時停止するのに使われます。
IsMessageQueueRunning == falseのとき、OnPhotonSerializeViewは呼ばれず、クライアントには何も送られません。
着信メッセージも、メッセージキューを再有効化するまではキューに追加されません。
この設定が便利なのは、最初にレベルを読み込んで、次にPhotonViewとRPCのデータを受信し続けようとしたときです。
クライアントは、着信パケットとRPC/イベントの応答を、受信し送信し続けます。
これは「ラグ」を増加させ、一時停止が長引き着信メッセージはすべてキューに貯まるだけという問題を引き起こす可能性があります。

 なるほど、isMessageQueueRunningfalseにしておけばイベントを受け取らないみたいですよ。
 試してみましょう。

using UnityEngine;
using UnityEngine.SceneManagement;

public class DemoNetwork : MonoBehaviour
{
    [SerializeField]
    private string  m_resourcePath  = "";
    [SerializeField]
    private float   m_randomCircle  = 4.0f;

    private const string ROOM_NAME  = "RoomA";
    private const string SCENE_NAME = "DemoB";

    void Start()
    {
        DontDestroyOnLoad( gameObject );

        // シーンの読み込みコールバックを登録.
        SceneManager.sceneLoaded += OnLoadedScene;

        PhotonNetwork.ConnectUsingSettings( Application.version );
    }

    public void JoinRoom()
    {
        PhotonNetwork.JoinOrCreateRoom( ROOM_NAME, new RoomOptions(), TypedLobby.Default );
    }

    void OnJoinedRoom()
    {
        // ルームに入ると同時にシーンを遷移させる.
        PhotonNetwork.isMessageQueueRunning = false;
        SceneManager.LoadScene( SCENE_NAME );
    }

    private void OnLoadedScene( Scene i_scene, LoadSceneMode i_mode )
    {
        PhotonNetwork.isMessageQueueRunning = true;

        // シーンの遷移が完了したら自分用のオブジェクトを生成.
        if( i_scene.name == SCENE_NAME )
        {
            Vector3 pos = Random.insideUnitCircle * m_randomCircle;
            PhotonNetwork.Instantiate( m_resourcePath, pos, Quaternion.identity, 0 );
        }
    }
} // class DemoNetwork

f:id:urahimono:20161002095808g:plain

 ルームに入った瞬間にPhotonNetwork.isMessageQueueRunning = falseを呼んでイベントを受け取らないようにします。
 そしてシーン切り替えた後にPhotonNetwork.isMessageQueueRunning = trueを呼んでイベントを受け取るようにしています。
 そうすることで、オブジェクトの生成の同期がシーン切り替え後に行われるようになりました。

 これで自分の任意のタイミングで同期がとれるようになりました。

 このPhotonNetwork.isMessageQueueRunning、少し気になることがあります。
 PhotonNetwork.isMessageQueueRunningRPC()に対してはどのような挙動になるのでしょうか。
 調べてみましょう。

RPC()とisMessageQueueRunning

 では以下のスクリプトを使って調査してみます。
 RPC()を使ってオブジェクトの生成を同期します。
 オブジェクトの生成には、PhotonNetwork.Instantiate()ではなくGameObject.Instantiate()を使うので、オブジェクトそのものは同期しません。

using UnityEngine;

public class DemoNetwork : MonoBehaviour
{
    [SerializeField]
    private string  m_resourcePath  = "";
    [SerializeField]
    private float   m_randomCircle  = 4.0f;

    private PhotonView  m_photonView    = null;

    private const string ROOM_NAME  = "RoomA";

    void Start()
    {
        m_photonView    = GetComponent< PhotonView >();

        PhotonNetwork.ConnectUsingSettings( Application.version );
    }

    public void JoinRoom()
    {
        PhotonNetwork.JoinOrCreateRoom( ROOM_NAME, new RoomOptions(), TypedLobby.Default );
    }

    public void SwitchMessageQueueRunning()
    {
        PhotonNetwork.isMessageQueueRunning = !PhotonNetwork.isMessageQueueRunning;
    }

    public void SpawnObject()
    {
        Vector3 pos = Random.insideUnitCircle * m_randomCircle;
        m_photonView.RPC( "SpawnObjectRPC", PhotonTargets.All, pos );
    }
    public void SpawnObjectBuffered()
    {
        Vector3 pos = Random.insideUnitCircle * m_randomCircle;
        m_photonView.RPC( "SpawnObjectRPC", PhotonTargets.AllBuffered, pos );
    }
    [PunRPC]
    private void SpawnObjectRPC( Vector3 i_pos )
    {
        Vector3 pos = Random.insideUnitCircle * m_randomCircle;
        GameObject.Instantiate( Resources.Load( m_resourcePath ), i_pos, Quaternion.identity );
    }
} // class DemoNetwork

 まずは、RPC()PhotonTargetsを復習しましょう。
 PhotonTargets.AllPhotonTargets.AllBufferedについてです。

www.urablog.xyz

f:id:urahimono:20161002100016g:plain

 ルームに入室前に呼ばれたRPC()の情報はPhotonTargets.AllBufferedのようなBufferedタイプでなければ受け取りません。

 では、PhotonNetwork.isMessageQueueRunningのプロパティを切り替えてRPC()の情報を受け取ってみましょう。

f:id:urahimono:20161002100512g:plain

 isMessageQueueRunningfalseにした後に、PhotonTargets.AllPhotonTargets.AllBufferedRPC()を呼んで、その後isMessageQueueRunningtrueにしてイベントを受け取れるようにします。

 この場合は、PhotonTargets.AllPhotonTargets.AllBuffered両方でイベントを受け取れています。

 この違いはちゃんと覚えておいたほうがよさそうですね。

 さて、これで乱入型ゲームの基礎が出来ました。
 引き続きゲームを作っていきますー。

 次回 www.urablog.xyz

 前回 www.urablog.xyz