制作進捗11 ~通信同期検証~
前回は、ルームの作成とマッチングを実装しましたので
今回は、PUN による通信同期とデータ送受信について、検証しました。
最初に言っておくと、全然おもしろい話ではありません。
PhotonView リアルタイム通信
エディタでの操作
リアルタイムに同期させたい GameObject には、PhotonView コンポーネントをアタッチします。
ObservedComponents に同期させたい要素を追加。
種類は Animator / Rigidbody / Rigidbody2D / Transform の4種類。
今回はこちらの方法は使わないので割愛。
スクリプトでの操作
Photon.MonoBehaviour 継承クラスなら photonView に直接アクセスできますが、
標準の MonoBehaviour や GameObject であれば、以下のように参照できます。
PhotonView photonView = PhotonView.Get(this);
※PhotonView コンポーネントは、最低1つアタッチしなければならない
スクリプトから任意の値を指定することで、柔軟な同期が可能です。
// 頻繁に任意の値を同期したい場合、ここで指定する // ストリームに書き込まなければ送信はされない void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { if (stream.isWriting) { //We own this player: send the others our data stream.SendNext((int)controllerScript._characterState); stream.SendNext(transform.position); stream.SendNext(transform.rotation); } else { //Network player, receive data controllerScript._characterState = (CharacterState)(int)stream.ReceiveNext(); correctPlayerPos = (Vector3)stream.ReceiveNext(); correctPlayerRot = (Quaternion)stream.ReceiveNext(); } }
Photonでサポートされている型
どんな型の値を送受信できるのか、という情報はこちらのページにまとまっています。
Photonでシリアル化 | Exit Games
自分で定義した型をやりとりしたい場合、
Photon に対してメソッドを登録してあげる必要があります。
上記のページでは、Vector2 のオブジェクト(8バイト)を
シリアライズ、デシリアライズするメソッドを定義し、
PhotonPeer.RegisterType() で登録する方法が紹介されています。
PhotonPeer はフレームワークの中身をいじることになるため、情報は一般公開されていません。
\Photon Unity Networking\Plugins\PhotonNetwork\CustomTypes.cs
このソースに追記していくことになります。
通信内容の特性上、やりとりするカスタムオブジェクトが多くなる場合は、
オブジェクトの構造を JSON や YAML などに変換して登録する、というのも手です。
これなら、速度は犠牲になっても、フレームワークの汎用性は損なわれません。
最近だと JSON はやめて、MessagePack を使うことを推奨されているようですが、
De/Serialize にかかる負荷が許容できるようなら、これらの選択肢も無くはない?
ただし、リアルタイム通信用途の話ではないと思われます。
構造体をbyte配列にして一括送信
変数を個別に同期するのは手間、かつ応答回数も増えるので、
構造体単位でまとめて送信できないか、検証してみました。
参考文献
BinaryReader・BinaryWriterでの構造体の読み書き (構造体⇔バイト配列の変換) - Programming/.NET Framework/Tips - 総武ソフトウェア推進所
上記の方法では、BitConverter が対応する基本型しか変換できないため、
例えば構造体フィールドに対して配列や構造体の定義には未対応でした。
後から知ったんですが、 BinaryFormatter を使えば、
どんなフィールドでもまるっとシリアライズできるそうな。
【Unity】みなさん、データの保存ってどうやってます?俺はこうやっちゃってます。 - ハルシオンシステムの気ままBlog
以前はこれを使うと iOS では動かない、と言われていましたが、
なんだか回避方法が見つかっている?また時間作って検証してみます。
リモートプロシージャコール(RPC)
リアルタイム通信とは違って、
任意のタイミングで通信を発生させたい場合(ターン制、ステート同期など)や、
ルーム内のプレイヤーにブロードキャストしたい場合なんかには、この RPC を使います。
受信コールバックには [RPC] Attribute の定義が必要となります。
// RPC送信側 photonView.RPC("OnSyncMyDataRPC", PhotonTargets.All, (byte[])data); // RPC受信側 [RPC] public void OnSyncMyDataRPC(byte[] data, PhotonMessageInfo info) { // dataを受け取る処理を記述 Debug.Log(String.Format("Info: from {0} {1} {2}", info.sender, info.PhotonView, info.timestamp)); }
注意点として、RPC は各クライアントの一致する PhotonView に対して行われるため、
シーンロード中に呼び出す時など、状況が揃わない場合は実行されない。
対策としては、シーンロード前にメッセージキューを停止させる。
// これ以降のネットワークメッセージの処理を一時的に停止する PhotonNetwork.isMessageQueueRunning = false; Application.LoadLevel(levelName);
ロードが終わったらメッセージキュー停止を戻しておく。
RPC を使えば、簡易テキストチャットが作れます。
[RPC] void ChatMessage(string name, string msg) { Debug.Log(name + ": " + msg); } photonView.RPC("ChatMessage", PhotonTargets.All, name, msg);
ハマりポイント
素直に考えれば間違えないけど、頭が固い自分が個人的にハマった点をメモ。
PhotonView の同期は、ルーム内共通の同一オブジェクト(ViewID)に対してやりとりされる。
つまり、それぞれの世界ではお互いのプレイヤーが唯一無二の存在となり、
完全に別オブジェクトに分かれないといけない。
(1つのゲーム機で1画面で格闘ゲームをプレイしている、と考えると腑に落ちる)
例えば2人対戦の時、マスターがプレイヤー1、クライアントがプレイヤー2であれば、
クライアントのゲームから見ても自身はプレイヤー2である必要がある。
これを勘違いして、自身はプレイヤー(1P)、相手はライバル(2P)、という考え方にすると、
お互いがプレイヤー(1P)のデータを同期しあって衝突を起こすことになる。
(2人が別々のゲーム機でオンライン対戦ゲームをプレイしていて、
自分は必ず1P側になる、と考えるとみごとにバグる)
そのため、ゲーム内に複数存在するプレイヤーのうち、
操作プレイヤーをどのオブジェクトに振り分けるのか、といったルールが必要になる。
あとがき
ただの PUN の解説日記になってしまった感。
ゴールデンウィーク中に対戦実装まで完了したかったけど
検証だけで過ぎ去ってしまいました・・・
しばらく平日は体力的に作業ができず、進みが遅いです。あと、花粉。
引き続き、同期通信によるターン制御を進めます。