chronote

ゲーム創作活動の備忘録、日々の雑記など。

制作進捗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
このソースに追記していくことになります。

通信内容の特性上、やりとりするカスタムオブジェクトが多くなる場合は、
オブジェクトの構造を JSONYAML などに変換して登録する、というのも手です。
これなら、速度は犠牲になっても、フレームワークの汎用性は損なわれません。

最近だと 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 の解説日記になってしまった感。
ゴールデンウィーク中に対戦実装まで完了したかったけど
検証だけで過ぎ去ってしまいました・・・
しばらく平日は体力的に作業ができず、進みが遅いです。あと、花粉。
引き続き、同期通信によるターン制御を進めます。