Photonでデータの同期を取る方法はいろいろあります。
今回はRPCを使って同期を取ってみましょう。
PhotonTargets
のことを調べていたら、予想以上に時間が掛かってしまいました。
そのため今回は、RPCの中でもPhotonTargets
ことについてのみ掘り下げています。
この記事にはUnity5.4.1f1及びPUN1.75を使用しています。
RPCを使ってみよう
前回はPhotonView
とOnPhotonSerializeView()
を使ってデータの同期を取りました。
他に同期を取る方法はないのでしょうか。ドキュメントを見てみましょう。
リモートプロシージャコール(RPC)というロールプレイングゲーム(RPG)によく似た名前の方法があるようです。
RPCの使い方をドキュメントで見てみましょう。
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
簡単にできました。これは便利ですね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 ); }
自分自身のゲームには影響がなく、他のプレイヤーのゲームに影響がありました。
PhotonTargets.MasterClient
次はPhotonTargets.MasterClient
です。
void OnMouseDown() { m_photonView.RPC( "ShowEffect", PhotonTargets.MasterClient ); }
ルームの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 ); }
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 ); }
PhotonTargets.All
は即時実行されるため同時間上で呼ばれ、PhotonTargets.AllViaServer
はPhotonサーバーを経由するため時間がずれているのがわかります。
され、これは何が問題なのでしょう。
図解してみます。
まず以下の条件下で各プレイヤーがそれぞれRPC()
を呼ぶとします。
- Player1がRPCを使ってMethodAを呼ぶ。
- Player2がRPCを使ってMethodBを呼ぶ。
- Player3がRPCを使ってMethodCを呼ぶ。
- 上記は極々わずかな時間差で呼ばれる。
これを以下のフローチャートにまとめてみました。
PhotonTargets.AllViaServer
の場合
PhotonTargets.All
の場合
ドキュメントの「ターゲット、バッファリング、順番の項目」に以下のような文面が記述されています。
PhotonTargetsにはViaServerで終わる値があります。
通常、送信クライアントがRPCを実行しなければならない場合、サーバを介してRPCを送信せずに即時に行います。
ローカルでメソッドを呼び出す場合は遅れがないので、これはイベントの順序に影響を与えます!
ViaServerは、「All」ショートカットを無効にします。
RPCを順番に行う必要がある場合に、これは特に興味深いです。
サーバーを経由して送信されたRPCはすべての受信クライアントによって、同じ順序で実行されます。これは、サーバー上の先着順です。
Photonサーバーを経由する分にはRPC()
関数を呼んだ(正確にはPhotonサーバーに届いた)順と同じ順序を呼ばれるようです。
PhotonTargets.All
の場合は、自分自身の関数の呼び出しは即時呼ばれてしまうため、他のプレイヤーと関数の呼ばれる順序が違ってきてしまいます。
場合によっては、厄介なバグが発生する恐れがあるため、注意が必要なのです。
ViaServerタイプのPhotonTargets
は以下の二つです。
- AllViaServer
- AllBufferedViaServer
PhotonTargets.AllBuffered
さて最後にPhotonTargets.AllBuffered
です。
PhotonTargets.All
とPhotonTargets.AllBuffered
は何が違うのでしょう。
All
RPCをその他全員に送信して、このクライアントで即座に実行します。後で入ってきたプレイヤーはこのRPCを実行しません。AllBuffered
RPCをその他全員に送信して、このクライアントで即座に実行します。
新規プレイヤーは、Roomに入室するとき、RPCがバッファリングされているので、(このクライアントが退室するまでは)このRPCを受信します。
PhotonTargets.AllBuffered
はバッファリングしておいてくれるんですって。
もちろん試してみます。
PhotonTargets.All
void OnMouseDown() { m_photonView.RPC( "ShowEffect", PhotonTargets.All ); }
PhotonTargets.AllBuffered
void OnMouseDown() { m_photonView.RPC( "ShowEffect", PhotonTargets.AllBuffered ); }
PhotonTargets.AllBuffered
ならば途中から入ったプレイヤーにも、今まで呼んだRPC()
関数が呼ばれました。
BufferedタイプのPhotonTargets
は以下の三つです。
- AllBuffered
- OthersBuffered
- AllBufferedViaServer
おお、RPC()
関数は本当にいろんなことが出来ますね。
さて次にRPC()
関数の引数について調べようかと思いましたが、結構時間がかかってしまったので、引数については次回に持ち越します。