Mixed Reality Toolkit - Unity で始めるUNET ～ SharingWithUNETの仕組み

# HoloLensやImmersiveHMDのデバイス間の連携
Mixed Reality Toolkit - Untiy（以下MRTK-Unity）はSharingの機能を提供しています。これを利用すれば複数のHoloLensで同じアプリケーションを体験することができます。
さらに、MRTK-Unityでは昨年リリースされたWindows Mixed Realityデバイスと連携も可能です。この機能のサンプルアプリケーションとしてMixed Reality 250が提供されています。
今回は後者で使用している機能「SharingWithUNET」で提供されているコンポーネントについての機能を紹介します。

## MTRK-Unityがもつ機能
MRTK-Unityは2種類の方法を提供してます。

* Sharing機能
* SharingWithUNET機能

### Sharing機能
HoloLens同士をネットワーク接続しアプリの共有体験を可能にする機能です。
この機能は専用のSharingサーバが必要です。HoloLensはSharingサーバを介して通信を行い体験の共有ができます。
Sharingサーバが必要ですが、クラウド上に配置することで遠隔地で体験を共有する機能も構築可能になります。

### SharingWithUNET機能
この機能はUnityの通信機能であるUNETを使った通信を実現します。この機能では同一LAN内のHoloLensやWindows Mixed Realityデバイスとの連携が可能となっています。UNETでは最初に起動したデバイスがHostとして動作し、他のデバイスはHostに参加するという仕組みになっています。
このサンプルとしてMixed Reality 250があります。
ただし、Mixed Reality 250ではMRTK-Unityの古いバージョンのものが使われています。SharingWithUNETやImmersive HMDに対応する機能を持っていません。このために新しいMRTK-Unityから必要な機能を追加されたものとなっています。

#### SharingWithUNETは現在MRTK-Unityのサンプルに移動
Host用のサーバが不要になり、Sharing機能を使ったアプリケーションをストア公開できるため、有効な技術です。ただ、最新のMRTK-Unity(2017.2.1.3)ではExamplesに移動しました。
SharingWithUNETを利用する場合、開発者がその機能のカスタイズ（コード修正）が必要なのが理由であると考えられます。
例えば、キャラクターの制御等を作る場合などはPlayerの機能を修正しなければならないです。
これが理由でサンプルコードとして公開する方がふさわしいとなったのではないかと思います。

## 今回の構成について

環境については以下の通りです。

* Windows 10 Fall Creators Update
* Unity 2017.2.1p2
* Mixed Reality Toolkit - Unity 2017.2.1.1
* Visual Studio 2017 Community Edition(15.6)

### Mixed Reality Toolkit - UnityでのUNET関連の話
最新のMRTKを利用する場合は2017.2.1.3を使用してください。2017.2.1.2はビルドエラーが発生し使えません。
また、最新の2017.2.1.3では今回利用しているSharingWithUNETの機能が削除されてExamplesの方に移っています。
SharingWithUNETとして提供されていた機能は、開発者が利用する際に必ず修正が必要なものが多くライブラリの機能としては不適切なためExamplesでサンプルコードとしての扱いに変えたと考えられます。

## SharingWithUNETの機能
SharingWithUNETは名前の通りUnityのUNETの機能を利用しています。UNETはアプリケーションを起動したデバイスのいずれかがHostとして機能しネットワークアプリケーションを構築できます。
SharingWithUNETはこのUNETの機能をMixed Reality向けにラッパーしたものになります。よって基本的な考え方はUNETの機能と同じです。

### SharingWithUNETの部品

SharingWithUNETが提供する部品は以下の通りです。
このうちNetBullet、PlayerはSharingWithUNETのサンプルアプリケーションで利用します。

