FirstVR　キャリブレーションをasync/awatiで書き直す

# はじめに

FirstVRのSDKはジェスチャーのキャリブレーションをコルーチンで実行する仕組みになっています。
これを書き換え、async/awaitで使えるようにします。

# 変更前のコード

次のコードは、FirstVRのサンプルコードの一部抜粋です。

```cs

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

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


/// <summary>
/// Calibrate the gesture with target or non-target values.
/// Calibration requires time, and it's best to let the user know what's going on, so this process is best done in a coroutine.
/// </summary>
IEnumerator Calibrate(bool target)
{
    if (target)
    {
        // Setting target values
        fvr.gestureManager.SetTargetData(gesture);
        tCalibRounds++;
    }
    else
    {
        // Setting non-target values
        fvr.gestureManager.SetNonTargetData(gesture);
        /// The first time we set a target or non-target value, the round length and samples per second are ignored and the SVM takes only one value with dummy data then
        /// the dummy data is replaced with real data. 
        /// After the first round the FVRGesture.calibrated flag is set to true and you are ready to start calibrating with real data
        if (gesture.calibrated)
        {
            ntCalibRounds++;
        }
        else
        {
            nonTargetBtn.GetComponentInChildren<Text>().text = "Set\nNonTarget";
            foreach (Button b in varBtns)
            {
                b.interactable = false;
            }
        }
    }

    // We dont wan't multiple coroutines taking the same data so it's good to block the user from starting a new one before this round is done
    targetBtn.interactable = false;
    nonTargetBtn.interactable = false;
    resetBtn.interactable = false;
    float t = 0;
    while (gesture.registering)
    {
        /// While the target or non-target data is being set, the FVRGesture.registering flag will be set to true.
        /// A count down or a image fill loading bar is a good way to let the user know your app is doing something.
        /// Once the porcess is done, the FVRGesture.registering flag will be set to false, and we will exit this while loop.
        t += Time.deltaTime;
        if (target)
            targetImg.fillAmount = t / (float) roundLength;
        else
            nonTargetImg.fillAmount = t / (float) roundLength;
        yield return null;
    }

    UpdateTexts();
    targetImg.fillAmount = 0;
    nonTargetImg.fillAmount = 0;
    // After the process is done you can enable whatever buttons you need to proceed with the calibration or move on with your app.
    targetBtn.interactable = true;
    nonTargetBtn.interactable = true;
    resetBtn.interactable = true;
}
```

コルーチンでベタッと書かれているのを書き換えてみます。

# 拡張メソッド

キャリブレーションする部分を拡張メソッドに切り出します。

```cs
using UniRx.Async;
using UnityEngine;

namespace FVRSamples
{
    public static class FVRGestureManagerExt
    {
        public static async UniTask CalibrateTargetGestureAsync(this FVRGestureManager manager, FVRGesture gesture,
            IProgress<float> progress = null, CancellationToken token = default)
        {
            manager.SetTargetData(gesture);

            var startTime = Time.time;
            var span = manager.calibrationRoundLength;

            while (gesture.registering)
            {
                await UniTask.Yield();
                progress?.Report((Time.time - startTime) / span);
            }
        }

        public static async UniTask CalibrateNonTargetGestureAsync(this FVRGestureManager manager, FVRGesture gesture,
            IProgress<float> progress = null, CancellationToken token = default)
        {
            manager.SetNonTargetData(gesture);

            var startTime = Time.time;
            var span = manager.calibrationRoundLength;
            while (gesture.registering)
            {
                await UniTask.Yield();
                progress?.Report((Time.time - startTime) / span);
            }
        }
    }
}
```

`UniTask.WaitWhile`を用いることで特定のフラグがfalseになるまでawaitすることがきます。
`IProgress<float>`を外から渡し、現在のキャリブレーションの進行状況を通知できるようにしています。


# 全体を書き換えたあとのコード

```csharp

public void SetTargetPress()
{
    _ = CalibrateAsync(true);
}

public void SetNonTargetPress()
{
    _ = CalibrateAsync(false);
}

private async UniTaskVoid CalibrateAsync(bool target)
{
    if (target)
    {
        tCalibRounds++;
    }
    else
    {
        if (gesture.calibrated)
        {
            ntCalibRounds++;
        }
        else
        {
            nonTargetBtn.GetComponentInChildren<Text>().text = "Set\nNonTarget";
            foreach (Button b in varBtns)
            {
                b.interactable = false;
            }
        }
    }

    targetBtn.interactable = false;
    nonTargetBtn.interactable = false;
    resetBtn.interactable = false;

    if (target)
    {
        await fvr.gestureManager.CalibrateTargetGestureAsync(gesture,
            Progress.Create<float>(f => targetImg.fillAmount = f));
    }
    else
    {
        await fvr.gestureManager.CalibrateNonTargetGestureAsync(gesture,
            Progress.Create<float>(f => nonTargetImg.fillAmount = f));
    }

    UpdateTexts();
    targetImg.fillAmount = 0;
    nonTargetImg.fillAmount = 0;
    targetBtn.interactable = true;
    nonTargetBtn.interactable = true;
    resetBtn.interactable = true;
}

```

コルーチンで書くよりかは制御しやすいかなと思います。たぶん。