徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】僕もPhotonを使いたい #08 RPC() PhotonTargets編

 Photonでデータの同期を取る方法はいろいろあります。
 今回はRPCを使って同期を取ってみましょう。

 PhotonTargetsのことを調べていたら、予想以上に時間が掛かってしまいました。
 そのため今回は、RPCの中でもPhotonTargetsことについてのみ掘り下げています。


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

RPCを使ってみよう

 前回はPhotonViewOnPhotonSerializeView()を使ってデータの同期を取りました。
 他に同期を取る方法はないのでしょうか。ドキュメントを見てみましょう。

doc.photonengine.com

 リモートプロシージャコール(RPC)というロールプレイングゲーム(RPG)によく似た名前の方法があるようです。
 RPCの使い方をドキュメントで見てみましょう。

doc.photonengine.com

 PhotonViewがアタッチされていて、関数に[PunRPC]というアトリビュートをつけてRPC()関数を呼べばいいみたいですね。
 おや、これはOnPhotonSerializeView()より簡単そうですよ。
 早速使ってみましょう。

 以下のスクリプトではオブジェクトをクリックするとエフェクトが出る処理をRPCを使って同期させています。

using UnityEngine;

public class DemoObject : MonoBehaviour
{
    [SerializeField]
    private string  m_effectPath    = "";

    private PhotonView  m_photonView    = null;

    private readonly Color[]    MATERIAL_COLORS = new Color[]
    {
        Color.white, Color.red, Color.green, Color.blue, Color.green,
    };

    void Awake()
    {
        m_photonView    = GetComponent<PhotonView>();
    }

    void Start()
    {
        int ownerID = m_photonView.ownerId;

        var render  = GetComponent<Renderer>();
        render.material.color   = MATERIAL_COLORS[ ownerID ];
    }

    void OnMouseDown()
    {
        m_photonView.RPC( "ShowEffect", PhotonTargets.All );
    }

    [PunRPC]
    private void ShowEffect()
    {
        // エフェクトを生成.
        // 適当な時間が経過したら消すように設定.
        GameObject effect = GameObject.Instantiate( Resources.Load( m_effectPath ), transform.position, Quaternion.identity ) as GameObject;
        GameObject.Destroy( effect, 3.0f );
    }

} // class DemoObject

f:id:urahimono:20160920210638g:plain

 簡単にできました。これは便利ですねRPC!

 さて使ってみて、気になったのはPhotonView.RPC()の第二引数として渡したPhotonTargetsについてです。
 スクリプトリファレンスを見る限り、いろいろな種類があるみたいです。

https://photonengine.jp/pun-api/group__public_api.html#gab84b274b6aa3b3a3d7810361da16170f

 いろいろ試してみましょう。

 先ほど試したのはPhotonTargets.Allです。Player1,Player2両方のゲームに影響がありました。

PhotonTargets.Others

 次に試すのはPhotonTargets.Othersです。

void OnMouseDown()
{
    m_photonView.RPC( "ShowEffect", PhotonTargets.Others );
}

f:id:urahimono:20160920210639g:plain

 自分自身のゲームには影響がなく、他のプレイヤーのゲームに影響がありました。

PhotonTargets.MasterClient

 次はPhotonTargets.MasterClientです。

void OnMouseDown()
{
    m_photonView.RPC( "ShowEffect", PhotonTargets.MasterClient );
}

f:id:urahimono:20160920210640g:plain

 ルームのMasterClientのゲームのみ影響がありました。

PhotonTargets.AllViaServer

 さて、ここからは少し複雑になりそうです。
 PhotonTargets.AllとPhotonTargets.AllViaServerは何が違うのでしょうか。

 再びスクリプトリファレンスを見てみましょう。
https://photonengine.jp/pun-api/group__public_api.html#gab84b274b6aa3b3a3d7810361da16170f

  • All
    RPCをその他全員に送信して、このクライアントで即座に実行します。後で入ってきたプレイヤーはこのRPCを実行しません。

  • AllViaServer
    RPCをサーバーを介して(このクライアントも含めた)全員に送信します。
    このクライアントは、他のクライアントと同じように、サーバーからRPCを受信したとき、RPCを実行します。
    利点: サーバーのRPC送信指示は、どのクライアントに対しても同じでよくなります。

 RPC()を呼んだプレイヤーの処理が、サーバーを経由するか、即時実行されるかの違いのようですね。
 試してましょう。

PhotonTargets.All

void OnMouseDown()
{
    Debug.LogFormat( "{0} OnMouseDown", Time.time );
    m_photonView.RPC( "ShowEffect", PhotonTargets.All );
}

