Unity 6|コルーチンの仕組みを図解!yield returnで処理が止まり、再開するまで

広告

Unityでゲームを作っていると、こんな処理を実現したくなることがあります。

  • 敵を倒してから、1秒後に次の敵を出現させたい
  • 「3、2、1、スタート!」と、時間を空けながら表示したい
  • キャラクターを少しずつ透明にして、最後に消したい
  • ダメージを受けたキャラクターを、一定時間だけ点滅させたい

このような処理で便利なのが、コルーチン(Coroutine)です。

しかし、初めてコルーチンを見ると、IEnumerator、StartCoroutine()、yield return など、見慣れない書き方が登場します。

「普通のメソッドと何が違うの?」

「yield return で止まった処理は、いつ動き出すの?」

「ゲーム全体が止まってしまうの?」

この記事では、こうした疑問を一つずつ解決していきます。

目標は、コルーチンのコードを見たときに、どの行がいつ実行されるのかを自分で説明できるようになることです。

対象はUnity 6です。


1. 普通のメソッドは、途中で待ってくれない

まずは、普通のC#のメソッドを見てみましょう。

private void Start()
{
    Debug.Log("処理A");
    Debug.Log("処理B");
    Debug.Log("処理C");
}

このコードを実行すると、Consoleには次のように表示されます。

処理A
処理B
処理C

処理は上から順番に進みます。

途中に待機処理がないため、A、B、Cは同じフレーム内で続けて実行されます。

では、次のような処理を作りたい場合はどうでしょうか。

処理A
   ↓
1秒待つ
   ↓
処理B
   ↓
1秒待つ
   ↓
処理C

普通のメソッドには、「いったん処理を中断して、ゲームを動かしながら、後で続きを実行する」という仕組みはありません。

Update() とタイマー変数を組み合わせても実現できますが、処理が増えると状態の管理が複雑になることがあります。

そこで登場するのがコルーチンです。

2. コルーチンは「途中で止めて、後から続きを実行する」仕組み

コルーチンは、簡単にいうと、

「処理の途中でしおりを挟み、後から続きの行を実行できる仕組み」

です。

本を読んでいる場面を想像してみてください。

普通のメソッドは、最初から最後まで一気に読み進めるようなものです。

一方、コルーチンは途中にしおりを挟んで本を閉じ、後でその場所から読み始められます。

例えば、次のコードです。

private IEnumerator SampleCoroutine()
{
    Debug.Log("処理A");

    yield return null;

    Debug.Log("処理B");
}

この yield return null; がポイントです。

最初に「処理A」が実行されます。

そして yield return null; に到達すると、コルーチンは一時中断します。

次のフレームになると、その次の行から処理を再開して「処理B」が実行されます。

ここで大切なのは、コルーチンが止まっていても、ゲーム全体は止まらないということです。

ほかのGameObjectは動き続けますし、ほかのスクリプトの Update() なども通常どおり実行されます。

コルーチンは、ゲーム全体を止めずに、特定の処理の続きを後回しにできるのです。

3. 最初に知っておきたい3つの書き方

コルーチンを使うときは、基本的に次の3つが登場します。

① IEnumerator

private IEnumerator SampleCoroutine()

普通のメソッドでは、戻り値の型として void を使うことが多いですね。

コルーチンとして使うメソッドでは、一般的に IEnumerator を使用します。

これはC#に用意されているインターフェースで、処理を段階的に進めるために利用されます。

yield return を含むメソッドでは、コンパイラーが途中の実行状態を保持できる仕組みを作ってくれます。

最初の段階では、

IEnumeratorは「途中から続きを実行できるようにするための型」

と考えれば十分です。

なお、IEnumerator を使うには、ファイルの先頭に次の記述が必要です。

using System.Collections;

② StartCoroutine()

StartCoroutine(SampleCoroutine());

これは、コルーチンを開始するためのUnityのメソッドです。

注意したいのは、次の書き方との違いです。

SampleCoroutine();

こちらは IEnumerator を返すメソッドを呼び出しているだけです。

