凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

HoloLens で UDP 通信を行う方法について調べてみた

はじめに

前回のデモで iPad と HoloLens を OSC で通信しようとした際に通信周りが面倒だったのでまとめておきます。

tips.hecomi.com

ちなみに OSC はアドレス付きのデータを扱えるプロトコルで、UDP ベースのものがよく使われます。

薄いプロトコルなので速いですし、生の UDP よりもアドレスや型情報がついている分、圧倒的に使いやすいのでオススメです。

デモ

前回の一例ですが、ここではタッチ座標を iPad から OSC で送信して、それを HoloLens 側で受け取って、その位置に線を描く、というものになっています。

f:id:hecomi:20170415135131g:plain

概要

Unity では System.Net.Sockets.UdpClient クラスを利用して簡単に UDP 通信が可能です。

using UnityEngine;
using System.Net;
using System.Net.Sockets;

public class UdpTest : MonoBehaviour
{
    [SerializeField]
    int listenPort = 3333;

    UdpClient udpClient_;
    IPEndPoint endPoint_;

    void Start()
    {
        endPoint_ = new IPEndPoint(IPAddress.Any, listenPort);
        udpClient_ = new UdpClient(endPoint_);
    }

    void Update()
    {
        while (udpClient_.Available > 0) {
            byte[] data = udpClient_.Receive(ref endPoint_);
            // data を使って何か処理する
        }
    }
}

ただ、HoloLens 上で動く UWP アプリでは UdpClient クラスが含まれていません。

代わりに、Windows.Networking.Sockets.DatagramSocket を使う必要があります。

参考: Hololens UDP server? — Hololens Developer Community

コード

ここでは UDP 経由で受け取ったデータは OSC のメッセージに変換して処理するものとします。例として OSC への変換には keijiro さんの unity-osc を利用しています。

github.com

using UnityEngine;
using UnityEngine.Assertions;

#if UNITY_EDITOR
using System.Net;
using System.Net.Sockets;
#else
using System;
using System.IO;
using Windows.Networking.Sockets;
#endif

public class TouchOscServer : MonoBehaviour
{
    [SerializeField]
    int listenPort = 3333;

    Osc.Parser osc_ = new Osc.Parser();

    void OnMessage(Osc.Message msg)
    {
        // ここで適当に処理する
        Debug.LogFormat("{0} => {1}", msg.path, msg.data[0]);
    }
    
#if UNITY_EDITOR
    UdpClient udpClient_;
    IPEndPoint endPoint_;

    void Start()
    {
        Assert.IsNotNull(handler, "should set handler.");
        endPoint_ = new IPEndPoint(IPAddress.Any, listenPort);
        udpClient_ = new UdpClient(endPoint_);
    }

    void Update()
    {
        while (udpClient_.Available > 0) {
            var data = udpClient_.Receive(ref endPoint_);
            osc_.FeedData(data);
        }

        while (osc_.MessageCount > 0) {
            var msg = osc_.PopMessage();
            OnMessage(msg);
        }
    }
#else
    DatagramSocket socket_;
    object lockObject_ = new object();

    const int MAX_BUFFER_SIZE = 1024;
    byte[] buffer = new byte[MAX_BUFFER_SIZE];

    async void Start()
    {
        try {
            socket_ = new DatagramSocket();
            socket_.MessageReceived += OnMessage;
            await socket_.BindServiceNameAsync(listenPort.ToString());
        } catch (System.Exception e) {
            Debug.LogError(e.ToString());
        }
    }

    void Update()
    {
        lock (lockObject_) {
            while (osc_.MessageCount > 0) {
                var msg = osc_.PopMessage();
                OnMessage(msg);
            }
        }
    }

    async void OnMessage(DatagramSocket sender, DatagramSocketMessageReceivedEventArgs args)
    {
        using (var stream = args.GetDataStream().AsStreamForRead()) {
            await stream.ReadAsync(buffer, 0, MAX_BUFFER_SIZE);
            lock (lockObject_) {
                osc_.FeedData(buffer);
            }
        }
    }
#endif
}

}

Unity と UWP 側で UDP の処理を切り分けています。Unity Eidtor 上の C# プロジェクトでは #else ブロックの内容が補完付きで編集できませんが、ビルド後の C# プロジェクトの中であれば補完付きで編集やデバッグもできるので、手を加えたい場合はビルド後のプロジェクトに対してやるのをオススメします。

本例では OSC 側でデータをキューに詰めてもらってますが(Osc.Parser.FeedData())、生の UDP のデータとして使いたい場合も同様に受信部でキューしておいて、メインスレッドで処理されるよう次回の Update 時にキューから取り出して処理、みたいな流れになると思います。

おわりに

HoloLens といろんなものを連携させて是非遊んでみてください。