コルーチンとは?

Unityでゲームを作っていると、「3秒待ってから次の処理を実行したい」「フェードアウトを少しずつ進めたい」といった、時間に関わる処理を書きたくなる場面が出てきます。

こうした処理をUpdateだけで書こうとすると、経過時間を測るための変数を用意し、毎フレーム加算して、しきい値を超えたかどうかを判定して……と、コードがあっという間に膨らんでいきます。

using UnityEngine;

public class WaitWithUpdate : MonoBehaviour
{
    private float _elapsedTime;
    private bool _isWaiting = true;

    private void Update()
    {
        if (_isWaiting)
        {
            // 経過時間を加算する
            _elapsedTime += Time.deltaTime;

            // 3秒経過したかチェックする
            if (_elapsedTime >= 3f)
            {
                Debug.Log("3秒経過しました");
                _isWaiting = false;
            }
        }
    }
}

たった「3秒待ちたい」だけなのに、状態を管理するフラグや経過時間の変数が必要になってしまいます。

こんなときに便利なのがコルーチン(Coroutine)です。

コルーチンを使うと、処理の途中で一時停止して、後で再開するような書き方ができます。

Updateに状態管理を持ち込まずに、自然な流れで「待つ」処理を書けるのが利点です。

先ほどの3秒待つ処理を、コルーチンで書き直してみると以下のようになります。

using System.Collections;
using UnityEngine;

public class WaitWithCoroutine : MonoBehaviour
{
    private void Start()
    {
        // コルーチンを開始する
        StartCoroutine(WaitAndLog());
    }

    private IEnumerator WaitAndLog()
    {
        // 3秒待つ
        yield return new WaitForSeconds(3f);

        Debug.Log("3秒経過しました");
    }
}

フラグも経過時間の変数もなくなり、コードを上から下へ追うだけで処理の流れが読み取れます。

コルーチンの基本的な使い方

コルーチンはIEnumeratorを戻り値に持つメソッドとして定義します。メソッドの中にyield returnを書くと、その時点で処理が一時中断し、指定した条件を満たしたタイミングで再開する、という仕組みです。

IEnumeratorとyield return

コルーチンの最小構成は、以下のようにIEnumeratorを返すメソッドを書くだけです。

using System.Collections;
using UnityEngine;

public class CoroutineSample : MonoBehaviour
{
    private void Start()
    {
        // コルーチンを開始する
        StartCoroutine(SimpleCoroutine());
    }

    private IEnumerator SimpleCoroutine()
    {
        Debug.Log("コルーチン開始");

        // 1フレーム待つ
        yield return null;

        Debug.Log("1フレーム後");

        // 2秒待つ
        yield return new WaitForSeconds(2f);

        Debug.Log("さらに2秒後");
    }
}

yield return nullは「次のフレームまで待つ」という意味です。

yield return new WaitForSeconds(2f)は「2秒間待ってから再開する」となります。

処理がyield returnの行に到達するとそこで一旦停止し、条件が満たされたタイミングで続きから処理が再開されます。

StartCoroutineで開始する

IEnumeratorを返すメソッドを書いただけでは、コルーチンは実行されません。

MonoBehaviourのStartCoroutineメソッドを呼んではじめて、コルーチンとして動き始めます。

// メソッドを直接渡すパターン
StartCoroutine(SimpleCoroutine());

// メソッド名を文字列で渡すパターン(非推奨)
StartCoroutine("SimpleCoroutine");

文字列指定の方法もありますが、タイプミスがコンパイルエラーにならず実行時エラーになりやすいため、基本的にはメソッドを直接渡す書き方を使いましょう。

yield returnのバリエーション

yield returnにはさまざまな指定方法があり、用途によって使い分けます。代表的なものをいくつか紹介しますね。

yield return null

もっとも基本的な指定で、「次のフレームのUpdateタイミングまで待つ」という意味です。