今回のように yield return を含むメソッドでは、これだけでは本体の処理は進みません。

Unityにコルーチンとして実行させるには、StartCoroutine() を使います。

③ yield return

yield return null;

yield return は、コルーチンの実行をいったん中断するための記述です。

その後ろに何を指定するかによって、再開する条件が変わります。

例えば、

yield return null;

なら、次のフレームまで待ちます。

yield return new WaitForSeconds(1f);

なら、ゲーム内時間で指定した時間が経過するのを待ちます。

ここがコルーチンの最大の特徴です。

4. 実際に動かして、実行順序を確認しよう

ここからが、この記事で最も重要な部分です。

次のコードをUnityで実行してみましょう。

Hierarchyで空のGameObjectを作成し、CoroutineFlowDemo.cs というスクリプトをアタッチしてください。

using System.Collections;
using UnityEngine;

public class CoroutineFlowDemo : MonoBehaviour
{
    private void Start()
    {
        Debug.Log($"① 呼び出し前:フレーム {Time.frameCount}");

        StartCoroutine(ShowSteps());

        Debug.Log($"③ 呼び出し後:フレーム {Time.frameCount}");
    }

    private IEnumerator ShowSteps()
    {
        Debug.Log($"② コルーチン開始:フレーム {Time.frameCount}");

        yield return null;

        Debug.Log($"④ 次のフレームで再開:フレーム {Time.frameCount}");

        yield return new WaitForSeconds(1f);

        Debug.Log($"⑤ 1秒の待機後:フレーム {Time.frameCount}");
    }
}

コードを実行する前に、少し考えてみてください。

Consoleには、①から⑤がどの順番で表示されるでしょうか?

答えは次のとおりです。

① 呼び出し前
② コルーチン開始
③ 呼び出し後
④ 次のフレームで再開
⑤ 1秒の待機後

① → ② → ③ → ④ → ⑤ の順番です。

「どうして③より先に②が表示されるの?」

ここがとても重要です。

順番に追ってみましょう。

ステップ1:①が実行される

まず、Unityから Start() が呼び出され、①がConsoleに表示されます。

ステップ2:StartCoroutine()によって②が実行される

StartCoroutine(ShowSteps());

Unityは ShowSteps() の処理を開始します。

このとき、コルーチンは最初の yield return まで実行されます。

そのため、②が表示されます。

ステップ3:yield return nullで中断する

yield return null;

ここでコルーチンは一時中断します。

Unityは StartCoroutine() の呼び出し元に処理を戻します。

ステップ4:③が実行される

Debug.Log($"③ 呼び出し後:フレーム {Time.frameCount}");

この時点ではコルーチンの処理がすべて終了したわけではありません。

①、②、③は同じフレームで実行されています。

ステップ5:次のフレームで④が実行される

次のフレームになると、Unityは中断していたコルーチンを再開します。

yield return null; の次の行である④が実行されます。

ステップ6:指定時間の経過後に⑤が実行される

yield return new WaitForSeconds(1f);

ここでもう一度中断します。

指定時間が経過すると、その後の再開可能なフレームで⑤が実行されます。

最後まで到達したコルーチンは、そこで終了します。

フレーム番号にも注目しよう

コードでは、次の値を表示しています。

Time.frameCount

これはUnityのフレーム番号です。

例えば、実行結果が次のようになることがあります。

① 呼び出し前:フレーム 100
② コルーチン開始:フレーム 100
③ 呼び出し後:フレーム 100
④ 次のフレームで再開:フレーム 101
⑤ 1秒の待機後:フレーム 162

※数字は説明用の例です。実際の結果は実行環境やフレームレートによって変わります。

ここからわかるのは、

  • ①②③は同じフレーム
  • ④は次のフレーム
  • ⑤は時間が経過した後のフレーム

ということです。

コルーチンを理解するときは、処理の順番だけでなく、フレームが変わっているかどうかにも注目しましょう。

5. yield return nullとWaitForSecondsの違い

よく使用する待機方法は次の3つです。

