Photon Unity Networking 2 (PUN2) のRPCとRaiseEventを使ってテクスチャデータを送信する

はじめに

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

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

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

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

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

TextureSharingComponent.cs

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

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

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

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

f:id:sotanmochi-tech:20190515171112p:plain 画像出典:http://reactivex.io/documentation/operators/buffer.html

bytePerMessageとmessagePerSecondの値は以下のページで、データグラムのサイズや秒間メッセージ数の制約を参考にした。

doc.photonengine.com

connect.unity.com

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

受信したテクスチャデータ(バイト配列)をバッファに格納する。 全データを受信したらテクスチャ更新処理を呼び出す。

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