Monobit Unity Networking 2 (MUN2) のRPCを使ってテクスチャデータを送信する

はじめに

  • Monobit Unity Networking 2 (MUN 2)を使って、ランタイムで生成されたテクスチャ画像を他のユーザーに送信したい
  • オンラインストレージを経由する方式ではなく、インメモリのデータをRPCで直接送信する方式で実装する
  • ペンで手書きしたテクスチャを送信するサンプルプロジェクトを作ってみた

これのモノビットエンジン版

サンプルプロジェクトについて

サンプルプロジェクト ⇒ https://github.com/sotanmochi/TextureSharing-MUN2

テクスチャを同期させたいオブジェクトに TextureSharingComponentForMUN.cs をアタッチする。 GetRawTextureDataFromMasterClient()を呼び出すと、 テクスチャデータを取得するリクエスト(RPC)をマスタークライアントへ送信して、データ受信後に呼び出し元のテクスチャを更新する。

f:id:sotanmochi-tech:20190515174539p:plain

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);
}

f:id:sotanmochi-tech:20190515131748p:plain

テクスチャデータの分割送信

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);
    });

f:id:sotanmochi-tech:20190515171112p:plain 画像出典: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で直接送信する方式で実装した
  • バイト配列を送受信しているだけなので、テクスチャ以外の大きなデータを送りたい場合にも応用できる
  • ランタイムで生成されない静的な画像データなどの場合は今回の実装方式である必要はないので、使いどころはよく考えること
  • データ通信量には注意が必要。頻度が多い場合は、別のアプローチで情報を共有・同期できないかを考えること (例えば、ペンの動きを同期するような方式にして、テクスチャへの書き込み自体は各クライアントごとに処理する)