Hololensでスーパーボールを投げまくってみる

#目標
Hololensが現実世界に重ねる系のツールならとりあえずここらへんは作りたいですよね。

 - スタンド
 ![世界.jpg](https://qiita-image-store.s3.amazonaws.com/0/157437/2bf51c3e-77f9-e76f-e56a-ee4c18135963.jpeg)

 - 黒魔法
 ![黒魔法.jpg](https://qiita-image-store.s3.amazonaws.com/0/157437/f48a71a2-8034-aadd-240b-22ccc3a78363.jpeg)

 - 言霊
 ![アハトアハト.jpg](https://qiita-image-store.s3.amazonaws.com/0/157437/1afbf797-05a1-b15b-36b6-a3eff58885d2.jpeg)

 - スカウター
 ![スカウター.jpg](https://qiita-image-store.s3.amazonaws.com/0/157437/0bc4aa2a-87b2-5789-f9cf-62749ce3c6bb.jpeg)

 - 妖精さん
 ![妖精さん.jpg](https://qiita-image-store.s3.amazonaws.com/0/157437/40615175-5360-a3d9-be59-3ee26ea55368.jpeg)

（召喚獣はスタンドと近いので割愛）

スカウターは顔認証がほしいので実機が手に入るまで保留。（ってか売ってるのか！）
妖精さんはすたんどあろーんなので別として、スタンドや黒魔法、言霊で使いそうな機能としては

 - なにもないところに「音声認識」で何か（スタンドとか魔法とか）を出現させる
 - ターゲットに照準を合わせて射出する
 - 物によっては複数

ということで、以上３つの機能を備えたプロトタイプを実装をしてみる。
プロトタイプのイメージは↓
![射撃.jpg](https://qiita-image-store.s3.amazonaws.com/0/157437/b1bb279f-e8f3-18d1-4f0d-d6a436b84741.jpeg)
さらに今回は余計なものを削り

 - 射出する弾はボール状
 - 自分の周りからランダムに射出ではなく、カメラの位置からターゲットに向かって射出

とする。また、弾が発射されても現実世界に影響をおよぼすわけではないためイマイチつまらなかったので

 - スーパーボールのように壁や床にあたって反射する

を追加する。
無駄に長々となってしまったのでそろそろ実装へ。

#プロジェクトの作成
ここを参考にプロジェクトを新規作成する。
https://developer.microsoft.com/ja-jp/windows/holographic/holograms_101e
プロジェクト名はSuperBallにしました。

#Hierarchy
Hierarchyはこんな感じ
![superball-hierarchy.PNG](https://qiita-image-store.s3.amazonaws.com/0/157437/d0e71d1b-3d41-edce-7130-a37089a1e3ad.png)
Main Camera
Cursor
Spatial Mapping
は101Eをそのまま流用。スクリプトもそのまま使います。
新規に追加するObjectはBullet、WorldManagerです。
まず、Bulletは弾です。プロジェクト名からするとSuperBallですが、これはプロトということでCreate > Sphereでデフォルトのものを使います。
弾性だけは1に変更しておきました。
WorldManagerはEmptyオブジェクトです。101Eではターゲットに照準が当たっているときにDrop音声認識する機能があるためOrigamiCollectionに付与してましたが、照準関係なくいつでも呼び出せるようにしたかったため別途作成しました。また、音声認識だけだと動作確認がちょっとめんどくさいためGazeGestureManagerもこれに付与してます。Aボタンを押したら照準関係なく弾を打ち出します。

#Bullet
![bullet-inspector.PNG](https://qiita-image-store.s3.amazonaws.com/0/157437/109431fe-6425-a0f6-d297-0fbb09538ecb.png)
SphereColliderのSuperBallMatは弾性を1にしただけのマテリアルです。

BulletCommandsは以下の通り

```C#:BulletCommands.cs
using UnityEngine;

public class BulletCommands : MonoBehaviour
{
    private MeshRenderer meshRenderer;

    private float speed = 1000.0f;

    // Use this for initialization
    void Start()
    {
        // If the sphere has no Rigidbody component, add one to enable physics.
        if (!this.GetComponent<Rigidbody>())
        {
            var rigidbody = this.gameObject.AddComponent<Rigidbody>();
            rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous;
        }

        // Grab the mesh renderer that's on the same object as this script.
        meshRenderer = this.gameObject.GetComponentInChildren<MeshRenderer>();
        meshRenderer.enabled = true;
    }

    // Called by GazeGestureManager when the user performs a Select gesture
    void OnShot()
    {
        GameObject cursor = GameObject.Find("Cursor");
        //this.transform.position = cursor.transform.position;

        this.transform.position = Camera.main.transform.position;

        GameObject bullet = GameObject.Find("Bullet");

        // 弾丸の複製
        GameObject bullets = GameObject.Instantiate(bullet) as GameObject;

        meshRenderer.enabled = false;

        Vector3 force;
        force = Camera.main.transform.forward * speed;
        // Rigidbodyに力を加えて発射
        bullets.GetComponent<Rigidbody>().AddForce(force);
    }
}
```
private float speed = 1000.0f;のところで射出スピードを調整します。見やすくするため少しゆっくりにしています。
onShotの処理は順番にCursorオブジェクトを見つけ、次にメインカメラのポジションに弾を移動させます。
で、弾を複製してカメラの前方に射出するだけ。
と、あと複製前の弾のメッシュも消すのを忘れずに。どうせカメラの位置にかぶせてるから見えないんだけど。

#WorldManager
![worldManager-inspector.PNG](https://qiita-image-store.s3.amazonaws.com/0/157437/f28105c5-16d1-291f-5f79-e7e6e5efa85a.png)
GazeGestureManagerとSpeechManagerを付与します。
中身は101Eとほぼ同じ。ターゲットに照準があっているかどうかの判定を取り除き、認識する言葉を変更しています。

```C#:SpeechManager.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Windows.Speech;

public class SpeechManager : MonoBehaviour
{
    KeywordRecognizer keywordRecognizer = null;
    Dictionary<string, System.Action> keywords = new Dictionary<string, System.Action>();

    // Use this for initialization
    void Start()
    {
        keywords.Add("Da", () =>
        {
            GameObject bullet = GameObject.Find("Bullet");
            bullet.SendMessageUpwards("OnShot");
        });

        keywords.Add("Shot", () =>
        {
            GameObject bullet = GameObject.Find("Bullet");
            bullet.SendMessageUpwards("OnShot");
        });


        // Tell the KeywordRecognizer about our keywords.
        keywordRecognizer = new KeywordRecognizer(keywords.Keys.ToArray());

        // Register a callback for the KeywordRecognizer and start recognizing!
        keywordRecognizer.OnPhraseRecognized += KeywordRecognizer_OnPhraseRecognized;
        keywordRecognizer.Start();
    }

    private void KeywordRecognizer_OnPhraseRecognized(PhraseRecognizedEventArgs args)
    {
        System.Action keywordAction;
        if (keywords.TryGetValue(args.text, out keywordAction))
        {
            keywordAction.Invoke();
        }
    }
}
```

```C#:GazeGestureManager.cs
using UnityEngine;
using UnityEngine.VR.WSA.Input;

public class GazeGestureManager : MonoBehaviour
{
    public static GazeGestureManager Instance { get; private set; }

    // Represents the hologram that is currently being gazed at.
    public GameObject FocusedObject { get; private set; }

    GestureRecognizer recognizer;

    // Use this for initialization
    void Start()
    {
        Instance = this;

        // Set up a GestureRecognizer to detect Select gestures.
        recognizer = new GestureRecognizer();
        recognizer.TappedEvent += (source, tapCount, ray) =>
        {
            GameObject bullet = GameObject.Find("Bullet");
            bullet.SendMessageUpwards("OnShot");
        };
        recognizer.StartCapturingGestures();
    }

    // Update is called once per frame
    void Update()
    {
        // Figure out which hologram is focused this frame.
        GameObject oldFocusObject = FocusedObject;

        // Do a raycast into the world based on the user's
        // head position and orientation.
        var headPosition = Camera.main.transform.position;
        var gazeDirection = Camera.main.transform.forward;

        RaycastHit hitInfo;
        if (Physics.Raycast(headPosition, gazeDirection, out hitInfo))
        {
            // If the raycast hit a hologram, use that as the focused object.
            FocusedObject = hitInfo.collider.gameObject;
        }
        else
        {
            // If the raycast did not hit a hologram, clear the focused object.
            FocusedObject = null;
        }

        // If the focused object changed this frame,
        // start detecting fresh gestures again.
        if (FocusedObject != oldFocusObject)
        {
            recognizer.CancelGestures();
            recognizer.StartCapturingGestures();
        }
    }
}
```

これで白いスーパーボール（大きめ）を連射することができるようになりました。SpetialMappingを無効にすれば弾んだりせず貫通していきますし、白いボールでなく炎のエフェクトをつけたボールにすればファイヤーボールを放てます。
スタンドの腕にして途中で止めるとかすれば無駄無駄だってできるはず。（はたから見るとアレですが）

画像認証使うともっと実用的なのがいっぱいできるんだろうけど。。。

こんなのとか。
![実用的MRな使い方-スタンド.jpg](https://qiita-image-store.s3.amazonaws.com/0/157437/46790d84-9565-c56e-7d8a-f21ea55d63b4.jpeg)