[PunRPC]
private void ShowEffect()
{
    Debug.LogFormat( "{0} ShowEffect", Time.time );

    // エフェクトを生成.
    // 適当な時間が経過したら消すように設定.
    GameObject effect = GameObject.Instantiate( Resources.Load( m_effectPath ), transform.position, Quaternion.identity ) as GameObject;
    GameObject.Destroy( effect, 3.0f );
}

f:id:urahimono:20160920210641g:plain

PhotonTargets.AllViaServer

void OnMouseDown()
{
    Debug.LogFormat( "{0} OnMouseDown", Time.time );
    m_photonView.RPC( "ShowEffect", PhotonTargets.AllViaServer );
}

[PunRPC]
private void ShowEffect()
{
    Debug.LogFormat( "{0} ShowEffect", Time.time );

    // エフェクトを生成.
    // 適当な時間が経過したら消すように設定.
    GameObject effect = GameObject.Instantiate( Resources.Load( m_effectPath ), transform.position, Quaternion.identity ) as GameObject;
    GameObject.Destroy( effect, 3.0f );
}

f:id:urahimono:20160920210642g:plain

 PhotonTargets.Allは即時実行されるため同時間上で呼ばれ、PhotonTargets.AllViaServerはPhotonサーバーを経由するため時間がずれているのがわかります。

 され、これは何が問題なのでしょう。
 図解してみます。

 まず以下の条件下で各プレイヤーがそれぞれRPC()を呼ぶとします。

  1. Player1がRPCを使ってMethodAを呼ぶ。
  2. Player2がRPCを使ってMethodBを呼ぶ。
  3. Player3がRPCを使ってMethodCを呼ぶ。
  4. 上記は極々わずかな時間差で呼ばれる。

 これを以下のフローチャートにまとめてみました。

PhotonTargets.AllViaServerの場合
f:id:urahimono:20160920212005p:plain

PhotonTargets.Allの場合
f:id:urahimono:20160920212015p:plain

 ドキュメントの「ターゲット、バッファリング、順番の項目」に以下のような文面が記述されています。

doc.photonengine.com

PhotonTargetsにはViaServerで終わる値があります。
通常、送信クライアントがRPCを実行しなければならない場合、サーバを介してRPCを送信せずに即時に行います。
ローカルでメソッドを呼び出す場合は遅れがないので、これはイベントの順序に影響を与えます!

ViaServerは、「All」ショートカットを無効にします。  
RPCを順番に行う必要がある場合に、これは特に興味深いです。  
サーバーを経由して送信されたRPCはすべての受信クライアントによって、同じ順序で実行されます。これは、サーバー上の先着順です。  

 Photonサーバーを経由する分にはRPC()関数を呼んだ(正確にはPhotonサーバーに届いた)順と同じ順序を呼ばれるようです。
 PhotonTargets.Allの場合は、自分自身の関数の呼び出しは即時呼ばれてしまうため、他のプレイヤーと関数の呼ばれる順序が違ってきてしまいます。
 場合によっては、厄介なバグが発生する恐れがあるため、注意が必要なのです。

 ViaServerタイプのPhotonTargetsは以下の二つです。

  • AllViaServer
  • AllBufferedViaServer

PhotonTargets.AllBuffered

 さて最後にPhotonTargets.AllBufferedです。
 PhotonTargets.AllPhotonTargets.AllBufferedは何が違うのでしょう。

  • All
    RPCをその他全員に送信して、このクライアントで即座に実行します。後で入ってきたプレイヤーはこのRPCを実行しません。

  • AllBuffered
    RPCをその他全員に送信して、このクライアントで即座に実行します。
    新規プレイヤーは、Roomに入室するとき、RPCがバッファリングされているので、(このクライアントが退室するまでは)このRPCを受信します。

 PhotonTargets.AllBufferedはバッファリングしておいてくれるんですって。
 もちろん試してみます。

PhotonTargets.All

void OnMouseDown()
{
    m_photonView.RPC( "ShowEffect", PhotonTargets.All );
}

f:id:urahimono:20160920211637g:plain

PhotonTargets.AllBuffered

void OnMouseDown()
{
    m_photonView.RPC( "ShowEffect", PhotonTargets.AllBuffered );
}

f:id:urahimono:20160920211638g:plain

 PhotonTargets.AllBufferedならば途中から入ったプレイヤーにも、今まで呼んだRPC()関数が呼ばれました。
 BufferedタイプのPhotonTargetsは以下の三つです。

  • AllBuffered
  • OthersBuffered
  • AllBufferedViaServer

 おお、RPC()関数は本当にいろんなことが出来ますね。

 さて次にRPC()関数の引数について調べようかと思いましたが、結構時間がかかってしまったので、引数については次回に持ち越します。

 次回 www.urablog.xyz

 前回 www.urablog.xyz