処理の分割(コルーチン)
時間のかかる処理を待機する
フレーム駆動であるゲームでは、1秒間に 30 ~ 60 回もの更新・描画を行います。従って、1画面(フレーム)を生成するまで、全てのスクリプトの処理は数十ミリ秒の時間で処理を終えなければなりません。処理が遅れれば画面の更新が遅くなり、滑らかなアニメーションが損なわれカクカクとした動きになってしまいます。ユーザーにとってはストレスとなるでしょう。
このような場合、複数の時間のかかる処理を分割したり、処理が終了するのを何らかの方法で待機する必要があります。時間のかかる処理には AI などの複雑な計算だけではなく、ファイルの入出力やネットワークの応答待機なども含まれます。このような時間のかかる処理のために Update() メソッド内で流れを止めてしまえば、ゲームそのものが止まってしまいます。
Unity では、長時間の待機処理や計算コストの高い処理を複数のフレームに分割して行います。しかし、フレームごとに現在の状態を保持し、進捗管理や非同期処理を記述するのは非常に面倒です。そこで C# が持つ基本的な文法とコレクションの機能を応用したコルーチンによって、手続き的なコードでフレーム単位に処理を分割できます。
コルーチンとは、関数の途中で処理を中断し、再び関数を呼び出したときに中断した場所から再開を実行できる仕組みのことで、文法的にコルーチンを持つ言語は多くはありません。通常は、進捗情報を保持し、関数を呼び出す度に進捗情報から次に実行する処理を再現する必要がありますが、C# では、これを反復子を用いて実現します。
反復子は「次の要素」を持つ IEnumerator インターフェイスで表されます。これは、単純な反復子であり、配列などのコレクションを抽象化したオブジェクトに過ぎませんが、C# にはメソッドの戻り値を反復子の要素とする yield return 文があり、これを用いることでメソッドをコルーチン化できます。
IEnumerator Coroutine() { Debug.Log("One"); yield return null; Debug.Log("Two"); yield return null; Debug.Log("Three"); yield return null; }
上記の Coroutine() メソッドは、3 つの null の要素を持つ反復子を返します。最終的に Coroutine() メソッドが返す結果は new Object[] { null, null, null } に等しいものですが、この結果は反復子によって「次の要素」が要求されたときに遅延実行されます。
var e = Coroutine(); e.MoveNext(); //One e.MoveNext(); //Two e.MoveNext(); //Three
上記のように Coroutine() メソッドが返した反復子の MoveNext() メソッドを呼び出すごとに、次の yield return 文までのコードが実行されます。メソッドは yield return 文で制御を返し、次の MoveNext() メソッドで、前回の yield return 文の続きから処理を実行します。
Unity では、この仕組みを応用して、フレームごとに反復子を呼び出す StartCoroutine() メソッドを提供しています。
public Coroutine StartCoroutine(IEnumerator routine);
このメソッドの routine パラメータに反復子を渡すと、フレーム単位で MoveNext() メソッドが呼び出され、フレーム間で連続する処理を分離できます。反復子はフレームごとに分解されて呼び出されるため、StartCoroutine() メソッドは即座に制御を返します。すなわち、反復子の処理を待機しません。
メソッドの結果は UnityEngine.Coroutine クラスのオブジェクトです。このオブジェクトは、コルーチンの内部で別のルーチンを待機させるために必要なオブジェクトです。
public sealed class Coroutine : YieldInstruction
ここで重要なのは Coroutine クラスの基底クラスとなっている YieldInstruction クラスです。StartCoroutine() メソッドに渡す反復子が YieldInstruction 型の要素を返す場合、Unity は対象のコルーチンが終了するのを待ってから次の要素を呼び出します。従って、コルーチンの中で何かを待機するといった場合に必要になります。
using System.Collections; using UnityEngine; public class Sample : MonoBehaviour { void Start() { StartCoroutine(Coroutine()); } IEnumerator Coroutine() { var startTime = Time.time; Debug.Log("Begin Coroutine"); while (Time.time - startTime < 5) { transform.Rotate(0, 1 ,0); yield return null; } Debug.Log("End Coroutine"); } }
コード1は Start() メソッドが実行されたときに Coroutine() メソッドが返す反復子を StartCoroutine() メソッドに渡しています。よって、Coroutine() メソッドが、フレームごとにコルーチンとして呼び出されます。
Coroutine() メソッド内では、単に while 文で 5 秒経過するまで自分自身を回転させるだけの処理を行っていますが、本来であれば時間のかかる待機処理や複雑な計算の分割されたタスクを、このコルーチンで実行することになるでしょう。
コルーチンはフレームごとに呼び出されるので Update() メソッドと同じようだと感じるかもしれませんが、重要なのはメソッドの途中で結果を返し、次の呼び出しで前回の続きからメソッドを再開できるという点です。Update() メソッドでコード1と同様の処理を実行するには、開始時間(startTime 変数)をフィールドなどの状態として保持しなければなりませんが、コルーチンは状態が保持されるためローカル変数だけで実現できます。