|プロパティ名|種類|説明|
|:-:|:--|:--|
|NetBullet|Prefab|SharingWithUNETのサンプルで使用する弾です。カメラの正面方向に一定時間とんだ消滅します。各デバイス間の座標の同期は「NetworkTransform」を使用。|
|Player|Prefab|カメラの位置にCubeを表示するプレーヤです。カメラの位置情報をHostとClient間で通信し共有します。またHoloLens同士の位置合わせを行うためのアンカー制御の機能も実装しています。|
|UNETAnchorManager|Prefab|UNET経由でアンカーを管理機能を提供するPrefabです。|
|UIContainer|Prefab|Unityが提供するNetworkDiscoveryでネットワーク接続の制御（開始等）のための標準UIの代わりに利用するPrefabです|
|BulletController|Component|SharingWithUNETのサンプルで使用する弾に対して利用するコンポーネントです。|
|GenericNetworkTransmitter|Script|UWPでIPアドレスによるデータ送受信を行うための部品です。|
|NetworkDiscoveryWithAnchors|Script|UNETのNetworkDiscoveryコンポーネントです。UNETAnchorManagerのアンカーの生成/更新/ダウンロートなどの機能を利用してアンカー制御を行う機能を追加しています。|
|PlayerController|Component|カメラの位置にCubeを表示するプレーヤコンポーネントです。|
|SharedAnchorDebugText|Component|アンカー共有についてのデバッグを出力するためのコンポーネントです。デバッグ時に利用します。|
|SharedCollection|Component|UNETで共有するGameObjectのルートを表すコンポーネントです。このコンポーネントが空のGameObjectに追加してScene内に配置します。|
|UNetSharedHologram|Component|GameObjectの移動を行うためのコンポーネントです。このコンポーネントはMixed Reality 250のHoloLens側でアプリを起動した際に島を移動する際の操作方法を実現するためのコンポーネントになります。|
|Energy_Laser_Fire|sound|弾のSEです。|

### 機能
以下はSharingWithUNETに関する主要な部品の機能については紹介します。

#### NetBullet