private IEnumerator MoveOverFrames()
{
    for (int i = 0; i < 60; i++)
    {
        // 少しずつ右に移動する
        transform.position += Vector3.right * 0.1f;

        // 次のフレームまで待つ
        yield return null;
    }
}

毎フレーム少しずつ処理を進めたいときに使います。

WaitForSeconds

指定した秒数だけ待ちます。Time.timeScaleの影響を受けるので、ポーズ中などは時間が止まる点に注意してください。

private IEnumerator DelayedAction()
{
    Debug.Log("開始");

    // 1.5秒待つ
    yield return new WaitForSeconds(1.5f);

    Debug.Log("1.5秒経過");
}

Time.timeScaleの影響を受けたくない場合は、代わりにWaitForSecondsRealtimeを使います。

WaitForFixedUpdate

次のFixedUpdateが呼ばれるタイミングまで待ちます。物理演算のタイミングに合わせて処理したい場合に使います。

private IEnumerator ApplyForceOnFixedUpdate()
{
    // 次のFixedUpdateを待ってから力を加える
    yield return new WaitForFixedUpdate();

    GetComponent<Rigidbody>().AddForce(Vector3.up * 10f, ForceMode.Impulse);
}

WaitForEndOfFrame

フレーム内のすべての描画処理が完了したタイミングまで待ちます。スクリーンショットの取得など、レンダリング後に処理したい場面で使います。

private IEnumerator CaptureScreen()
{
    // 描画完了を待つ
    yield return new WaitForEndOfFrame();

    // この時点で画面の描画が終わっているのでキャプチャできる
    var texture = ScreenCapture.CaptureScreenshotAsTexture();
    Debug.Log($"撮影サイズ:{texture.width}x{texture.height}");
}

なお、URPやHDRPなどのScriptable Render Pipelineでは、レンダリングの実行タイミングがビルトインパイプラインと異なる場合があります。

スクリーンショット取得などレンダリング結果に依存する処理を書く際は、使っているパイプラインで期待通りのタイミングになるか確認しておきましょう。

WaitUntilとWaitWhile

特定の条件が満たされるまで待ちたいときに使います。

  • WaitUntil:指定した条件がtrueになるまで待つ
  • WaitWhile:指定した条件がtrueの間ずっと待つ
private bool _isReady;

private IEnumerator WaitForReady()
{
    Debug.Log("準備待ち");

    // _isReadyがtrueになるまで待つ
    yield return new WaitUntil(() => _isReady);

    Debug.Log("準備完了");
}

private IEnumerator WaitWhileLoading()
{
    // ロード中の間ずっと待つ
    yield return new WaitWhile(() => IsLoading());

    Debug.Log("ロード完了");
}

private bool IsLoading()
{
    // 実際はロード中かどうかを返す処理を書く
    return false;
}

条件は毎フレーム評価されるので、Update相当のチェック処理が走るイメージです。

別のコルーチンを待つ

yield returnに別のコルーチンを渡すと、そのコルーチンが終わるまで待ってから次に進みます。処理を細かく分けて組み合わせたい場合に便利です。

// using System.Collections; using UnityEngine; などのusingとMonoBehaviourを継承したクラス定義は省略

private IEnumerator MainSequence()
{
    Debug.Log("ステップ1開始");

    // 別のコルーチンを実行して、完了を待つ
    yield return StartCoroutine(SubRoutine());

    Debug.Log("ステップ1完了、ステップ2へ");

    yield return new WaitForSeconds(1f);

    Debug.Log("ステップ2完了");
}

private IEnumerator SubRoutine()
{
    yield return new WaitForSeconds(2f);
    Debug.Log("サブルーチン完了");
}

フェードイン → 演出 → フェードアウトのように、複数の段階を順に実行したいときに役立ちます。

コルーチンの停止と再開

開始したコルーチンを途中で止めたいケースもよくあります。その場合は、コルーチンを開始するときに返ってくるCoroutine型の参照を保持しておきましょう。