書き方どのくらい待つ?
yield return null;次のフレームまで
yield return new WaitForSeconds(1f);ゲーム内時間で約1秒
yield return new WaitForSecondsRealtime(1f);現実の経過時間で約1秒

次のフレームまで待つ

yield return null;

これは、コルーチンを次のフレームまで中断します。

例えば、キャラクターを少しずつ移動したり、透明度を変化させたりする場面で使います。

private IEnumerator MoveRight()
{
    for (int i = 0; i < 60; i++)
    {
        transform.position += Vector3.right * 0.05f;

        yield return null;
    }
}

このコードでは、1フレームごとに少しずつ右へ移動します。

60回繰り返しますが、60回分の移動が一度に実行されるわけではありません。

途中に yield return null; があるため、60フレームに分けて実行されます。

ただし、移動量はフレームレートに依存します。実際のゲームで一定の速度にしたい場合は、Time.deltaTimeを使って移動量を計算する方法が一般的です。

yield return null; で待っているコルーチンは、そのフレームの Update() が実行された後、LateUpdate() が実行される前に再開されます。

次の図で、同じコルーチンが毎フレーム再開される流れを確認してみましょう。図中の B が、コルーチンの再開位置です。

指定した秒数だけ待つ

yield return new WaitForSeconds(1f);

こちらは、指定時間の経過を待ちます。

例えば、

private IEnumerator Countdown()
{
    Debug.Log("3");

    yield return new WaitForSeconds(1f);

    Debug.Log("2");

    yield return new WaitForSeconds(1f);

    Debug.Log("1");

    yield return new WaitForSeconds(1f);

    Debug.Log("スタート!");
}

このように、時間を空けながら順番に処理できます。

StartCoroutine(Countdown()); で開始すれば、カウントダウンの完成です。

ちょうど1秒後とは限らない

ここで注意したいのが、WaitForSeconds(1f) の意味です。

「1秒後に必ずぴったり実行する」という意味ではありません。

Unityでは、コルーチンが yield return に到達してから指定時間が経過した後、再開できる最初のフレームで処理が続きます。

そのため、実際の再開時刻には多少のずれが生じます。

また、WaitForSeconds は Time.timeScale の影響を受けます。

例えば、ゲームを一時停止するために Time.timeScale = 0 にすると、WaitForSeconds の待機も進まなくなります。

一方、WaitForSecondsRealtime は Time.timeScale に影響されない待機に使います。

6. コルーチンは別スレッドで動いているの?

ここは重要な勘違いの一つです。

コルーチンを使用すると、待機している間もほかのゲーム処理が動いています。

そのため、

「コルーチンは裏側で別のスレッドとして動いているのでは?」

と考えてしまうかもしれません。

しかし、コルーチン自体は別スレッドではありません。

通常のコルーチン内の同期処理は、Unityのメインスレッド上で実行されます。

コルーチンは、処理をフレームごとに区切って、Unityに制御を返す仕組みです。

例えば、

private IEnumerator HeavyWork()
{
    for (int i = 0; i < 100000000; i++)
    {
        // 重い計算を行う
    }

    yield return null;
}

この場合、yield return null; に到達するまでは、長いループが一度に実行されます。

コルーチンに書いたからといって、自動的に処理が分散されるわけではありません。

コルーチンは「同時に別の場所で計算する仕組み」ではなく、「実行を途中で区切る仕組み」です。

7. コルーチンはいつ終了するの?

コルーチンには、主に次のような終了・停止のパターンがあります。

① 最後まで実行したとき

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

    yield return null;

    Debug.Log("終了");
}

最後の行まで実行すると、コルーチンは自然に終了します。

② StopCoroutine()で停止したとき

実行中のコルーチンを途中で止めることもできます。

private Coroutine runningCoroutine;

private void Start()
{
    runningCoroutine = StartCoroutine(Sample());
}

private IEnumerator Sample()
{
    while (true)
    {
        Debug.Log("実行中");

        yield return new WaitForSeconds(1f);
    }
}

public void StopSample()
{
    if (runningCoroutine != null)
    {
        StopCoroutine(runningCoroutine);
        runningCoroutine = null;
    }
}

