Unityな日々(Unity Geek)

Unityで可視化アプリを開発するための試行錯誤の覚書

コルーチンについての覚書

たまに使いたくなるが、いまいち理解できていないもののひとつが、コルーチン(Coroutine)。

ということで、いくつかメモ書きを。

コルーチン(Coroutine)とは何か?

Coroutineとは何だろうか? Wikipediaには次のようにある。

「コルーチン(英: co-routine)とはプログラミングの構造の一種。サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できる。接頭辞 co は協調を意味するが、複数のコルーチンが中断・継続により協調動作を行うことによる。 サブルーチンと異なり、状態管理を意識せずに行えるため、協調的処理、イテレータ、無限リスト、パイプなど、継続状況を持つプログラムが容易に記述できる。」

簡潔に言うと、コルーチンとは、プログラムを任意の箇所で中断・再開するしくみ、と言っていいだろう。

Coroutineはどう記述するか?

Coroutineとして中断・再開するメソッドは、

  • IEnumerator型を戻り値とし
  • 中断・再開する場所に、yield return 「真偽判定結果」を置く

が基本形。

このメソッドを、

  • StartCoroutine(メソッドを代入したIEnumerator型メソッド、または、それを代入した変数) または、
  • StartCoroutine("メソッド名の文字列")

として呼び出す。

前者の記述方法は、メソッドに引数がある場合・ない場合の両方に使えるが、後者は引数がない場合にしか使えない。

Coroutineはどんな時に使うか?

1.一定時間処理を止めたいとき

Unityでも他のアプリケーションでも、ループの中で、ある処理を一定時間止めたい、というのはよくある要請。

この場合は、ループ(for, while)の中の停止したい場所にyield returnを置き、真偽判定条件として、WaitForSecondsクラスを設定する。

次の例では、0.2秒ごとにアルファ値が0.1小さくなる。

IEnumerator Fade() {
    for (float f = 1f; f >= 0; f -= 0.1f) {
        Color c = renderer.material.color;
        c.a = f;
        renderer.material.color = c;
        yield return new WaitForSeconds(0.2f);  //毎ループここで0.2秒停止する
    }
}

void Update() {
    if (Input.GetKeyDown("f")) {
        StartCoroutine("Fade");
    }
}

2.呼び出されるごとに、処理を変えたい時

yield returnを複数個記述すれば、呼び出されるごとに前のyield returnの次から処理が続けられ、次にあたったyield returnで処理が停止する。これによって戻り値を順番に変えることができる。

IEnumerator coRoutine(){
    msgText.text = "Andy ";
    yield return null;
    msgText.text = "Bob";
    yield return null;
    msgText.text = "Carter ";
    yield return null;
    msgText.text = "Dave";
}

3.あるメソッドの(for)ループを、Updateループに同期して進めたいとき

UnityでのCoroutineの特徴的な使い方としては、Updateループ外のメソッドのループをUpdateフレームに対応させて進ませる、というのがある。次は、Unityマニュアルにある具体例

IEnumerator Fade() {
    for (float f = 1f; f >= 0; f -= 0.1f) {
        Color c = renderer.material.color;
        c.a = f;
        renderer.material.color = c;
        yield return null;
    }
}

void Update() {
    if (Input.GetKeyDown("f")) {
        StartCoroutine("Fade");
    }
}

yield return nullは、ここで処理を中断し、次のフレームで再開するための記述法。これにより、Updateループが1回進むごとに、Fade()内のforループも1回進むことになる。

この例では、yield returnの真偽判定に'null'が設定されている。これは、単にyield returnと書いてもいい。

「ここで制御を呼び出し側に戻すけど、次に呼び出されたら、無条件にこの場所から再開するよ」と解釈される。

コルーチンから戻り値を受け取る

コルーチンから戻り値を受け取りたい時はどうするか?これは次を参考に!

qiita.com

カスタムコルーチン(Custom Coroutine)

Unity5.3から「カスタムコルーチン」が使えるようになり、yield returnの処理中断・再開条件を、自分でカスタマイズできるようになった。(関連Unityブログ)。

次はカスタムコルーチンのサンプル。指定したwaitTime秒待つメソッド(WaitForSecondsと同じ機能)の改良版。(注:ソースコードに誤りがあり訂正しました。また、WaitForSecondsRealtimeはすでにUnity本体に実装されたようです。2017/4/13)

using UnityEngine;
using System.Collections;

public class WaitForSecondsRealtime: CustomYieldInstruction {

    private float waitTime;

    public override bool keepWaiting
    {
        get { return Time.realtimeSinceStartup < waitTime; }
    }

    public void WaitForSecondsRealtime(float time)
    {
        waitTime = Time.realtimeSinceStartup + time;
    }
}

カスタムコルーチン・クラスのポイントは、

  • CustomYieldInstructionクラスを継承する
  • public bool keepWaitingをoverrideし、中断・再開の条件を記述する

の2点。

using UnityEngine;
using System.Collections;

public class CoTest : MonoBehaviour {

    private IEnumerator coroutine;

    // Use this for initialization
    IEnumerator Sub(float delay = 1.0f)
    {
        Debug.Log("Sub Before "+Time.realtimeSinceStartup);
        yield return new WaitForSecondsRealtime(delay);
        Debug.Log("Sub After" + Time.realtimeSinceStartup);
    }

    // Update is called once per frame
    private void Start()
    {
        Debug.Log("Start Before "+Time.realtimeSinceStartup);
        coroutine = Sub(3.0f);
        StartCoroutine(coroutine);
        Debug.Log("Start After " + Time.realtimeSinceStartup);
    }
}

Start()の中のStartCoroutine(coroutine);でコルーチンを呼び出している。(変数coroutineは、そのひとつ前の行でSub(3.0fとして定義している)

これを実行した結果(Console出力):"Sub Before"~"Sub After"の間が約3秒空いていることがわかる。

f:id:yasuda0404:20170412175441p:plain

なお、コルーチン呼び出し側の処理は止まらないことに注意。すなわち、呼び出し側では、"Start Before"から"Start After"は、即座に実行される。

コルーチンの実体

コルーチンは、コレクション(Collections)と深くつながっている。

Collectionsの要素をひとつずつ取り出して処理する場合、foreach(){...}を使う。このforeachループで次々に値を取得できるようにしているのが、IENumerableインターフェースである。

IEnumerableインファーフェースを実装したクラスに、IEnumeratorを戻り型とするGetIEnumeratorメソッドを実装することで、列挙を可能にしている(ややこしい!)。

IEnumerableインターフェースには、Collectionsの現在の要素を取得したり、次の要素に進めたり(MoveNext)、最初の要素に戻ったり(Reset)する機能が定義されている。

そして、yieldは、Collectionsの次の要素をforeachで処理できるようにするためのものである。

つまり、IEnumerable/IEnumerator/yieldという組み合わせは、もともとSystem.Collectionsに列挙可能性を付加する機能で、それをコルーチンの遅延実行に利用している、と考えてもいいかな。あるいは、その逆に、遅延実行機能を、Collectionsの列挙可能性に利用した、でもいいのかも。

unity3d.com