[Unity]UNET の LLAPI(Low-Level API) を使ってテクスチャを送受信する
こんにちは、たるこすです。
Unity には複数のUnityアプリ間で通信を行うための、UNET という仕組みがあります。
UNET には HLAPI (High-Level API) と LLAPI (Low-Level API) があり、
今回は LLAPI を用いてテクスチャの送受信を行いたいと思います。
LLAPI はソケット通信をラップしたようなものなので、
別の Unity アプリ間でも通信が可能です。
今回紹介するコードやサンプルシーンを含んだUnityプロジェクトは以下のリポジトリで公開しているので、 良ければ参考にしてみてください。
初期化(Server側, Client側)
public class LLAPINetworkManager : MonoBehaviour { public int localPort = 8212; public int maxConnection = 10; private Dictionary<QosType, int> channelIdDictionary = new Dictionary<QosType, int>(); private int hostId; void Start() { NetworkTransport.Init(); ConnectionConfig config = new ConnectionConfig(); foreach (QosType qosType in Enum.GetValues(typeof(QosType))) { var channeld = config.AddChannel(qosType); channelIdDictionary.Add(qosType, channeld); } HostTopology topology = new HostTopology(config, maxConnection); hostId = NetworkTransport.AddHost(topology, localPort); } ... }
はじめに、使用するポートやチャンネルなどの初期設定を行います。
チャンネルには QoS(Quality of Service) を設定します。
QoS はデータの送り方(再送処理を行うかどうかなど)を表しています。
接続(Client側)
public class LLAPINetworkManager : MonoBehaviour { public string serverAddress = "127.0.0.1"; public int serverPort = 8212; private int connectionId; void Connect() { byte error; connectionId = NetworkTransport.Connect(hostId, serverAddress, serverPort, 0, out error); } ... }
Client側からServer側に接続します。
戻り値で取得した connectionId はデータ送信時に利用します。
データ送信(Server側、Clien側)
public class LLAPINetworkManager : MonoBehaviour { public void SendPacketData(byte[] data, QosType qos = QosType.Reliable) { var channelId = channelIdDictionary[qos]; byte error; NetworkTransport.Send(hostId, connectionId, channelId, data, data.Length, out error); } ... }
hostId, connectionId, channelId を指定して、byte 配列のデータを送信します。
Client側の場合は Connect 時に connectionId を取得できますが、
Server側では次に紹介するデータ受信の ConnectEvent を受け取った際に connectionId を取得できます。
データ受信(Server側、Client側)
public class LLAPINetworkEventArgs : EventArgs { public NetworkEventType eventType { set; get; } public byte[] data { set; get; } public LLAPINetworkEventArgs(NetworkEventType t, byte[] d) { eventType = t; data = d; } } public class LLAPINetworkManager : MonoBehaviour { public bool isServer = true; private bool connected = false; public readonly int MaxBufferSize = 65535; public delegate void NetworkEventHandler(object sender, LLAPINetworkEventArgs e); public event NetworkEventHandler OnConnected = delegate (object s, LLAPINetworkEventArgs e) { }; public event NetworkEventHandler OnDisconnected = delegate (object s, LLAPINetworkEventArgs e) { }; public event NetworkEventHandler OnDataReceived = delegate (object s, LLAPINetworkEventArgs e) { }; public event NetworkEventHandler DataReceived = delegate (object s, LLAPINetworkEventArgs e) { }; void Update() { int recHostId; int connectionId; int channelId; byte[] recBuffer = new byte[MaxBufferSize]; int bufferSize = MaxBufferSize; int dataSize; byte error; NetworkEventType recData = NetworkTransport.Receive(out recHostId, out connectionId, out channelId, recBuffer, bufferSize, out dataSize, out error); LLAPINetworkEventArgs e; switch (recData) { case NetworkEventType.Nothing: if (!isServer && !connected) { Connect(); connected = true; } break; case NetworkEventType.ConnectEvent: this.connectionId = connectionId; connected = true; e = new LLAPINetworkEventArgs(recData, null); OnConnected(this, e); break; case NetworkEventType.DataEvent: e = new LLAPINetworkEventArgs(recData, recBuffer); OnDataReceived(this, e); break; case NetworkEventType.DisconnectEvent: connected = false; e = new LLAPINetworkEventArgs(recData, null); OnDisconnected(this, e); break; } } ... }
Update 関数内で、NetworkTransport.Receive を呼び、接続イベントの受け取りやデータ受信を行います。
先程書いたとおり、Server 側ではここで connectionId を取得できます。
(ただし、上記コードでは複数のClientから接続された場合、connectionId を上書いてしまいます。)
テクスチャの送信
public class TextureSender : MonoBehaviour { public LLAPINetworkManager NetworkManager; Type textureType; Texture2D texture2D; RenderTexture renderTexture; void SendTexture() { var texture = GetComponent<Renderer>().material.mainTexture; textureType = texture.GetType(); if (textureType == typeof(Texture2D)) { texture2D = texture as Texture2D; } else if (textureType == typeof(RenderTexture)) { renderTexture = texture as RenderTexture; } if (textureType == typeof(RenderTexture)) { RenderTexture currentActiveRT = RenderTexture.active; RenderTexture.active = renderTexture; texture2D = new Texture2D(renderTexture.width, renderTexture.height); texture2D.ReadPixels(new Rect(0, 0, texture2D.width, texture2D.height), 0, 0); } var pngTexture = texture2D.EncodeToPNG(); NetworkManager.SendPacketData(pngTexture, QosType.UnreliableFragmented); } }
Texture2D の EncodeToPNG 関数を呼ぶことで、png 画像形式のバイト配列を取得できます。
これを、先程のデータ送信のコードを使って送信します。
テクスチャの受信
public class TextureReceiver : MonoBehaviour { public LLAPINetworkManager NetworkManager; Texture2D mainTexture; private void Awake() { mainTexture = (Texture2D)GetComponent<Renderer>().material.mainTexture; NetworkManager.OnDataReceived += OnDataReceived; } void OnDataReceived(object o, LLAPINetworkEventArgs args) { ApplyTextureData(args.data); } void ApplyTextureData(byte[] data) { using (MemoryStream inputStream = new MemoryStream(data)) { BinaryReader reader = new BinaryReader(inputStream); byte[] readBinary = reader.ReadBytes((int)reader.BaseStream.Length); // get texture size int pos = 16; int width = 0; for (int i = 0; i < 4; i++) { width = width * 256 + readBinary[pos++]; } int height = 0; for (int i = 0; i < 4; i++) { height = height * 256 + readBinary[pos++]; } var texture = new Texture2D(width, height); texture.LoadImage(readBinary); Destroy(GetComponent<Renderer>().material.mainTexture); GetComponent<Renderer>().material.mainTexture = texture; } } }
テクスチャの縦横の大きさをPNGデータから取得し、
その値でTexture2Dを初期化しています。
受信したバイト配列のPNGデータからTextureに変換するには、
Texture2D の LoadImage を使います。
おまけ
今回紹介した仕組みを使って Android アプリと HoloLens アプリで通信を行い、 Android アプリから書いた絵やコマンドが送れるようになりました。