その他
    ホーム 技術発信 DoRuby 【C#/Unity】コルーチン(Coroutine)とは何なのか
     

    【C#/Unity】コルーチン(Coroutine)とは何なのか

    この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

    コルーチンについて理解を深めるためにざっくりと調べてまとめてみました。

    はじめに

    Unity、と言うよりC#など幾つかの言語には「コルーチン」という仕組みが備わっているのですが、実は今まであまり触れたことがありませんでした。
    私のプロジェクトでは普段使う場面も無く、使わないまま苦手意識が芽生えてしまうのは非常にまずいので、調べてみてDoRubyの記事としてまとめることで理解を深めようと思います。

    本記事で扱うコルーチンは、Unityの仕様として提供されているコルーチンを指します。
    

    What is Coroutine

    そもそもコルーチンがどんな仕組みなのか分かっていなかったので、そこから整理してみます。
    Unityの公式マニュアルによると、

    コルーチンとは実行を停止して Unity へ制御を戻し、ただし続行するときは停止したところから次のフレームで実行を継続することができる関数です。

    とのこと。

    つまりコルーチンは、「フレームを跨いで処理を中断・再開させることが出来る仕組みを持った、関数の亜種みたいなもの」という認識で良さそうです。

    How to use Coroutine

    マニュアルによると、IEnumerator型を戻り値とした関数を定義することで、その関数をコルーチンとして扱うことが出来るようです。
    IEnumerator型はコレクションを扱うためのインターフェースなので、
    using System.Collections; しておく必要があります。
    定義したコルーチンを実行したい場合は、

    StartCoroutine("コルーチン名")
    
    StartCoroutine(IEnumerator型の変数)
    

    のように呼び出すことで実行できます。(MonoBehaviourを継承する必要あり)

    同様に、

    StopCoroutine("コルーチン名")
    
    StopCoroutine(Enumerator型の変数)
    

    でコルーチンを停止(≠中断)できます。

    使い方も分かったところで、適当にコルーチンを使うコードを書いてみました。

    using System.Collections;
    public class Test : MonoBehaviour{
        void Start(){
            IEnumerator coroutine = TestCoroutine();
            StartCoroutine(coroutine);
        }
        IEnumerator TestCoroutine(){
            Debug.Log("First.");
            yield return null;
            Debug.Log("Second.");
        }
    }
    

    StartCoroutineでコルーチン(TestCoroutine)が呼ばれると、コルーチンの中身が実行されてUnityのコンソールに「First.」と表示されます。
    次の行に yield return null; と書いてあるのが、コルーチンの中断処理です。
    これが呼ばれるとコルーチンは処理を中断するため、Unityは別の処理を行えるようになります。
    そして、次のフレームになるとコルーチンの処理が再開され、先ほど中断した行の次の行から実行されます。
    すなわち、「Second.」と表示されるわけです。
    このように、yield return を挟むことで処理を一時中断し、次のフレームまで待機することが出来ます。

    When to use Coroutine

    コルーチンをどんな時に使えばいいのか考えてみます。

    コルーチンを使って実現できるのは、「一連の処理を複数フレームに跨って実行させること」です。
    例えば、横スクロールアクションゲームでありがちな「穴から敵が垂直に飛び出てきて攻撃して戻っていく」処理を例とすると、

    ①何らかの条件を満たすことで、敵が飛び出すフラグがオンになる
    ②60フレームかけて敵を上に移動させる
    ③5フレームかけて敵が攻撃するアニメーションを行う
    ④60フレームかけて敵を下に移動させる
    ⑤フラグを消す

    といった5つのステップが考えられます。(フレーム数は適当です)
    UnityではMonoBehaviourクラスを継承することでオーバーライドしたUpdate関数が毎フレーム呼び出されるので、Update関数を使ってフレーム処理を行うのが良さそうです。

    ということでコルーチンを使わずに安直に実装したのがこちらです。

    int frameCount = 0;
    void Update(){
        if (moveFlag) {
            if (frameCount < 60) {
                // 上に移動させる処理
            } else if (frameCount < 65) {
                // 攻撃アニメーション処理
            } else if (frameCount < 125) {
                // 下に移動させる処理
            } else if (frameCount == 125) {
                moveFlag = false;
                frameCount = 0;
            }
            frameCount++;
        }
    }
    

    moveFlagがtrueの時だけUpdate関数の中でframeCountをインクリメントし、その値に応じて各処理を行います。
    しかし、この書き方ではif文やswitch文による条件分岐が多くなりがちで、フレーム数をカウントするためだけの変数が必要になります。(しかもどこかのタイミングで0に戻す必要がある)
    65や125といった数字も前の数字に依存するため、上に移動する処理を40フレームに変更しようとした場合、それに伴って65を45に、125を105に直す必要が出てきます。
    (変更しやすいように各ステップに必要なフレーム数を予め定数にしておいた場合でも、
    65の部分には60を表す定数と5を表す定数を加算する式が入ることになり、冗長になります)

    そもそもこのようにUpdate関数を使う場合、moveFlagがfalseの間も毎フレームmoveFlagの判定を行ってしまい、無駄が多くなってしまいます。

    そこで、同じ処理をコルーチンを使って書くことで、無駄を省いた上に分かりやすいコードを書くことが出来ます。

    IEnumerator MoveAndAttack(){
        for (int i = 0; i < 60; i++) { // 60フレームだけ実行
            // 上に移動させる処理
            yield return null;
        }
        for (int i = 0; i < 5; i++) { // 5フレームだけ実行
            // 攻撃アニメーション処理
            yield return null;
        }
        for (int i = 0; i < 60; i++) { // 60フレームだけ実行
            // 下に移動させる処理
            yield return null;
        }
    }
    

    yield return null; にたどり着くまでの処理 = 1フレームで行われる処理なので、for文で60回実行させれば60フレーム分の処理が行えます。
    このコルーチンを、前述のmoveFlagがtrueになるタイミングでStartCoroutineで呼び出してあげることによって、Update関数で無駄な判定を行うことなく必要な処理を必要な分だけ実行することが出来ます。

    さいごに

    初見ではややこしいと感じていたコルーチンでしたが、基本的な使い方が理解できれば便利さも実感できました。
    ちなみに、フレームごとに処理を実行するだけでなく、指定した秒数ごとに実行させることも可能なようで、
    その場合 yield return null; のところを yield return new WaitForSeconds(秒数); と変えることで実現できます。
    また、StopCoroutineでコルーチンを止めた場合でも、全く同じコルーチンを再度StartCoroutineすると、StopCoroutineで停止した状態の続きから実行されてしまうようです。

    悪い例

    IEnumerator coroutine = ExampleCoroutine();
    StartCoroutine(coroutine);
    
    // 何らかの処理が挟まる
    
    StopCoroutine(coroutine); // ExampleCoroutineが停止することを期待するが...
    StartCoroutine(coroutine); // 完全に停止せず、続きから実行されてしまう
    

     
    これはIEnumerator型の変数に、同じコルーチンを再度代入することで回避できます。
     

    良い例

    IEnumerator coroutine = ExampleCoroutine();
    StartCoroutine(coroutine);
    
    // 何らかの処理が挟まる
    
    StopCoroutine(coroutine); 
    coroutine = ExampleCoroutine();
    StartCoroutine(coroutine); // coroutineの指す対象が変わり、正常に最初から開始
    

     
    コルーチンは画面上のオブジェクトの動きをコントロールするのに非常に便利だと感じました。
    これから少しずつ使い慣れて行きたいと思います。
    機会があれば、今度は「カスタムコルーチン」についても記事を書いてみようと思います。

    モバイルバージョンを終了