【Unity】WaitUntilが正常に止まらないときに確認すること

# はじめに

ゲーム内で準備が整うまで処理を停止するときに使う **WaitUntil** が正常に停止してくれない！！！ :cry: となったので、備忘録を兼ねて書き残しておきます。

# 環境

- Windows 10
- Unity 2019.4.5f1(64-bit)

# WaitUntil とは

WaitUntil は、`yield return new WaitUntil(() => bool値);` のように書き、bool 値が true になるまで処理を中断します。

以下にサンプルとして、ゲームのスタート画面の例を示します。

```C#:WaitUntilSample.cs
using System.Collections;
using UnityEngine;

public class WaitUntilSample : MonoBehaviour
{
    void Start()
    {
        StartCoroutine("Sample");
    }

    IEnumerator Sample () {
        Debug.Log("Waiting...");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        Debug.Log("Let's play!!!");
        // 以下ゲーム開始の処理
    }
}
```

ユーザーがスペースキーを押すまで、
 `yield return new WaitUntil(() => Input.GetKeyDown("space"));`
の部分で処理が止まります。

ゲーム開始の処理はユーザーがスペースキーを押すまで行われず、ユーザーを待つことが出来ます。

このほかにも、ノベルゲームでのセリフ送りやロード画面など、ユーザーの入力を待機させたいときや準備が整うまで処理を停止する場合は `WaitUntil` を使うと比較的綺麗に実装することができます。


# 詰まったところ

以下のコードを書いたところ、コンソールにバグは出ないものの、偶数番目の `WaitUntil` は正常に作動しませんでした。

```C#:WaitUntilTest.cs
using System.Collections;
using UnityEngine;

public class WaitUntilTest : MonoBehaviour
{
    void Start()
    {
        StartCoroutine("Sample");
    }

    IEnumerator Sample () {
        Log("Waiting...");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        Log("first");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        Log("second");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        Log("third");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        Log("fourth");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        Log("fifth");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        Log("sixth");
    }
    void Log(string message)
    {
        Debug.Log(message + " (" + Time.time.ToString("n4") + " s)");
    }
}
```

![mv4x0-ctrrs.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610894/f0b30343-b23e-3980-3527-e7ed392473a1.gif)

スペースキーを 1 回押すと 2 つの出力を受けていることが分かります。

>
少々話はそれますが、さらに注意深く観察すると、同時にコンソールに出ているのにも関わらず時間が異なっていることも分かります。
>
`Debug.Log` の処理はほぼ 0 秒で行われるため、それ以外の処理に時間がかかっているということです。
>
実は、`WaitUntil` の判定には最低 1 フレームかかるという仕様があり、詳しくは先人の「[UnityのWaitUntilは使わない](https://qiita.com/kema/items/0c80483d828d101b0693)」の記述に譲りますが、この仕様が影響していると考えられます。


# 解決法

結論から言うと、`yield return null` を `yield return new WaitUntil(() => Input.GetKeyDown("space"))` の後ろに書くと、予想通りに動きます。

```C#:WaitUntilTest.cs
using System.Collections;
using UnityEngine;

public class WaitUntilTest : MonoBehaviour
{
    void Start()
    {
        StartCoroutine("Sample");
    }

    IEnumerator Sample () {
        Log("Waiting...");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        yield return null;
        Log("first");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        yield return null;
        Log("second");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        yield return null;
        Log("third");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        yield return null;
        Log("fourth");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        yield return null;
        Log("fifth");
        yield return new WaitUntil(() => Input.GetKeyDown("space"));
        yield return null;
        Log("sixth");
    }
    void Log(string message)
    {
        Debug.Log(message + " (" + Time.time.ToString("n4") + " s)");
    }
}
```
![qiita2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610894/815059be-83af-e051-6b18-b6cd399b8491.gif)

やりました！成功です！

# バグの原因究明

`Input.GetKeyDown` は該当のキーを押すと、1 フレームの間ずっと true となります。

コードは 1 フレームの中で実行されるので、`WaitUntil` の条件を満たすと、即座（フレームを跨がず）に次の `WaitUntil` まで到達します。

ここで、1 フレームの間 `Input.GetKeyDown` の値は常に true なので、2 つ目の `WaitUntil` に到達した時点でも true を返します。

その結果、2 つ目の `WaitUntil` はスペースキーを押さなくても突破されてしまうというわけでした。

ちなみに、`WaitUntil` は一度止まってから動くまで最低でも 1 フレームかかるので、3 つ目が一瞬で突破されることはありません。

![qiita (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610894/dbe2b743-8376-b74c-0d42-8e71a3165299.png)

したがって、処理を 1 フレーム分だけ中断させることができる`yield return null` を`WaitUntil` の直後に書くことで、このバグをスマートに解決することができたというわけです。

# 終わりに
バグと向き合ったときに既存の記事が無かったようなので自分で記事にしました。（もしあったらごめんなさい）

テンポを重視してところどころ説明を端折ってしまったため、内容は完全な初心者向けではなくなってしまったかもしれませんが、この記事で `WaitUntil` への理解が深まれば幸いです。