はじめに
ゲーム内で準備が整うまで処理を停止するときに使う WaitUntil が正常に停止してくれない!!! となったので、備忘録を兼ねて書き残しておきます。
環境
- Windows 10
- Unity 2019.4.5f1(64-bit)
WaitUntil とは
WaitUntil は、yield return new WaitUntil(() => bool値);
のように書き、bool 値が true になるまで処理を中断します。
以下にサンプルとして、ゲームのスタート画面の例を示します。
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
は正常に作動しませんでした。
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)");
}
}
スペースキーを 1 回押すと 2 つの出力を受けていることが分かります。
少々話はそれますが、さらに注意深く観察すると、同時にコンソールに出ているのにも関わらず時間が異なっていることも分かります。
Debug.Log
の処理はほぼ 0 秒で行われるため、それ以外の処理に時間がかかっているということです。
実は、WaitUntil
の判定には最低 1 フレームかかるという仕様があり、詳しくは先人の「UnityのWaitUntilは使わない」の記述に譲りますが、この仕様が影響していると考えられます。
解決法
結論から言うと、yield return null
を yield return new WaitUntil(() => Input.GetKeyDown("space"))
の後ろに書くと、予想通りに動きます。
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)");
}
}
やりました!成功です!
バグの原因究明
Input.GetKeyDown
は該当のキーを押すと、1 フレームの間ずっと true となります。
コードは 1 フレームの中で実行されるので、WaitUntil
の条件を満たすと、即座(フレームを跨がず)に次の WaitUntil
まで到達します。
ここで、1 フレームの間 Input.GetKeyDown
の値は常に true なので、2 つ目の WaitUntil
に到達した時点でも true を返します。
その結果、2 つ目の WaitUntil
はスペースキーを押さなくても突破されてしまうというわけでした。
ちなみに、WaitUntil
は一度止まってから動くまで最低でも 1 フレームかかるので、3 つ目が一瞬で突破されることはありません。
したがって、処理を 1 フレーム分だけ中断させることができるyield return null
をWaitUntil
の直後に書くことで、このバグをスマートに解決することができたというわけです。
終わりに
バグと向き合ったときに既存の記事が無かったようなので自分で記事にしました。(もしあったらごめんなさい)
テンポを重視してところどころ説明を端折ってしまったため、内容は完全な初心者向けではなくなってしまったかもしれませんが、この記事で WaitUntil
への理解が深まれば幸いです。