FirstVRでカスタムジェスチャーの登録をするサンプル

## 概要

FirstVRでカスタムジェスチャーを登録し、利用する準備までをやってみたいと思います。
なお、**FirstVRってなんやねん**って人は前の記事（[FirstVRでHello World](https://qiita.com/edo_m18/items/c101b198e03f02625082)）を参照ください。

カスタムジェスチャーとは、任意の手の形を登録し、その手の形になっているかどうかを判断してくれるようになるものです。
実際に試してみたところ、精度はだいぶいい感じでした。


## シーンのセットアップ

まず、FirstVRデバイスとのコネクションなどは専用のPrefabがあるのでそれを配置します。
また、現在のコネクション状況などステータスを表示してくれるPrefabも用意されているので、それを配置しておくと認識しているかどうかを表示してくれるので便利です。

`FVRContainer`と`ConnectionCheck`、ふたつのPrefabをシーンに配置します。

<img width="429" alt="スクリーンショット 2018-12-27 13.59.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/5616/1da5aae6-3bd8-a2f0-2d88-8e7971aee22b.png">


## カスタムジェスチャの登録を行う

### コードを書く前の準備

まず、FirstVRの機能を使いやすくするために`FVRlib`を`using`しておきます。

```cs
using FVRlib;
```

また、`FVRConnection`クラスを利用してジェスチャ登録などを行うため、`FVRContainer`Prefabの中に配置されている`FVRConnection`をインスペクタから登録しておきます。

<img width="133" alt="スクリーンショット 2018-12-27 14.03.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/5616/49c6d887-3606-8d85-9b89-ac44efbeb0ab.png">

これで準備が出来たのでコードを書いていきます。


### カスタムジェスチャ登録のフロー

まずはざっくりとフローを確認します。

1. `FVRGestureManager`の`RegisterCustomGesture`メソッドを通して新規のジェスチャを生成（登録）する
2. `FVRGestureManager`の`SetTargetData`メソッドを通して、対象のジェスチャのキャリブレーションを行う
3. `FVRGestureManager`の`SetNonTargetData`メソッドを通して、対象「**以外**」のジェスチャのキャリブレーションを行う
4. `FVRGesture`の`held`プロパティの状況で判断する

という流れになります。
もう少し具体的に説明しておくと、`FVRGesture`が把握するのは任意のジェスチャ（手の形）ですが、複数のジェスチャを登録する場合は「それ以外」のジェスチャを`SetNonTargetData`として登録する必要があります。

例えば「グー」「チョキ」「パー」の3つを認識させたい場合、

1. 「グー」ジェスチャの`SetTargetData`には「グー」を、`SetNonTargetData`には「チョキ」と「パー」を登録。
2. 「チョキ」ジェスチャの`SetTargetData`には「チョキ」を、`SetNonTargetData`には「グー」と「パー」を登録。
3. 「パー」ジェスチャの`SetTargetData`には「パー」を、`SetNonTargetData`には「グー」と「チョキ」を登録。

という具合です。

### コード解説

さて、フローが分かったところでコードを見てみましょう。
フローで示した順にコード断片を見ていきます。

#### ジェスチャの生成（登録）

```cs
[SerializeField]
private FVRConnection _fvr;

// ... 中略 ...

private void Start()
{
    _gesture = _fvr.gestureManager.RegisterCustomGesture("Gesture1");
}
```

`Start`メソッドの中で生成していますが、他の任意のタイミングでも大丈夫です。
`_fvr`はインスペクタから登録しておきます。

そして`RegisterCustomGesture`メソッドに、文字列で名称を設定して、マネージャクラスを通してジェスチャの生成と登録を行っています。

#### ジェスチャの設定（キャリブレーション）

これでジェスチャを登録する準備が整いました。
次に行うのは、このジェスチャオブジェクトが「どの形の手をジェスチャとして認識すべきか」の設定です。
またそれと同様にして「どの形の手がジェスチャ**ではないか**」も設定します。

```cs
private IEnumerator Calibrate(bool isTarget)
{
    if (isTarget)
    {
        _fvr.gestureManager.SetTargetData(_gesture);
    }
    else
    {
        _fvr.gestureManager.SetNonTargetData(_gesture);
    }

    float time = 0;

    // ここでプログレスバーの更新など
    while (_gesture.registering)
    {
        time += Time.deltaTime;

        Debug.Log(time.ToString());

        yield return null;
    }
}
```

キャリブレーション処理です。
コルーチンで動く想定で作っていますが、これは、ジェスチャの認識に若干の時間がかかるためキャリブレーション中の表示を行うための処置です。

例えば認識中のプログレスバーなどを実装する場合はここにその処理を入れるなどするといいでしょう。

また引数は（今回は）`bool`型となっていますが、これは登録するジェスチャが1種類だけを想定しているため、「対象か否か」の2択で済むためです。

もし2つ以上の複数ジェスチャに対応したい場合は、どのジェスチャに対して設定を行うか、も判断する必要が出てきます。
が、今回はシンプルな例にするためこのようにしています。

この`bool`の値は、例えばユーザに「ジェスチャ登録」ボタンなどを押してもらってキャリブレーションモードに入る際に設定して登録を行うような形になるでしょう。
以下のような感じです。

<img width="351" alt="スクリーンショット 2018-12-28 9.29.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/5616/d055ca56-d8a8-8fef-f79a-ab1ee113bc0a.png">

実際にコンテンツを作る際は、登録が終わるまでユーザに該当する手の形のままにしておいてもらう、などの表示が必要でしょう。

#### ジェスチャを認識して処理を分ける

以上でカスタムジェスチャの生成・登録と設定が終わりました。
あとはこれを使って実際のコンテンツを作っていくことになります。

ということで、最後は「ジェスチャの状態をどう判断するか」です。
といっても本当にシンプルで、以下のようなコードで判断することができます。

```cs
if (_gesture.held)
{
    // 登録したジェスチャ状態
}
else
{
    // それ以外の状態
}
```

Unityの`Input`クラスのようなシンプルな使い方ですね。
生成したジェスチャオブジェクトの`held`プロパティを参照することで、登録したジェスチャの状態にあるか否かを`bool`で返してくれます。
あとはこの状態をチェックして適切にコンテンツに落とし込んでやれば、設定した任意のジェスチャに応じて処理を分けていくことができます。

冒頭でも書きましたが、精度はだいぶ高いのでゲーム以外の用途にも全然使えるなーというのが個人的な感想です。
さらに言えば、スマホをコントローラにしてなにかの装置を操作する、みたいな使い方すらもありなんじゃないかなーと思っています。

## コード全文

さて、最後に今回作成したサンプルの全文を載せておこうと思います。


```cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

using FVRlib;

public class CustomGesture : MonoBehaviour
{
    [SerializeField]
    private FVRConnection _fvr;

    [SerializeField]
    private Text _logText;

    [SerializeField]
    private Text _recognizedText;

    private FVRGesture _gesture;

    private void Start()
    {
        _gesture = _fvr.gestureManager.RegisterCustomGesture("Gesture1");
    }

    private void Update()
    {
        if (_gesture.held)
        {
            _recognizedText.text = "Target!";
        }
        else
        {
            _recognizedText.text = "Non Target!";
        }
    }

    private void Log(string log)
    {
        _logText.text = log;
    }

    public void SetGesture()
    {
        StartCoroutine(Calibrate(true));
    }

    public void SetNonGesture()
    {
        StartCoroutine(Calibrate(false));
    }

    private IEnumerator Calibrate(bool isTarget)
    {
        if (isTarget)
        {
            _fvr.gestureManager.SetTargetData(_gesture);
        }
        else
        {
            _fvr.gestureManager.SetNonTargetData(_gesture);
        }

        float time = 0;

        while (_gesture.registering)
        {
            time += Time.deltaTime;

            Log(time.ToString());

            yield return null;
        }
    }
}
```