StopCoroutineで停止する

StartCoroutineはCoroutine型のオブジェクトを返します。これを変数に保存しておくと、StopCoroutineに渡して個別に停止できます。

using System.Collections;
using UnityEngine;

public class StopCoroutineSample : MonoBehaviour
{
    private Coroutine _runningCoroutine;

    private void Start()
    {
        // コルーチンを開始し、参照を保持する
        _runningCoroutine = StartCoroutine(LoopRoutine());
    }

    private void Update()
    {
        // スペースキーで停止する
        if (Input.GetKeyDown(KeyCode.Space) && _runningCoroutine != null)
        {
            StopCoroutine(_runningCoroutine);
            _runningCoroutine = null;
            Debug.Log("コルーチンを停止しました");
        }
    }

    private IEnumerator LoopRoutine()
    {
        while (true)
        {
            Debug.Log("ループ実行中");
            yield return new WaitForSeconds(1f);
        }
    }
}

参照を保持せず、メソッドを直接StopCoroutineに渡す書き方もありますが、同名のコルーチンを複数起動していると、意図せず別のものを止めてしまうことがあります。

安全のため、Coroutine型の参照を保持して停止する方法を基本にしておきましょう。

StopAllCoroutinesで一括停止

そのMonoBehaviourが起動しているコルーチンをすべて止めたいときは、StopAllCoroutinesを使います。

private void OnDisable()
{
    // このMonoBehaviourが起動しているコルーチンをすべて停止する
    StopAllCoroutines();
}

画面遷移やシーン終了時に、関連する処理を一気に止めたい場合に便利です。

一度停止したコルーチンは再開できない

StopCoroutineで止めたコルーチンは、「途中から再開」できません。停止後にもう一度動かしたい場合は、StartCoroutineで改めて開始する形になります。

途中まで進んだ状態を保持して再開したいなら、自前で進行状態を変数に持たせるか、後述の「async/await・UniTaskとの違い」で紹介するasync/awaitを検討してみてください。

GameObjectが無効化されたときの自動停止

コルーチンの挙動でよくハマるのが、GameObjectの有効・無効状態に関する仕様です。コルーチンは、起動したMonoBehaviourが紐づくGameObjectが無効化されたタイミングで自動的に停止します。

private void Start()
{
    StartCoroutine(LoopRoutine());
}

private IEnumerator LoopRoutine()
{
    while (true)
    {
        Debug.Log("実行中");
        yield return new WaitForSeconds(1f);
    }
}

private void OnDisable()
{
    // この瞬間、上のコルーチンも停止する
}

SetActive(false)でGameObjectを非アクティブにすると、そのオブジェクトに紐づくコルーチンはすべて停止します。

一方で、MonoBehaviourのenabledをfalseにしただけではコルーチンは止まりません。

「コンポーネントを無効化しても止まらないが、GameObjectそのものを非アクティブにすると止まる」というのは混乱しがちなポイントなので、覚えておきましょう。

GameObjectがDestroyされたときも、当然コルーチンは停止します。破棄後にも後始末をしたい場合は、OnDisableやOnDestroyで明示的に処理を書いておくと安全です。

気をつける点

コルーチンは便利ですが、いくつか気をつけたいポイントがあります。

任意の場所で停止・再開はできない

コルーチンが一時停止できるのは、あくまでyield returnを書いた行のみです。外部からStopCoroutineで止めることはできますが、止めた地点から続きを再開する仕組みは用意されていません。

細かい中断・再開や、複雑なキャンセル処理が必要な場合は、後述の「async/await・UniTaskとの違い」で紹介するasync/awaitを使ったほうがシンプルに書けます。

WaitForSecondsはGC Allocが発生する

new WaitForSeconds(1f)のように毎回newすると、その都度ヒープにオブジェクトが確保されます。頻繁に呼ばれるコルーチンでは、GC Allocの発生がパフォーマンス低下の原因になりがちです。

