HoloLensとUNET使って玉を撃ち合ってみた

Unityのマルチプレイ機能を使ってHoloLensとUnity間で玉の撃ち合いをしてみました。
UNETを使えば簡単にシェアリングが出来ました。

#環境
* [Unity 2017.1.0f3](https://unity3d.com/unity/whats-new/unity-2017.1.0)
* [MixedRealityToolkit-Unity for Unity 2017.1.0f3](https://github.com/Microsoft/MixedRealityToolkit-Unity/releases/tag/v1.2017.1.0)

#サンプルコード
https://github.com/hirophilip/UNETTest

#実装イメージ
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">HoloLens使ってUNETで玉撃ち合いデモ Unityエディタ上からリアルタイムでキャラ移動や玉が撃てます <a href="https://twitter.com/hashtag/HoloLensJP?src=hash">#HoloLensJP</a> <a href="https://twitter.com/hashtag/HoloLens?src=hash">#HoloLens</a> <a href="https://twitter.com/hashtag/MR?src=hash">#MR</a> <a href="https://twitter.com/hashtag/Unity?src=hash">#Unity</a> <a href="https://t.co/dxxOp73akT">pic.twitter.com/dxxOp73akT</a></p>&mdash; がおまる@HoloLens研究者 (@gaomar) <a href="https://twitter.com/gaomar/status/912190310729392128">2017年9月25日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

#プロジェクト作成
UNETTestとしてプロジェクトを作成しました。
`HoloLensCamera` `InputManager` `CursorWithFeedback`をHierarchyViewに設置しています。
UNETの基本的な説明は割愛させて頂きます。

##MultiPlay設定
SERVICESタブにあるUnity ProjectIDを指定します。
ログインしているUnityIDを選択してCreateボタンをクリックします。
既にプロジェクトIDがある場合は右下の`I already have a Unity Project ID`をクリックして対象のプロジェクトを選択してください。

![s100.png](https://qiita-image-store.s3.amazonaws.com/0/154355/1b9c8d5d-8e39-c1e4-c1a6-61dc2bede8a9.png)

MultiPlayerをクリック
![s101.png](https://qiita-image-store.s3.amazonaws.com/0/154355/4c0011f9-c09c-ad97-733e-e4245480b717.png)

Proなら200人まで同時接続が可能
![s102.PNG](https://qiita-image-store.s3.amazonaws.com/0/154355/41b51b82-bad4-4736-c74f-233c50d1c495.png)

##PlayerのPrefabを作成する
自分のキャラを作成しましょう。適当にCapsule ColliderとBoxを設定して以下のようなキャラを作りました。

![s200.PNG](https://qiita-image-store.s3.amazonaws.com/0/154355/9691fc84-f481-b250-2651-c0bb438ede47.png)

###キャラクターの設定
キャラクターの設定は`NetworkIdentiy`のスクリプトをアタッチします。
`Local Player Authority`のチェックを入れてください。このチェックを入れることでクライアント側で動きが制御出来るようになります。

新規スクリプトを作成してください。
Playerの動きを制御するプログラムを書きます。

`[SyncVar]`とあるのが、クライアントの情報をホストに同期するものです。
`[Command]`とあるのが、クライアントで発火して、中身のプログラム自体はホストで実行されるものです。
メソッド名の頭に`Cmd`とするのが特徴です。

Updateの中にある`CmdTransform`メソッドで自キャラの位置を更新してホストに座標を更新してもらっています。
あくまでも、命令を実行するのは全てホストの仕事になります。

AirTapすると玉が発射されます。

```csharp:PlayerController.cs
using System;
using HoloToolkit.Unity.InputModule;
using UnityEngine;
using UnityEngine.Networking;

/// <summary>
/// Controls player behavior (local and remote).
/// </summary>
[NetworkSettings(sendInterval = 0.033f)]
public class PlayerController : NetworkBehaviour, IInputClickHandler
{
    public GameObject bullet;

    /// <summary>
    /// The transform of the shared world anchor.
    /// </summary>
    private Transform sharedWorldAnchorTransform;

    [SyncVar]
    private Vector3 localPosition;

    [SyncVar]
    private Quaternion localRotation;

    [Command]
    public void CmdTransform(Vector3 postion, Quaternion rotation)
    {
        if (!isLocalPlayer)
        {
            localPosition = postion;
            localRotation = rotation;
        }
    }

    public void OnInputClicked(InputClickedEventData eventData)
    {
        if (isLocalPlayer)
        {
            CmdFire();
        }
    }

    // Use this for initialization
    void Start () {
        if (isLocalPlayer)
        {
            // 全てのジェスチャーイベントをキャッチできるようにする
            InputManager.Instance.AddGlobalListener(gameObject);

        }
        else
        {
            // リモートプレイヤーは赤色にする
            GetComponentInChildren<MeshRenderer>().material.color = Color.red;
        }

        sharedWorldAnchorTransform = SharedCollection.Instance.gameObject.transform;
        transform.SetParent(sharedWorldAnchorTransform);
    }

    private void OnDestroy()
    {
        if (isLocalPlayer)
        {
            InputManager.Instance.RemoveGlobalListener(gameObject);
        }
    }

    // 自キャラの位置更新
    void Update () {
        if (!isLocalPlayer)
        {
            transform.localPosition = Vector3.Lerp(transform.localPosition, localPosition, 0.3f);
            transform.localRotation = localRotation;
            return;
        }

        // カメラの位置で更新する
        transform.position = Camera.main.transform.position;
        transform.rotation = Camera.main.transform.rotation;

        localPosition = transform.localPosition;
        localRotation = transform.localRotation;

        // ホストへ更新命令投げる
        CmdTransform(localPosition, localRotation);

    }

    // ローカルプレイヤーは青色
    public override void OnStartLocalPlayer()
    {
        GetComponentInChildren<MeshRenderer>().material.color = Color.blue;
    }

    public override void OnStartServer()
    {
        GetComponentInChildren<MeshRenderer>().material.color = Color.blue;
    }


    // ホスト側で実行される
    [Command]
    void CmdFire()
    {
        Vector3 bulletDir = transform.forward;
        Vector3 bulletPos = transform.position + bulletDir;

        // プレイヤーの位置から玉を発射する
        GameObject nextBullet = (GameObject)Instantiate(bullet, sharedWorldAnchorTransform.InverseTransformPoint(bulletPos), Quaternion.Euler(bulletDir));
        nextBullet.GetComponentInChildren<Rigidbody>().velocity = bulletDir * 1.0f;
        NetworkServer.Spawn(nextBullet);

        // 8秒後に消える
        Destroy(nextBullet, 8.0f);
    }

}

```

##玉のPrefabを作成する
発射するための玉のPrefabを作成しましょう
単純な丸いオブジェクトをつくってみました。軌跡が分かるようにTrailRendererもアタッチしています。

![s202.PNG](https://qiita-image-store.s3.amazonaws.com/0/154355/40e16662-e7c4-7fa8-48e9-163d896d0ae5.png)

![s203.PNG](https://qiita-image-store.s3.amazonaws.com/0/154355/6abbcb4a-8590-6f19-f6be-eee9433e61e1.png)

###玉の動きのスクリプト
では、実際に発射された時の動きのプログラムです。
SyncVarで玉の位置の同期を行っているので、位置がホスト側で更新されます。

```csharp:BulletController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;

public class BulletController : NetworkBehaviour
{
    //現在位置
    private Transform myTransform;

    //補間率
    private float lerpRate = 0.3f;

    [SyncVar] private Vector3 syncPos;
    [SyncVar] private float syncYRot;

    //1つ前のTransform情報
    private Vector3 lastPos;
    private Quaternion lastRot;

    // Use this for initialization
    void Start () {
        transform.SetParent(SharedCollection.Instance.transform, false);

        Rigidbody rb = GetComponentInChildren<Rigidbody>();
        rb.velocity = transform.parent.TransformDirection(rb.velocity);

        myTransform = transform;

    }
	
	// Update is called once per frame
	void Update () {
        TransmitMotion();
        LerpMotion();
    }

    void TransmitMotion()
    {
        if (!isServer)
        {
            return;
        }

        //Transform情報更新(キャッシュされたTransformから取得するほうが早い)
        lastPos = myTransform.position;
        lastRot = myTransform.rotation;


        //SyncVar変数を変更し、全クライアントと同期を図る
        syncPos = myTransform.position;
        //localEulerAngles: Quaternion→オイラー角(360度表記)
        syncYRot = myTransform.localEulerAngles.y;

    }

    //現在のTransform情報とSyncVar情報とを補間する
    void LerpMotion()
    {
        if (isServer)
        {
            return;
        }

        //位置情報の補間
        myTransform.position = Vector3.Lerp(myTransform.position, syncPos, Time.deltaTime * lerpRate);

        //Y軸のみ変える
        Vector3 newRot = new Vector3(0, syncYRot, 0);

        //角度の補間
        //Euler: オイラー角→Quaternion
        myTransform.rotation = Quaternion.Lerp(myTransform.rotation, Quaternion.Euler(newRot), Time.deltaTime * lerpRate);
    }
}

```

##マルチプレイ接続処理
マルチプレイに接続する部分のプログラムです。
空のGameObjectを作成して名前を`NetworkManager`としました。

`NetworkManager`のスクリプトをアタッチしてください。
その中にPlayer Prefabという部分があると思います。
ホストかクライアントの初期化に成功するとPlayer Prefabで設定したものが自動的に作成されます。
この例だと、PlayerのPrefabが自動的に生成され、キャラクターが出現します。

Spawnable Prefabsに先程作った玉のPrefabを指定します。
![s300.png](https://qiita-image-store.s3.amazonaws.com/0/154355/c3dae38f-f303-2380-9bc5-64ffa9d0971b.png)

##ネットワーク接続スクリプト
新規スクリプトを作成しましょう。名前は`NetworkManagerTest.cs`としました。
接続するためのスクリプトは以下の通りです。
接続するためのルーム名の指定と接続上限を設定しています。

```csharp:NetworkManagerTest.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.Networking.Match;

public class NetworkManagerTest : MonoBehaviour {

    // 接続部屋名
    public string matchName = "default";

    // 同時接続人数
    public uint matchSize = 4U;

    // Use this for initialization
    void Start () {
        // マルチプレイ初期化
        NetworkManager.singleton.matchName = matchName;
        NetworkManager.singleton.matchSize = matchSize;
        NetworkManager.singleton.StartMatchMaker();

        // マルチプレイ接続状況確認
        matchListCheck();

    }
	
	// Update is called once per frame
	void Update () {
    }

    // マルチプレイルーム作成
    private void createMatch()
    {
        NetworkManager.singleton.matchMaker.CreateMatch(NetworkManager.singleton.matchName, NetworkManager.singleton.matchSize, true, "", "", "", 0, 0, OnMatchCreate);
    }

    // マルチプレイ状態チェック
    private void matchListCheck()
    {
        NetworkManager.singleton.matchMaker.ListMatches(0, 10, "", true, 0, 0, OnMatchList);
    }

    public void OnMatchCreate(bool success, string extendedInfo, MatchInfo matchInfo)
    {
        // 部屋作れた？
        if (success)
        {
            // ホストとして起動
            NetworkManager.singleton.StopHost();
            NetworkManager.singleton.StartHost(matchInfo);
        }
    }

    public void OnMatchList(bool success, string extendedInfo, List<MatchInfoSnapshot> matches)
    {
        // ホストが存在しなければ部屋作ってホストになる
        if (matches == null)
        {
            createMatch();

        }
        else if (matches.Count == 0)
        {
            createMatch();

        }
        else
        {
            // すでにあればクライアントとして接続
            NetworkManager.singleton.matchMaker.JoinMatch(matches[0].networkId, "", "", "", 0, 0, OnMatchJoined);

        }

    }

    public void OnMatchJoined(bool success, string extendedInfo, MatchInfo matchInfo)
    {
        if (success)
        {
            // クライアントとして接続
            NetworkManager.singleton.StopClient();
            NetworkManager.singleton.StartClient(matchInfo);
        }

    }
}

```

##UWP出力
HoloLensアプリを作成する時と同じようにAppフォルダ作ってUWPファイルを出力します。
そこで一点注意なのですが、クライアントとサーバーのインターネット接続許可の設定をしておきます。

`Package.appxmanifest`ファイルを開いて
インターネット通信の許可をしておきましょう。

![s103.png](https://qiita-image-store.s3.amazonaws.com/0/154355/83342232-76f9-8866-6847-0ba50b4750c3.png)

###実行
HoloLensとUnityでアプリを実行しましょう。
クライアントのキャラは青色、それ以外は赤色のキャラに色分けされます。
Unity側でキャラを操作すると空中にキャラが出現するので、結構面白いです。

![s302.PNG](https://qiita-image-store.s3.amazonaws.com/0/154355/722a508e-41a6-51b0-8d5d-7feeca3bd6df.png)

玉を発射すると以下のようにリアルタイムでHoloLens側にも玉が発射されます。

![s303.PNG](https://qiita-image-store.s3.amazonaws.com/0/154355/1e0265c5-4515-65d1-b350-8679a79b9918.png)

#まとめ
比較的簡単にUNETでリアルタイムなマルチプレイが実装出来ました。
WebRTC等を使ってボイスチャットを実装出来ればさらに面白いことができそうですね。