この記事について

この記事ではCoroutine(コルーチン)での非同期処理をスクリプトで制御する方法が学べます。

この記事でわかること

  • Coroutine(コルーチン)の基本的な使い方
  • WaitForSecondsで時間を待つ方法
  • yield returnの使い方と種類
  • Coroutineの途中停止方法(yield break・StopCoroutine)
  • メモリ効率を高める最適化の方法

Coroutine(コルーチン)とは?

Coroutine(コルーチン)は、処理を途中で中断して、次のフレームや指定した時間の後に続きを実行できる仕組みです。

コルーチン内の処理は基本的にメインスレッドで実行されます。重い処理をコルーチンに移すだけでは、画面のフリーズを防げません。

同期・非同期処理との違いについては以下の記事を参考にしてください。

Coroutine(コルーチン)の使い方

Coroutine(コルーチン)の準備

まずはヒエラルキーで右クリックをして空のGameObjectを1つ用意します。
・「右クリック」→「Create Empty」

次にスクリプトを作成して作成した空のGameObjectにアタッチしてください。

WaitForSecondsを使用した非同期処理

Coroutineの基本的な使い方として「yield return new WaitForSeconds(3f)」で一定時間待って処理することができます。

using System.Collections;
using UnityEngine;

public class Coroutine_Yeild : MonoBehaviour
{
    void Start()
    {
        Debug.Log("Time : " + Time.time);
        StartCoroutine(ExampleCoroutine_wait());
        StartCoroutine(ExampleCoroutine_null1());
        StartCoroutine(ExampleCoroutine_null2());
    }

    
    IEnumerator ExampleCoroutine_wait()
    {
        yield return new WaitForSeconds(3); // 3秒待つ
        Debug.Log("ExampleCoroutine_wait : " + Time.time);
    }
    IEnumerator ExampleCoroutine_null1()
    {
        Debug.Log("ExampleCoroutine_null1 : Start");
        yield return null;  // ここで一旦中断し、次のフレームで再開
        Debug.Log("ExampleCoroutine_null1 : " + Time.time);
    }
    IEnumerator ExampleCoroutine_null2()
    {
        Debug.Log("ExampleCoroutine_null2 : Start");
        yield return null;  // ここで一旦中断し、次のフレームで再開
        Debug.Log("ExampleCoroutine_null2 : " + Time.time);
    }
}

下の動画では、WaitForSeconds(3)の後に約3秒でログが出ていますね。
2つのyield return nullは、その位置で一旦中断して次のフレームで再開します。そのため、null1とnull2のStartのログが先に出て、待機後のログが続きます。

WaitForSecondsはTime.timeScaleの影響を受ける時間で待機します。timeScaleが1なら約3秒、0.5なら実時間で約6秒が目安です。待機は現在のフレームの終わりから始まり、指定時間が過ぎた後のフレームで再開するため、秒単位でぴったりの実行を保証するものではありません。

yield breakで途中停止

コルーチンの中から処理を終了させたいときは「yield break」を使うと強制的にコルーチンを終了できます。

using System.Collections;
using UnityEngine;

public class Coroutine_Yeild : MonoBehaviour
{
    void Start()
    {
        Debug.Log("Time : " + Time.time);
        StartCoroutine(FadeOut());
    }

    // 終了を説明するための例(フェード処理は含みません)
    IEnumerator FadeOut()
    {
        Debug.Log("ProcessTime : " + Time.time);
        yield return new WaitForSeconds(3);
        yield break; // コルーチンを即終了
        Debug.Log("FinishTime : " + Time.time);
    }
}

下の動画では「FinishTime」のログが出ていませんね。この例では3秒待った後、yield breakでコルーチンが終了するためです。

この例では、終了後の行が実行されないことを示すためにDebug.Logを残しています。実際のコードでは到達できない行として警告の対象になるため削除してください。条件によって終了したい場合は、if文の中にyield breakを書きます。

Coroutine(コルーチン)のパフォーマンス最適化

最適化①:待機オブジェクトをキャッシュする