同じ秒数を繰り返し使う場合は、インスタンスをキャッシュしておくとよいでしょう。

public class CachedWaitSample : MonoBehaviour
{
    // 毎フレーム使うWaitForSecondsをキャッシュしておく
    private readonly WaitForSeconds _waitOneSecond = new WaitForSeconds(1f);

    private IEnumerator Loop()
    {
        while (true)
        {
            Debug.Log("1秒ごとに実行");

            // キャッシュしたインスタンスを使い回す
            yield return _waitOneSecond;
        }
    }
}

WaitForEndOfFrameやWaitForFixedUpdateも同様にキャッシュ可能です。

シーンを跨ぐと止まる

コルーチンは起動したMonoBehaviourに紐づいているため、シーンを切り替えるとそのGameObjectごと破棄されてコルーチンも止まります。

シーンを跨いで処理を続けたい場合は、DontDestroyOnLoadで残るオブジェクトにコルーチンを持たせるか、シーン遷移の前後で必要な処理を分割しましょう。

using System.Collections;
using UnityEngine;

public class PersistentCoroutineRunner : MonoBehaviour
{
    private void Awake()
    {
        // シーンを跨いでも破棄されないようにする
        DontDestroyOnLoad(gameObject);
    }

    private void Start()
    {
        // このコルーチンはシーンを切り替えても止まらない
        StartCoroutine(LoopRoutine());
    }

    private IEnumerator LoopRoutine()
    {
        while (true)
        {
            Debug.Log("シーン跨ぎでも動作中");
            yield return new WaitForSeconds(1f);
        }
    }
}

async/await・UniTaskとの違い

コルーチンと似た用途で使える機能として、C#標準のasync/awaitや、サードパーティ製のUniTaskがあります。

これらは「非同期処理を直線的に書く」という点ではコルーチンと共通していますが、いくつか違いがあります。

観点コルーチンasync/await・UniTask
戻り値IEnumeratorTask / UniTask 系
起動の起点MonoBehaviourが必要どこからでも呼び出し可能
キャンセルStopCoroutineCancellationToken
値の返却直接は返せないTask<T> / UniTask<T> で返せる
例外処理try/catchが使いにくいtry/catchで自然に書ける

ざっくりまとめると、簡単な「待ち」や「演出の流れ」はコルーチンで十分書けます。キャンセル処理や例外処理、戻り値が必要になる場面では、async/awaitやUniTaskのほうが扱いやすいでしょう。

また、Unity 2023.1以降では、Unity公式の非同期処理APIAwaitableも追加されました。WaitForSecondsやNextFrameAsyncといった処理をawaitで書ける機能で、UniTaskに近い感覚で使えます。

とはいえ、コルーチンはシンプルで歴史も長く、Unityらしい書き方として馴染みやすい仕組みです。

まずはコルーチンに慣れておいて、複雑な非同期処理が必要になったタイミングでasync/awaitやUniTaskに手を伸ばしていく、という流れがおすすめです。

まとめ

コルーチンは、Unityで時間に関わる処理や非同期処理を書く際の、もっとも基本的な仕組みです。

Update内で状態管理を頑張らなくても、IEnumeratorとyield returnを組み合わせるだけで「待つ」「順番に処理する」といった流れを自然に書けます。

一方で、停止後の再開ができない点や、WaitForSecondsのGC Alloc、シーン跨ぎでの停止など、気をつけたい挙動もあります。

これらを踏まえつつ、まずは小さな演出処理や順序のある処理からコルーチンを取り入れてみてください。

より複雑な非同期処理が必要になったら、async/awaitやUniTask、Awaitableといった選択肢にも目を向けてみてください。

まずは身近な「3秒待ちたい」「フェードしたい」あたりから、コルーチンで書き換えてみるところから始めてみましょう。