Unityにはコルーチンという便利な機能があります。
(Unityの機能というか.Netの機能をUnityが使っているというか…)
コルーチンとは?
コルーチン自体はUnity特有のものではありません。
マイクロスレッドやファイバー等といった名前でいろんなところで利用されています。
例えば、LuaスクリプトやPythonなんかにも導入されています。
さっくりいうと
さっくりコルーチンをいうと、これは関数(メソッド)を任意の場所で中断/再開する機能です。
メリットは大きく2点あります。
- 複数の処理を疑似並列できる。
- 状態を関数内で保持できる。
疑似並列できる
本来関数は1つずつしか実行できません。
Function1(); Function2(); Function3();
とあれば1が終われば2、2が終われば3と順番に実行されます。
この123を同時に実行するにはマルチスレッドを利用する必要があります。
コルーチンでは完全に並列ではありませんが、Function1の途中まで→Function2の途中まで→Function3の途中まで→Function1の続きという風な流れで疑似的に並列処理を実現できます。
(※シーケンス図っぽい図ですが本来のシーケンス図ではないです。)
OSの疑似マルチスレッドと同様ですね。
状態を関数内で保持できる
状態遷移するような処理では
void HasStateFunction() { if ( state == 1 ) { Function1(); state = 2 } else if ( state == 2 ) { Function2(); state = 3 } else if ( state == 3 ) { Function3(); state = 4 } }
(OOPしてなかったりであまりいい処理ではないですが)このような形になります。
このstateはクラスのメンバ変数であったり、グローバル変数であったりに保持し続ける必要があります。
コルーチンではこれを
void HasStateFunction() { Function1(); yield; Function2(); yield; Function3(); }
のようなイメージで実装できます。(※これはイメージです)
stateをどこかに持つ必要がなくなり、処理の流れもわかりやすくなりました。
前者の疑似並列では通信処理やロード処理といった時間がかかるけど画面が止まっては困る場合なんかに便利です。
後者の状態保持ではアニメーション処理等のゲームシステムとは独立した状態を持った処理に便利です。
Unityでのコルーチンの実装
先ほどはイメージでしたが、実際にUnityでどのように利用するかを解説します。
Unityのコルーチンは基本的にMonoBehaivourを継承したクラスでしか使えません。
コルーチンの定義
コルーチンの定義はIEnumeratorを返り値に持ち、yeildステートメントが関数内に含まれる事が条件です。
IEnumerator CoroutineSample() { yield return null; }
返り値は必ずIEnumeratorでなければいけません。
(※UnityではジェネリクスのIEnumerator<T>はダメです。)
yield
yieldで関数の処理が一度中断されます。
Unityではyieldの後ろの内容によりどのタイミングで処理が再開されるかが決まります。
代表的なものは
- yield return new WaitForEndOfFrame();
- 次のフレームに再開します。
- yield return new WaitForSeconds(秒数);
- 指定秒数後に再開します。
- yield return new WaitUntil(再開条件);
- 再開条件に指定した関数がtrueを返すと再開します。
- yield return new WaitWhile(待機条件);
- 待機条件にした関数がfalseを返すと再開します。
- yield break;
- 関数は再開されずにそこで終わります。
- yield return StartCoroutine();
- 別のコルーチンを新たに実行しそれが終わるまで中断します。
- yield return 一部非同期オブジェクト;
です。
例1
IEnumerator Sample1() { Function1(); // Function1実行あと2.5秒後に関数が再開される yield return new WaitForSeconds(2.5f); Function2(); // Function2実行あと次のフレームに再開される yield return new WaitForEndOfFrame(); if ( state == 1 ) { // state変数が1ならFunction3を実行して関数は終わり、Function4は実行されない Function3(); yield break; } // state変数が1以外ならbreakしない為この処理が通る Function4(); // 4後にOtherCoroutine関数を呼び出し終わるまで中断 yield return StartCoroutine(OtherCoroutine()); // 上記OtherCoroutineが終わったらFunction5を実行 Fuction5(); }
例2
IEnumerator Sample2() { // 1秒かけてposition.xが約10増加する float time = 0; while ( time < 1 ) { yield return new WaitForEndOfFrame(); position.x += 10 * Time.deltaTime; time += Time.deltaTime; } }
一部非同期オブジェクト
一部非同期オブジェクトと書きましたが、これはWWWやAssetBundleのLoadAssetAsyncの
返り値であるAssetBundleRequest等です。
IEnumerator WWWSample() { WWW www = 初期化; yield return www; /// wwwの通信が終わるまで中断 }
IEnumerator AssetBundleSample() { AssetBundle assetbundle = 初期化; AssetBundleRequest request = assetbundle.LoadAssetAsync("オブジェクト名"); yield return request; // アセットバンドルの非同期読み込みが終わるまで中断 }
といった具合で、一部のUnityのオブジェクトはyeild returnする事でその処理が終わるまでコルーチンを中断することが出来ます。
コルーチンの実行
コルーチンの実行は2通りあります
タイプ1
// コルーチンに引数が1つある場合 StartCoroutine(コルーチン関数(引数)); // コルーチンに引数が3つある場合 StartCoroutine(コルーチン関数(引数1, 引数2, 引数3)); // 引数がない場合 StartCoroutine(コルーチン関数());
基本はこちらのタイプを利用します。
普通に関数を呼び出して、その返り値であるIEnumeratorをStartCoroutineに渡します。
最初のyieldまではこのStartCoroutineのタイミングで実行されます。
タイプ2
// コルーチンに引数が1つある場合 StartCoroutine("コルーチン関数名", 関数の引数); // 引数が無い場合 StartCoroutine("コルーチンの関数名");
文字列で関数名を指定するタイプです。こちらはStartCoroutineの第二引数にコルーチンに渡す引数を1つまで指定できます。
こちらも同じく最初のyieldまではこのStartCoroutineのタイミングで実行されます。
2つの違い
この2つの違いは
タイプ1 | タイプ2 | |
---|---|---|
引数の個数 | 任意の個数 | 0か1つ |
コルーチンの定義箇所 | 特に制限なし
(返り値のIEnumeratorを渡しさえすればよい) |
StartCoroutineを呼ぶMonoBehaivourのメンバ関数 |
後から個別停止
(StopCoroutine) |
コルーチン関数の返り値(IEnumerator)を指定 | 関数名を指定 |
くらいです。
タイプ2は個別の停止(StopCoroutine)時がお手軽という程度しかメリットがないので、タイプ1を利用で良いと思います。
タイプ1をStopCoroutineするためには
private IEnumerator coroutine_; void Start() { coroutine_ = Coroutine(); StartCoroutine( coroutine_ ); } void SomeMethod() { StopCoroutine( coroutine_ ); }
のようにどこかにコルーチンの返り値のIEnumeratorを保持する必要が出て来るため少し使いづらいです。
コルーチンの実行イメージ
Unityの処理の流れがゲームループです。大体1/60秒ごとに一度実行されます。
右側がコルーチンです。yieldを検知してUnityによって自動的に各フレームに処理が分散します。
注意点
スパゲティに注意
疑似とはいえ並列処理はコードが入り組みスパゲティになりがちです。
また、処理の順番もわかりにくくなりがちです。分かりやすいコードを意識しましょう。
GameObjectのActiveに注意
コルーチンはGameObjectがアクティブでない状態では動きません。
具体的には非アクティブになったタイミングでそのGameObjectの実行されているコルーチンがすべて停止します。
また、非アクティブ中のタイミングでのStartCoroutineの呼び出しはエラーが発生します。
この停止というのは一時中断ではなく停止です。この後再度アクティブになったとしてもコルーチンは再開されません。
完全並列ではない
あくまで疑似並列であり、完全並列ではありません。
その為例えば以下のような無限ループが発生するとアプリ全体が止まります。
IEnumerator BadSample1() { while ( true ) { // yieldしてないので無限ループ Function1(); } yield return new WaitForSeconds(1.0f); }
以下は(おそらく)無限ループではありませんが予期せぬ動作を起こします。
IEnumerator BadSample1() { float time = 0; while ( time < 1 ) { // yieldしてないので約1秒アプリ全体が止まる Function1(); time += Time.deltaTime; } yield return new WaitForSeconds(1.0f); }
.Netでのコルーチン
Unityでの話をしましたが、最後にちょっと本来のC#としての機能のコルーチンの書き方を紹介します。
と言っても古いやり方ですが。
using System; using System.Collections.Generic; /// <summary> /// コルーチンのサンプル /// </summary> public class Entry { /// <summary> /// コルーチン /// </summary> private static IEnumerator<string> OriginalCoroutine() { Console.WriteLine("最初"); yield return "1"; Console.WriteLine("2番目"); yield return "2"; Console.WriteLine("3番目"); yield return "3"; Console.WriteLine("最後"); } /// <summary> /// エントリポイント /// </summary> public static void Main() { IEnumerator<string> coroutine = OriginalCoroutine(); while (coroutine.MoveNext()) { string result = coroutine.Current; Console.WriteLine(result); } } }
最初
1
2番目
2
3番目
3
最後
このようになります。
通常のIEnumeratorと全く同じように使えます。MoveNextでコルーチンが次のyeildまで進みます。Currentにはyieldでreturnしている値が取得できます。
コメント
[…] Unityのコルーチン機能を使う […]