WaitForSecondsもWaitForSecondsRealtimeも、newで作るたびにオブジェクトを生成します。WaitForSecondsRealtimeに置き換えるだけではGC対策にはなりません。待ち時間が一定で繰り返し使う場合は、待機オブジェクトをキャッシュして、毎回のメモリ確保を減らせます。

以下は、1秒待つ処理を3回繰り返す例です。CoroutineWaitCacheExample.csとして保存して、GameObjectにアタッチしてください。

using System.Collections;
using UnityEngine;

public class CoroutineWaitCacheExample : MonoBehaviour
{
    private readonly WaitForSeconds waitTime = new WaitForSeconds(1f);

    void Start()
    {
        StartCoroutine(ExampleCoroutine());
    }

    IEnumerator ExampleCoroutine()
    {
        for (int i = 0; i < 3; i++)
        {
            yield return waitTime; // 待機のたびにnewしない
            Debug.Log("1秒ごとの処理");
        }
    }
}

WaitForSecondsとWaitForSecondsRealtimeの違いは時間の基準です。WaitForSecondsRealtimeはTime.timeScaleの影響を受けず、timeScaleが0でも待機が進みます。ポーズ中も動かしたい処理など、目的に合わせて選んでください。

上のコードは、1本のコルーチン内で同じ待機オブジェクトを順番に再利用する例です。特にWaitForSecondsRealtimeは待機中の状態を持つため、同じインスタンスを複数のコルーチンで同時に共有しないようにしましょう。

最適化②:IEnumeratorの使い回しに注意

yieldを使って作ったコルーチンのIEnumeratorは、途中まで進んだ状態を持っています。完了したIEnumeratorを保存しておいても、StartCoroutineに渡すだけで最初から実行し直せるわけではありません。同じIEnumeratorを同時に複数回開始することも避けてください。

最初からやり直したい場合は、その都度FadeOut()を呼び出して新しいIEnumeratorを作ります。停止したい実行は、StartCoroutineが返すCoroutineを保存して管理するとわかりやすいです。

以下は停止・再開始の管理例です。CoroutineRestartExample.csとして保存してください。FadeOutという名前ですが、ここでは待機後にログを出すだけで、実際のフェード処理は含みません。

using System.Collections;
using UnityEngine;

public class CoroutineRestartExample : MonoBehaviour
{
    private Coroutine fadeOutCoroutine;

    void Start()
    {
        StartFade();
    }

    public void StartFade()
    {
        StopFade(); // 実行中なら止めて、最初からやり直す
        fadeOutCoroutine = StartCoroutine(FadeOut());
    }

    public void StopFade()
    {
        if (fadeOutCoroutine != null)
        {
            StopCoroutine(fadeOutCoroutine);
            fadeOutCoroutine = null;
        }
    }

    IEnumerator FadeOut()
    {
        yield return new WaitForSeconds(1f);
        Debug.Log("待機後の処理");
        fadeOutCoroutine = null;
    }
}

この例では、StartFadeを呼ぶと実行中の処理を止めて最初から開始し、StopFadeを呼ぶと途中で終了します。yield breakはコルーチンの中から、StopCoroutineは外から停止するときに使います。StopCoroutineには、開始時に保存した同じCoroutineを渡してください。

なお、GameObjectを非アクティブにするとコルーチンは停止しますが、MonoBehaviourのenabledをfalseにするだけでは停止しません。停止が必要な場面を決めて管理しましょう。

キャッシュしてもコルーチン開始時のメモリ確保までなくなるわけではありません。まずはUnity ProfilerのGC.Allocなどで確保箇所を確認し、繰り返し発生している部分から見直していきましょう。

まとめ

  • Coroutineはフレームや時間経過に合わせた処理に便利な仕組み
  • yield returnで処理を一時停止/再開できる
  • yield breakやStopCoroutineで途中停止も可能
  • 最適化のポイント(WaitForSecondsのキャッシュ)も意識しよう!

参考:Unity公式:コルーチン/WaitForSeconds/WaitForSecondsRealtime/Unity公式:メモリのプロファイリング