この例では runningCoroutine に実行中のコルーチンを保持しています。

StopSample() を呼び出すと、そのコルーチンを停止できます。

なお、StopSample() を呼び出すためのボタンやキー入力などは、このサンプルには含めていません。

③ GameObjectが非アクティブになったとき

gameObject.SetActive(false);

GameObjectを非アクティブにすると、そこにアタッチされたMonoBehaviourのコルーチンは停止します。

再び SetActive(true) に戻しても、停止したコルーチンが自動で続きを実行することはありません。

必要なら改めて開始します。

8. コンポーネントを無効にしても止まらない?

ここは初学者が混乱しやすいポイントです。

Unityでは、コンポーネントを無効にする操作とGameObjectを無効にする操作は同じではありません。

操作実行中のコルーチン
enabled = false自動では停止しない
gameObject.SetActive(false)停止する
StopCoroutine()指定したものを停止する
StopAllCoroutines()そのMonoBehaviourのコルーチンをすべて停止する
MonoBehaviourをDestroy()停止する

例えば、Inspectorでスクリプトのチェックを外しても、そのコンポーネントで開始済みのコルーチンは自動停止しません。

Update() が呼ばれなくなることとは、別の話なのです。

一方、GameObjectそのものを非アクティブにすると、コルーチンは停止します。

この違いは、ゲーム制作中に予想外の動作を引き起こすことがあるため、覚えておきましょう。

なお、OnDisable() の中で自分で停止処理を書いている場合は、コンポーネントを無効化したときにも停止させられます。

9. よくある疑問

Q. yield returnを使うと、Start()も一緒に止まりますか?

いいえ。

今回の例では、StartCoroutine() が最初の yield return までコルーチンを進めてから、Start() の続きを実行します。

Start() 自体がコルーチンの終了まで待つわけではありません。

Q. yield return nullは、何秒待つという意味ですか?

秒数ではなく、次のフレームまで待つという意味です。

60fpsならフレーム間隔はおよそ1/60秒ですが、実際のフレーム時間は一定とは限りません。

Q. コルーチンを開始すると、ほかのUpdate()は止まりますか?

いいえ。

コルーチンが待機している間も、ほかのUnityの処理は通常どおり実行されます。

ただし、コルーチン内で重い同期処理を行えば、その間はメインスレッドが占有されるので注意が必要です。

Q. yield returnを書くたびに、新しいコルーチンが作られますか?

いいえ。

同じコルーチンの処理を中断し、その続きを再開しています。

yield return は、それ自体が新しいコルーチンを作る命令ではありません。

Q. コルーチンの中で変数を使ったら、再開時に値は残っていますか?

はい。通常、コルーチンの途中で使っていたローカル変数の状態は保持されます。

例えば、

private IEnumerator Sample()
{
    int count = 10;

    yield return null;

    Debug.Log(count);
}

この場合、次のフレームに再開したときも count は10です。

これも、途中から続きを実行できる理由の一つです。

10. まとめ|コルーチンは「しおりを挟めるメソッド」

この記事で覚えてほしいことを整理しましょう。

  • IEnumerator:処理を途中から進められる形にする
  • StartCoroutine():Unityにコルーチンの実行を開始させる
  • yield return:処理をいったん中断する
  • yield return null:次のフレームに再開する
  • WaitForSeconds:指定時間の経過を待つ
  • コルーチンは別スレッドではない:通常の同期処理はメインスレッド上で実行される

最初は、IEnumerator の内部構造を完全に理解する必要はありません。

大切なのは、コードを上から読みながら、

「ここまでは今のフレームで実行される」

「ここで一時中断する」

「次のフレーム、または指定時間の経過後に、ここから続きを実行する」

と説明できるようになることです。

まずはこの記事のサンプルを実行して、Consoleに表示される順番を確かめてみてください。

そして、yield return null; を yield return new WaitForSeconds(2f); に変更してみましょう。

コードを少し書き換え、実際の動作と予想を比べることが、理解への近道です。


関連記事

参考資料(Unity公式)

訪問数 4 回, 今日の訪問数 4回

広告