![image](https://qiita-image-store.s3.amazonaws.com/0/152548/a3eb3166-cf83-8c45-e151-e37f7cd07b08.png)

MRTKの中のSharingWithUNETのサンプルで利用される弾のPrefabです。PlayerControllerの中でAirTapがされるとこのオブジェクトが生成され弾が発射されます。
BulletControllerコンポーネントでは以下の処理をします。

* 生成したNetBulletの親オブジェクトを後述のSharedCollectionに変更
* 弾の打ち出す方向をSharedCollectionのワールド座標系に変換

NetBulletの同期はNetworkTransformを利用しています。

#### Player
![image](https://qiita-image-store.s3.amazonaws.com/0/152548/fca7b821-fa5d-c36c-0f1b-3ed8cb6867b7.png)
各デバイスのプレーヤを表すPrefabです。プレーヤーの座標点にはCubeを配置します。
プレーヤーに関する処理はPlayerControllerコンポーネントで実装されており以下の処理をします。

* HoloLensでアプリを使用している場合はUNETAnchorManagerでアンカーを管理
* プレーヤーの座標点(=カメラ位置）を同期
* AirTap（Immersiveの場合はSelectボタンを押下）時に弾を発射

アンカー制御が実装済みの状態なので、UNETでHoloLens同士を接続する場合はこのPlayerを参考にして必要な動作を加えるといいと思います。

#### UNETAnchorManager

![image](https://qiita-image-store.s3.amazonaws.com/0/152548/1ed0536b-5ef6-12c0-1410-39eba1c79adf.png)

UNET経由でアンカーを管理（生成/エクスポート/インポート）を提供するPrefabです。
この機能はImmersive HMDでは無効になります。特にパラメータなどは設定不要でPrefabをSceneに追加するだけで利用可能です。


#### UIContainer
![image](https://qiita-image-store.s3.amazonaws.com/0/152548/5638f1b4-acc5-562a-3186-13046f5daa75.png)

この機能はUnityで提供されるNetworkDiscoveryのGUIを置き換えるものです。
UNETでのネットワーク接続の開始および参加を行うためのUI部品です。Hostとなっているデバイスのリスト表示部、Hostの開始(Start)、Hostに参加(Join)の3つの構成となっています。HostになるユーザについてはStartを押すと他の参加者待ちの状態でアプリが開始されます。それ以外のユーザは接続リストに表示されているデバイス名を選択しJoinを押すことで参加することができます。

#### GenericNetworkTransmitter
UWPでIPアドレスによるデータ送受信を行うための部品です。データはバイナリデータです。この部品はUWP用なのでUnityEditor上では動作しない部品です。

#### NetworkDiscoveryWithAnchors
![image](https://qiita-image-store.s3.amazonaws.com/0/152548/bdecee8f-903a-d1e6-7978-9e4ba6ebbdff.png)
UNETのNetworkDiscoveryを拡張したコンポーネントです。NetworkDiscoveryはHostとして開始したデバイスのIPアドレスを同一LAN内にBroadcastします。他のデバイスはBroadcastで送信されているIPアドレスに対して参加を行うことでネットワーク通信を可能にします。NetworkDiscoveryWithAnchorsはこの機能に加えて、UNETAnchorManagerを利用してアンカー制御を行う機能を追加しています。プロパティに「Show GUI」とあるのですが、これにチェックが入っているとNetworkDiscoveryの通常のUIが表示されます。MRTKはUIContainerというリスト形式でHostを表示して選択するためのUIが別に提供しています。通常はこちらを使う方がよいと思います。

#### SharedCollection
UNETで共有するGameObjectのルートを表すコンポーネントです。このコンポーネントは空のGameObjectに追加してScene内に配置します。アンカーによる位置調整はこのSharedCollectionに対して実施されます。このためゲーム内で共有するGameObjectはSharedCollectionが追加されたオブジェクトを親に持つ必要があります。

#### UNetSharedHologram
GameObjectの移動を行うためのコンポーネントです。このコンポーネントをGameObjectに追加するとAirTap毎に以下の２つのモードを交互に切り替えます。

* 配置モード
* 同期モード

Mixed Reality 250のHoloLens側でアプリを起動した際に島を移動する際の操作方法を実現するためのコンポーネントになります。また、この機能はSpatialMappingを利用するためHoloLens専用です。Immersiveでは使えません。

##### 配置モード

GameObjectの同期を一時的に解除してGameObjectを配置するモードです。配置モードの場合カメラの正面方向にRayを飛ばし、Spatial Mappingとの境界を衝突した座標にGameObjectを配置します。再度AirTapを行うとGameObjectの位置が確定し、同期モードに移行します。

##### 同期モード

定期的に別のユーザが変更した座標情報の同期を行います。

### SharingWithUNETの仕組み

#### アプリケーションの開始
![image](https://qiita-image-store.s3.amazonaws.com/0/152548/c06b5ba7-a5df-b58a-ec16-da9618e9f900.png)

ベースはUNETを利用しているためUnityの動作とほとんど違いはないです。
SceneにUIContainerを追加していた場合、アプリケーション開始時にUIContainerのダイアログを表示します。HoloLensはHostもしくはクライアントとして参加することができます。同一ネットワーク上であればHostのIPアドレスがBroadcastされています。後から参加するHoloLensやImmersiveデバイスではUIContainerがHostの情報をリストに表示されます。
リストからHostを選択するとアプリケーションが開始されます。

#### アンカー制御
HoloLensの場合はサンプルのアプリではアンカー制御も併せて行っています。
デバイス間でアンカーを制御するためのUNETAnchorManagerが提供されています。

このコンポーネントは以下のコンポーネントを参照して動作する仕組みです。

* GenericNetworkTransmitter
* NetworkManager
* SharedCollection
* SpatialMappingManager(任意）

SpatialMappingManagerについては任意です。Spatial Mappingはアンカーの位置を検出する際に使用される場合があります。

##### UNETでのアンカーの同期（作成とエクスポート）

アンカーについてはHostとなるHoloLensのアンカーが基準となりクライアントに接続するHoloLens同士で同期化が行われます。

###### 1. NetworkDiscoveryWithAnchors.StartHosting
UIContainerでStartボタンをAirTapするとNetworkDiscoveryWithAnchors.StartHostingが呼ばれます。この中でUNETAnchorManager.CreateAnchorが実行されます。

```csharp:NetworkDiscoveryWithAnchor.cs
        /// <summary>
        /// Call to create a session 
        /// </summary>
        /// <param name="SessionName">The name of the session if a name can't be calculated</param>
        public void StartHosting(string SessionName)
        {
            StopListening();

#if !UNITY_EDITOR && UNITY_WSA
            NetworkManager.singleton.serverBindToIP = true;
            NetworkManager.singleton.serverBindAddress = LocalIp;
#endif
            // Starting as a 'host' makes us both a client and a server.
            // There are nuances to this in UNet's sync system, so do make sure
            // to test behavior of your networked objects on both a host and a client 
            // device.
            NetworkManager.singleton.StartHost();
            // Start broadcasting for other clients.
            StartAsServer();

#if !UNITY_EDITOR && UNITY_WSA
            // Invoke creating an anchor in a couple frames to give all the Unet network objects time to spawn.
            Invoke("InvokeCreateAnchor", 0.25f);
#else
            Debug.LogWarning("This script will need modification to work in the Unity Editor");
#endif

            SignalSessionListEvent();
            SignalConnectionStatusEvent();
        }
        
        /// <summary>
        /// The UNetAnchorManager won't be ready immediately after a scene is started so we defer calling 
        /// create anchor using unity's 'Invoke' on this function.
        /// </summary>
        void InvokeCreateAnchor()
        {
            UNetAnchorManager.Instance.CreateAnchor();
        }
```

###### 2. UNETAnchorManager.CreateAnchor
CreateAnchor内ではアンカー情報を送受信するために、GenericNetworkTransmitterの準備とAnchorの位置合わせのための座標を検索します。

```csharp:UNetAnchorManager.cs
        /// <summary>
        /// If we are supposed to create the anchor for export, this is the function to call.
        /// </summary>
        public void CreateAnchor()
        {
#if UNITY_WSA
            exportingAnchorBytes.Clear();
            GenericNetworkTransmitter.Instance.SetData(null);
            objectToAnchor = SharedCollection.Instance.gameObject;
            FindAnchorPosition();
#endif
        }
```

座標の検索はUNETAnchorManager.FindAnchorPositionメソッド内で行われます。このメソッドでは以下の順で座標を決定しています。

1.  アンカーがプレーヤーのprefs /アンカーストアに格納されている場合はその情報を使う
2.  Spatial Mappingがない場合、アンカーがセットされたオブジェクト（SharedCollectionコンポーネントを持つGameObject)の座標を使用
3.  Spatial Mappingがある場合、空間マッピングの頂点密集部分にアンカーを合わせる

上記の手順で座標を確定後SharedCollectionに対してWorldAnchorを追加しアンカーをエクスポートします。エクスポートに失敗した場合は再度アンカーの作成を試みます。エクスポートに成功した場合はGenericNetworkTransmitterにアンカーデータをセットしてサーバとしてデータ提供が可能な状態に設定します。

###### 3.UNETでのアンカーの同期（インポート）
HoloLensがクライアントとして参加した場合、Host側でアンカーのエクスポートが完了していると、UNETAnchorManager.Updateメソッド内でアンカーのインポートを実施します。インポート処理はHostのアンカー情報がエクスポートされていない限り実施されません。
getOneというフラグがfalseの間はHost側でアンカーのエクスポートが実施されていない状態を表しています。

```csharp:UNETAnchorManager.cs
        private void Update()
        {
#if UNITY_WSA
#if UNITY_2017_2_OR_NEWER
            if (HolographicSettings.IsDisplayOpaque)
            {
                return;
            }
#else
            if (!VRDevice.isPresent)
            {
                return;
            }
#endif

            if (gotOne)
            {
                Debug.Log("Importing");
                gotOne = false;
                ImportInProgress = true;
                WorldAnchorTransferBatch.ImportAsync(anchorData, ImportComplete);
            }

            if (oldAnchorName != AnchorName && !createdAnchor)
            {
                Debug.LogFormat("New anchor name {0} => {1}", oldAnchorName, AnchorName);
                oldAnchorName = AnchorName;
                if (string.IsNullOrEmpty(AnchorName))
                {
                    Debug.Log("Anchor is empty");
                    AnchorEstablished = false;
                }
                else if (!AttachToCachedAnchor(AnchorName))
                {
                    Debug.Log("Requesting download of anchor data");
                    WaitForAnchor();
                }
            }
#endif
        }
```

## 最後に
SharingWithUNETに関して気になる動作の仕組みとアンカーの部分について説明しました。SharingWithUNETを使うことでサーバを立てずにHoloLens同士やImmersiveその他のデバイスでも疎通が可能になります。
さらに、Mixed Reality 250のように同じアプリでImmersiveとHoloLensを切り替えてアプリケーションの幅を広げる事も可能です。
次回以降でSharinWithUNETのサンプルを少し見直し、ImmersiveとHoloLensで異なる動作を行いつつ同じアプリケーションで動かす方法について紹介したいと思います。