Monobit Unity Networking 2 (MUN2) のRPCを使ってテクスチャデータを送信する
はじめに
- Monobit Unity Networking 2 (MUN 2)を使って、ランタイムで生成されたテクスチャ画像を他のユーザーに送信したい
- オンラインストレージを経由する方式ではなく、インメモリのデータをRPCで直接送信する方式で実装する
- ペンで手書きしたテクスチャを送信するサンプルプロジェクトを作ってみた
これのモノビットエンジン版
RaiseEventでテクスチャデータを分割して送る処理、最速で処理しても特にエラーメッセージ出なかった。Photon Cloudの秒間メッセージ数を気にしなければ減速させる処理なしでも全く問題ない。 pic.twitter.com/5MQZgpXBTi
— sotan (@sotanmochi) May 10, 2019
サンプルプロジェクトについて
サンプルプロジェクト ⇒ https://github.com/sotanmochi/TextureSharing-MUN2
テクスチャを同期させたいオブジェクトに TextureSharingComponentForMUN.cs をアタッチする。 GetRawTextureDataFromMasterClient()を呼び出すと、 テクスチャデータを取得するリクエスト(RPC)をマスタークライアントへ送信して、データ受信後に呼び出し元のテクスチャを更新する。
TextureSharingComponentForMUN.cs
using UnityEngine; using UniRx; using MonobitEngine; namespace TextureSharing { public class TextureSharingComponentForMUN : MonobitEngine.MonoBehaviour { 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() { monobitView.RPC("GetRawTextureDataRPC", MonobitTargets.Host, MonobitNetwork.player); } //************************************************************************** // Client -> MasterClient (These methods are executed by the master client) //************************************************************************** [MunRPC] void GetRawTextureDataRPC(MonobitPlayer requestSender) { byte[] rawTextureData = texture.GetRawTextureData(); int width = texture.width; int height = texture.height; int dataSize = rawTextureData.Length; int viewId = this.monobitView.viewID; Debug.Log("*************************"); Debug.Log(" GetRawTextureDataRPC"); Debug.Log(" RPC sender: " + requestSender.ID); Debug.Log(" Texture size: " + width + "x" + height + " = " + width*height + "px"); Debug.Log(" RawTextureData: " + rawTextureData.Length + "bytes"); Debug.Log("*************************"); StreamTextureDataToRequestSender(rawTextureData, width, height, dataSize, viewId, requestSender); } void StreamTextureDataToRequestSender(byte[] rawTextureData, int width, int height, int dataSize, int viewId, MonobitPlayer targetPlayer) { Debug.Log("***********************************"); Debug.Log(" StreamTextureDataToRequestSender "); Debug.Log("***********************************"); // Send info int[] textureInfo = new int[4]; textureInfo[0] = viewId; textureInfo[1] = width; textureInfo[2] = height; textureInfo[3] = dataSize; monobitView.RPC("OnReceivedTextureInfo", targetPlayer, textureInfo); // Send raw data rawTextureData.ToObservable() .Buffer(bytePerMessage) .Subscribe(byteSubList => { byte[] sendData = new byte[byteSubList.Count]; byteSubList.CopyTo(sendData, 0); monobitView.RPC("OnReceivedRawTextureDataStream", targetPlayer, sendData); }); } //*************************************************************************** // MasterClient -> Client (These methods are executed by the master client) //*************************************************************************** [MunRPC] void OnReceivedTextureInfo(int[] data) { int viewId = data[0]; if (viewId != this.monobitView.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[3]); Debug.Log("*************************"); } [MunRPC] 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()の呼び出し ↓ リクエスト送信(クライアント ⇒ マスタークライアント) ↓ リクエスト受信とデータ送信準備(マスタークライアント側で実行) ↓ テクスチャデータの分割送信(マスタークライアント ⇒ クライアント) ↓ テクスチャデータの分割受信 ↓ テクスチャ更新処理の実行
リクエスト送信
monobitView.RPCを使ってマスタークライアントのGetRawTextureDataRPCを呼び出すことでマスタークライアント(ホスト)へリクエストを送る。
public void GetRawTextureDataFromMasterClient() { monobitView.RPC("GetRawTextureDataRPC", MonobitTargets.Host, MonobitNetwork.player); }
リクエスト受信とデータ送信準備
RPCによって、マスタークライアント(ホスト)側でGetRawTextureDataRPCが実行される。
テクスチャデータ(バイト配列)などを取得して、データ送信の処理を呼び出す。
テクスチャはReadableでなければならない。
[MunRPC] void GetRawTextureDataRPC(MonobitPlayer requestSender) { byte[] rawTextureData = texture.GetRawTextureData(); int width = texture.width; int height = texture.height; int dataSize = rawTextureData.Length; int viewId = this.monobitView.viewID; Debug.Log("*************************"); Debug.Log(" GetRawTextureDataRPC"); Debug.Log(" RPC sender: " + requestSender.ID); Debug.Log(" Texture size: " + width + "x" + height + " = " + width*height + "px"); Debug.Log(" RawTextureData: " + rawTextureData.Length + "bytes"); Debug.Log("*************************"); StreamTextureDataToRequestSender(rawTextureData, width, height, dataSize, viewId, requestSender); }
テクスチャデータの分割送信
RPCを使ってテクスチャの情報とデータ(バイト配列)を送信する。
void StreamTextureDataToRequestSender(byte[] rawTextureData, int width, int height, int dataSize, int viewId, MonobitPlayer targetPlayer) { Debug.Log("***********************************"); Debug.Log(" StreamTextureDataToRequestSender "); Debug.Log("***********************************"); // Send info int[] textureInfo = new int[4]; textureInfo[0] = viewId; textureInfo[1] = width; textureInfo[2] = height; textureInfo[3] = dataSize; monobitView.RPC("OnReceivedTextureInfo", targetPlayer, textureInfo); // Send raw data rawTextureData.ToObservable() .Buffer(bytePerMessage) .Subscribe(byteSubList => { byte[] sendData = new byte[byteSubList.Count]; byteSubList.CopyTo(sendData, 0); monobitView.RPC("OnReceivedRawTextureDataStream", targetPlayer, sendData); }); }
RPCを使ってテクスチャデータ(バイト配列)を分割送信している箇所は以下の通り。
UniRxのBufferオペレータを使うことで、[bytePerMessage]バイトずつデータを送る。
// Send raw data rawTextureData.ToObservable() .Buffer(bytePerMessage) .Subscribe(byteSubList => { byte[] sendData = new byte[byteSubList.Count]; byteSubList.CopyTo(sendData, 0); monobitView.RPC("OnReceivedRawTextureDataStream", targetPlayer, sendData); });
画像出典:http://reactivex.io/documentation/operators/buffer.html
テクスチャデータの分割受信
受信したテクスチャデータ(バイト配列)をバッファに格納する。 全データを受信したらテクスチャ更新処理を呼び出す。
[MunRPC] 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; }
まとめ
- Monobit Unity Networking 2 (MUN 2)を使って、ランタイムで生成されたテクスチャ画像を他のユーザーに送信した
- オンラインストレージを経由する方式ではなく、インメモリのデータをRPCで直接送信する方式で実装した
- バイト配列を送受信しているだけなので、テクスチャ以外の大きなデータを送りたい場合にも応用できる
- ランタイムで生成されない静的な画像データなどの場合は今回の実装方式である必要はないので、使いどころはよく考えること
- データ通信量には注意が必要。頻度が多い場合は、別のアプローチで情報を共有・同期できないかを考えること (例えば、ペンの動きを同期するような方式にして、テクスチャへの書き込み自体は各クライアントごとに処理する)