Photon Unity Networking 2 (PUN2) のRPCとRaiseEventを使ってテクスチャデータを送信する
はじめに
- Photon Unity Networking 2 (PUN 2)を使って、ランタイムで生成されたテクスチャ画像を他のユーザーに送信したい
- オンラインストレージを経由する方式ではなく、インメモリのデータをRaiseEventで直接送信する方式で実装する
- ペンで手書きしたテクスチャを送信するサンプルプロジェクトを作ってみた
RaiseEventでテクスチャデータを分割して送る処理、最速で処理しても特にエラーメッセージ出なかった。Photon Cloudの秒間メッセージ数を気にしなければ減速させる処理なしでも全く問題ない。 pic.twitter.com/5MQZgpXBTi
— sotan (@sotanmochi) May 10, 2019
サンプルプロジェクトについて
サンプルプロジェクト ⇒ https://github.com/sotanmochi/TextureSharing-PUN2
テクスチャを同期させたいオブジェクトに TextureSharingComponent.cs をアタッチする。 GetRawTextureDataFromMasterClient()を呼び出すと、 テクスチャデータを取得するリクエスト(RPC)をマスタークライアントへ送信して、データ受信後に呼び出し元のテクスチャを更新する。
using UnityEngine; using Photon.Pun; using Photon.Realtime; using ExitGames.Client.Photon; using UniRx; using UniRxExtension; namespace TextureSharing { public enum StreamingBytesEventCode { BeginStream = 10, Streaming = 11, } public class TextureSharingComponent : MonoBehaviourPunCallbacks, IOnEventCallback { [SerializeField] int messagePerSecond = 100; // 100 Messages / Second int bytePerMessage = 1000; // 1KBytes / Message Texture2D texture; // ★ Readable texture ★ bool isReceiving; byte[] receiveBuffer; int totalDataSize; int currentReceivedDataSize; int receivedMessageCount; void Start() { texture = (Texture2D)GetComponent<Renderer>().material.mainTexture; try { texture.GetPixels32(); } catch(UnityException e) { Debug.LogError("!! This texture is not readable !!"); } } public void GetRawTextureDataFromMasterClient() { photonView.RPC("GetRawTextureDataRPC", RpcTarget.MasterClient); } //************************************************************************** // Client -> MasterClient (These methods are executed by the master client) //************************************************************************** [PunRPC] public void GetRawTextureDataRPC(PhotonMessageInfo info) { byte[] rawTextureData = texture.GetRawTextureData(); int width = texture.width; int height = texture.height; int dataSize = rawTextureData.Length; int viewId = this.photonView.ViewID; Debug.Log("*************************"); Debug.Log(" GetRawTextureDataRPC"); Debug.Log(" RPC sender: " + info.Sender); Debug.Log(" Texture size: " + width + "x" + height + " = " + width*height + "px"); Debug.Log(" RawTextureData: " + rawTextureData.Length + "bytes"); Debug.Log("*************************"); StreamTextureDataToRequestSender(rawTextureData, width, height, dataSize, viewId, info.Sender); } void StreamTextureDataToRequestSender(byte[] rawTextureData, int width, int height, int dataSize, int viewId, Player requestSender) { Debug.Log("***********************************"); Debug.Log(" StreamTextureDataToRequestSender "); Debug.Log("***********************************"); RaiseEventOptions raiseEventOptions = new RaiseEventOptions { CachingOption = EventCaching.DoNotCache, Receivers = ReceiverGroup.All, TargetActors = new int[]{ requestSender.ActorNumber }, }; SendOptions sendOptions = new ExitGames.Client.Photon.SendOptions { Reliability = true, }; // Send info int[] textureInfo = new int[4]; textureInfo[0] = viewId; textureInfo[1] = width; textureInfo[2] = height; textureInfo[3] = dataSize; PhotonNetwork.RaiseEvent((byte)StreamingBytesEventCode.BeginStream, textureInfo, raiseEventOptions, sendOptions); // Send raw data // The SlowDown operator is not necessary if you ignore the limit on the number of messages per second of Photon Cloud. rawTextureData.ToObservable() .Buffer(bytePerMessage) // .SlowDown(1.0f/messagePerSecond) .Subscribe(byteSubList => { byte[] sendData = new byte[byteSubList.Count]; byteSubList.CopyTo(sendData, 0); PhotonNetwork.RaiseEvent((byte)StreamingBytesEventCode.Streaming, sendData, raiseEventOptions, sendOptions); }); } //*************************************************************************** // MasterClient -> Client (These methods are executed by the master client) //*************************************************************************** public void OnEvent(ExitGames.Client.Photon.EventData photonEvent) { if(photonEvent.Code == (byte)StreamingBytesEventCode.BeginStream) { int[] data = (int[])photonEvent.Parameters[ParameterCode.Data]; OnReceivedTextureInfo(data); } if(photonEvent.Code == (byte)StreamingBytesEventCode.Streaming) { byte[] data = (byte[])photonEvent.Parameters[ParameterCode.Data]; OnReceivedRawTextureDataStream(data); } } void OnReceivedTextureInfo(int[] data) { int viewId = data[0]; if (viewId != this.photonView.ViewID) { this.isReceiving = false; this.totalDataSize = 0; this.currentReceivedDataSize = 0; this.receivedMessageCount = 0; return; } this.isReceiving = true; this.currentReceivedDataSize = 0; this.receivedMessageCount = 0; int width = data[1]; int height = data[2]; int dataSize = data[3]; this.totalDataSize = dataSize; this.receiveBuffer = new byte[dataSize]; Debug.Log("*************************"); Debug.Log(" OnReceivedTextureInfo"); Debug.Log(" Texture size: " + width + "x" + height + "px"); Debug.Log(" RawTextureDataSize: " + data[2]); Debug.Log("*************************"); } void OnReceivedRawTextureDataStream(byte[] data) { if (this.isReceiving) { data.CopyTo(this.receiveBuffer, this.currentReceivedDataSize); this.currentReceivedDataSize += data.Length; this.receivedMessageCount++; if (this.currentReceivedDataSize >= (this.totalDataSize)) { this.isReceiving = false; this.currentReceivedDataSize = 0; this.receivedMessageCount = 0; OnReceivedRawTextureData(); } } } void OnReceivedRawTextureData() { Debug.Log("********************************"); Debug.Log(" OnReceivedRawTextureData "); Debug.Log("********************************"); texture.LoadRawTextureData(this.receiveBuffer); texture.Apply(); GetComponent<Renderer>().material.mainTexture = texture; } } }
処理の流れと実装について
GetRawTextureDataFromMasterClient()の呼び出し ↓ リクエスト送信(クライアント ⇒ マスタークライアント) ↓ リクエスト受信とデータ送信準備(マスタークライアント側で実行) ↓ テクスチャデータの分割送信(マスタークライアント ⇒ クライアント) ↓ テクスチャデータの分割受信 ↓ テクスチャ更新処理の実行
リクエスト送信
photonView.RPCを使ってマスタークライアントのGetRawTextureDataRPCを呼び出すことでマスタークライアントへリクエストを送る。
public void GetRawTextureDataFromMasterClient() { photonView.RPC("GetRawTextureDataRPC", RpcTarget.MasterClient); }
リクエスト受信とデータ送信準備
RPCによって、マスタークライアント側でGetRawTextureDataRPCが実行される。
テクスチャデータ(バイト配列)などを取得して、データ送信の処理を呼び出す。
テクスチャはReadableでなければならない。
[PunRPC] public void GetRawTextureDataRPC(PhotonMessageInfo info) { byte[] rawTextureData = texture.GetRawTextureData(); int width = texture.width; int height = texture.height; int dataSize = rawTextureData.Length; int viewId = this.photonView.ViewID; Debug.Log("*************************"); Debug.Log(" GetRawTextureDataRPC"); Debug.Log(" RPC sender: " + info.Sender); Debug.Log(" Texture size: " + width + "x" + height + " = " + width*height + "px"); Debug.Log(" RawTextureData: " + rawTextureData.Length + "bytes"); Debug.Log("*************************"); StreamTextureDataToRequestSender(rawTextureData, width, height, dataSize, viewId, info.Sender); }
テクスチャデータの分割送信
RaiseEventを使ってテクスチャの情報とデータ(バイト配列)を送信する。
void StreamTextureDataToRequestSender(byte[] rawTextureData, int width, int height, int dataSize, int viewId, Player requestSender) { Debug.Log("***********************************"); Debug.Log(" StreamTextureDataToRequestSender "); Debug.Log("***********************************"); RaiseEventOptions raiseEventOptions = new RaiseEventOptions { CachingOption = EventCaching.DoNotCache, Receivers = ReceiverGroup.All, TargetActors = new int[]{ requestSender.ActorNumber }, }; SendOptions sendOptions = new ExitGames.Client.Photon.SendOptions { Reliability = true, }; // Send info int[] textureInfo = new int[4]; textureInfo[0] = viewId; textureInfo[1] = width; textureInfo[2] = height; textureInfo[3] = dataSize; PhotonNetwork.RaiseEvent((byte)StreamingBytesEventCode.BeginStream, textureInfo, raiseEventOptions, sendOptions); // Send raw data // The SlowDown operator is not necessary if you ignore the limit on the number of messages per second of Photon Cloud. rawTextureData.ToObservable() .Buffer(bytePerMessage) // .SlowDown(1.0f/messagePerSecond) .Subscribe(byteSubList => { byte[] sendData = new byte[byteSubList.Count]; byteSubList.CopyTo(sendData, 0); PhotonNetwork.RaiseEvent((byte)StreamingBytesEventCode.Streaming, sendData, raiseEventOptions, sendOptions); }); }
RaiseEventを使ってテクスチャデータ(バイト配列)を分割送信している箇所は以下の通り。
UniRxのBufferオペレータを使うことで、[bytePerMessage]バイトずつデータを送る。
コメントアウトされているが「SlowDown」は自作したオペレータ。
Photonの秒間メッセージ数制限を考慮してデータ送信速度を下げる時に使えるように作った。
// Send raw data // The SlowDown operator is not necessary if you ignore the limit on the number of messages per second of Photon Cloud. rawTextureData.ToObservable() .Buffer(bytePerMessage) // .SlowDown(1.0f/messagePerSecond) .Subscribe(byteSubList => { byte[] sendData = new byte[byteSubList.Count]; byteSubList.CopyTo(sendData, 0); PhotonNetwork.RaiseEvent((byte)StreamingBytesEventCode.Streaming, sendData, raiseEventOptions, sendOptions); });
画像出典:http://reactivex.io/documentation/operators/buffer.html
bytePerMessageとmessagePerSecondの値は以下のページで、データグラムのサイズや秒間メッセージ数の制約を参考にした。
テクスチャデータの分割受信
受信したテクスチャデータ(バイト配列)をバッファに格納する。 全データを受信したらテクスチャ更新処理を呼び出す。
void OnReceivedRawTextureDataStream(byte[] data) { if (this.isReceiving) { data.CopyTo(this.receiveBuffer, this.currentReceivedDataSize); this.currentReceivedDataSize += data.Length; this.receivedMessageCount++; if (this.currentReceivedDataSize >= (this.totalDataSize)) { this.isReceiving = false; this.currentReceivedDataSize = 0; this.receivedMessageCount = 0; OnReceivedRawTextureData(); } } }
テクスチャ更新処理
バッファに格納されているデータをテクスチャに格納してApplyで変更を適用する。
void OnReceivedRawTextureData() { Debug.Log("********************************"); Debug.Log(" OnReceivedRawTextureData "); Debug.Log("********************************"); texture.LoadRawTextureData(this.receiveBuffer); texture.Apply(); GetComponent<Renderer>().material.mainTexture = texture; }
まとめ
- Photon Unity Networking 2 (PUN 2)を使って、ランタイムで生成されたテクスチャ画像を他のユーザーに送信した
- オンラインストレージを経由する方式ではなく、インメモリのデータをRaiseEventで直接送信する方式で実装した
- バイト配列を送受信しているだけなので、テクスチャ以外の大きなデータを送りたい場合にも応用できる
- ランタイムで生成されない静的な画像データなどの場合は今回の実装方式である必要はないので、使いどころはよく考えること
- データ通信量には注意が必要。頻度が多い場合は、別のアプローチで情報を共有・同期できないかを考えること (例えば、ペンの動きを同期するような方式にして、テクスチャへの書き込み自体は各クライアントごとに処理する)