【Unity】IEnumeratorを使って非同期処理を行う

2021年10月8日

Unityでは、簡単にマルチスレッドのような非同期処理を実装することが出来ます。ここでは様々なサンプルを見て、自分の使う方法にマッチしたものを利用して下さい。

非同期処理を使う基本

非同期処理を実装するにはいくつかのお約束が存在します。その殆どの利用方法は、お約束に則っているのでまずはこちらの作法を覚えましょう。

サンプルから学ぶ

まずは使われ方を見てみましょう。知ってる人はこれ見て自分のプログラムに戻って下さい。

public class Example01 : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(DelayLog());
    }
    IEnumerator DelayLog()
    {
        Debug.Log("5秒後にログが表示されます");
        yield return new WaitForSeconds(5.0f);
        Debug.Log("Here!");
    }
}

サンプルではゲームを起動して、「5秒後にログが表示されます」というログが表示された5秒後に「Here!」というログが表示されるプログラムです。(Example01のコンポーネントがアクティブなGameObjectにアタッチされているとする)

プログラムとしてはDelayLogというメソッドを呼んでいるだけですが、ログのタイミングをずらして表示させることに成功します。同様の処理を通常のUpdateで再現することももちろん可能ですが、その場合は時間を計測するための処理や、Here!のログを表示した後に再度ログが表示されないように気を使う処理などの実装も必要になります。

では、これらを実現するために必要になる基本ポイントを抑えてみましょう

メソッドの型はIEnumeratorを使う

サンプルのプログラムだとDelayLogというメソッドの型がそれに当たります。IEnumeratorはジェネリック型としてIEnumerator<T>を利用することで好きな型を返す方法もありますが、Unityで使う場合はまず何も指定せずに、タイミングを調整する利用方法を覚えるのが先で良いと思います。また、EditorなどでIEnumeratorを利用する場合はStartCoroutineを利用する方法だとうまく使えませんので注意が必要です。

呼び出す場合はStartCoroutineを使う

IEnumeratorのメソッドを呼び出す場合は、必ずStartCoroutineを利用してメソッドを呼び出して下さい。おそらくIEnumeratorを利用する上で一番エラーになりやすいのはStartCoroutineを使っていない、という場合が多いと思います。

というのも、StartCoroutineを介さずにメソッドを呼び出した場合でも、なんとエラーになりません!なら大丈夫なのでは?と思うかもしれませんが、その場合はメソッドの中が呼ばれずに、何も起こらない状態になってしまいます。

メソッドの中の処理が完璧でも、呼び出し側の問題で、意図しないプログラムになってしまうので注意が必要です。特に他の箇所で呼び出しが正しくて、新規に追加したところでうっかりStartCoroutineを忘れてしまい、謎の不具合に見舞われるということがあります。利用時には必ずStartCoroutineを使うようにして下さい

最低1つはyield return をしておく

実装時にたまに起こるのが、yield returnを書き忘れてエラーになります。後で遅延させる処理を入れるけど、とりあえずメソッドだけ用意します!ということがたまにあると思いますが、そういった時にyield returnなしのメソッドになった場合、エラーが発生します。

とりあえずに困る人は、宣言したら何も言わずに一番最後の行にyield break;を書いておけばOK!

IEnumeratorを使う上で覚えておくと便利な知識編

IEnumeratorを使う上での基本的なことは分かったでしょうか?ここでは実際に利用する時に覚えておくと便利な要素などをまとめます。

yield return の戻り値について

yield returnはどのぐらい待たせるかを指定することが出来ます。用途に応じて使い分けましょう。

戻り値
yield returnは省く
効果
new WaitForSeconds(float _fTime)_fTime秒待たせる
null1フレーム待たせる
new WaitForSecondsRealtime(float _fTime)スケール化されていない時間を利用して、
指定した秒数待たせる
new WaitForEndOfFrame()Camera,GUIの表示が終わるまで待つ
Inputとか取れないこともあるらしいので
用途は限られる
yield break;yield return 系の代わり
IEnumeratorを中断する

StartCoroutineを待つ

非同期処理を使う場合、これを使えるかどうかでかなり実装できる内容に幅が広がります。ざっくり話すと何かの非同期処理が終わったら、別の処理を再開するということができるというものですが、実際にサンプルを見たほうが早いですね。

public class Example02 : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(CallParent());
    }
    IEnumerator CallParent()
    {
        Debug.Log("CallParentが呼ばれました");
        yield return StartCoroutine(CallChild());
        Debug.Log("CallParentが終了します");
    }
    IEnumerator CallChild()
    {
        Debug.Log("CallChildが呼ばれました");
        yield return new WaitForSeconds(1.0f);
        Debug.Log("CallChildが終了します");
    }
}

上記のプログラムでは、CallParentを呼び出し、その中でCallChildを呼び出しています。yield returnの戻り値に、StartCoroutineでIEnumeratorのメソッドを指定することで、呼び出したメソッドが完了するまで待つことが出来ます。

上記プログラムのログは

CallParentが呼ばれました
CallChildが呼ばれました
CallChildが終了します
CallChildが終了します

このような順番になります。複雑な組み合わせも可能になるので、うまく利用してキレイなプログラムを書けるように心がけましょう!

演習問題

非同期処理はIEnumeratorに関する処理を利用して、以下の演習を行ってみましょう。

演習その1

以下のクラスをシーン内のGameObjectにアタッチすると、「CallAが呼ばれました」というログが表示されます。CallA内を変更して、「CallAが呼ばれました」というログの3秒後に「CallAが終了しました」というログが表示されるようにして下さい。なお、Timeのスケール影響は受けるものとします。

public class Practice01 : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(CallA());
    }
    IEnumerator CallA()
    {
        Debug.Log("CallAが呼ばれました");
        yield break;
        Debug.Log("CallAが終了しました");
    }
}

演習その2

次のプログラムを利用して、以下の要件を満たしたプログラムを作成して下さい。ただしプログラムを修正できるのは解答欄のregion内のみとします。

要件

  • m_bIsClickedがtrueになった時、Goalメソッドを呼び出す
  • m_bIsClickedがtrueになるかどうかをチェックする間隔は、1フレーム間隔で行う

なお、ButtonClickedメソッドは外部からの操作で呼び出されるものとしています。

public class Practice02 : MonoBehaviour
{
    private bool m_bIsClicked;
    void Start()
    {
        StartCoroutine(Base());
    }
    public void ButtonClicked()
    {
        m_bIsClicked = true;
    }
    IEnumerator Base()
    {
        m_bIsClicked = false;
		    #region 回答欄
		    #endregion
        Goal();
        yield break;
    }
    void Goal()
    {
        Debug.Log("Goal");
    }
}

本当はボタンを押されたメソッド内で必要な処理を呼び出すのが良いのですが、今回はCoroutineを利用した非同期処理の待ち合わせを行う処理を実装する練習です。いくつか方法があるかもしれません。頑張って要件を満たしましょう

解答のリンクはこちら→準備中