🍌

【Unity】怖くない!コルーチン解説

2021/05/05に公開

はじめに

Unityにはコルーチンと呼ばれる機能があります。
とても便利でちゃんと使えばかなり簡潔にプログラムを書くことができるようになります。

しかしこの「コルーチン」ですが、これ敬遠するUnity初学者の方を多く見ます。
私もUnityを覚えた直後は「Unityのプログラムを書くことがすでに大変なのに、コルーチンというよくわからないものを覚えて脳をパンクさせたくない」という思いからコルーチンを避けていました。
今思えばこれは非常に勿体ないことであったと感じています。

「よくわからないからコルーチンを使わず、Update()を使ってゴリ押しで書いてしまう」というやり方はプログラムを無駄にややこしくしてしまいます。
コルーチンをどういう場面で使うかをしっかり覚えた上で、適切に使えるようになりましょう。

早わかりUpdate()とコルーチンの違い

比較表

Update() コルーチン
1コンポーネント内で使える数 1個だけ 何個でも
処理の呼び出され方 毎回Update()関数の頭から実行される 前回中断したところから実行される
Inputへのアクセス 問題ない 問題ない
Rigidbodyへのアクセス よくない よくない
実行タイミング だいたい同じ だいたい同じ
開始のタイミング コンポーネントがEnableな間、1フレーム毎にずっと実行される StartCoroutineを呼び出したタイミング
終了のタイミング コンポーネントが無効化/破棄されたら停止する StopCoroutineを呼び出す/コンポーネントが破棄されたら停止する

それぞれの用途

Update()

  • 毎フレーム必ず実行したいとき

コルーチン

  • 「しばらく待つ」みたいな処理をしたいとき

それぞれの違いを解説

それぞれの記法

Update()とコルーチンとでは記法(書き方)が異なります。

Update()の書き方

MonoBehaviourを継承したクラスにおいて、Update()という関数を定義するだけです。
あとはUnityが勝手にこの関数を毎フレーム呼び出してくれます。

using UnityEngine;

// MonoBehaviourを継承する必要あり
public class SampleUpdate : MonoBehaviour
{
    // void Update() という関数を定義すれば勝手にUnityが実行してくれる
    private void Update()
    {
        // ここに毎フレームやりたい処理を書く
    }
}

コルーチンの書き方

書き方

コルーチンはちょっと書き方が複雑です。

  1. IEnumeratorを返す関数を書く(関数名は好きに付けてよい)。これがコルーチン本体になる。
  2. コルーチン本体の中身を書く( yield return/yield breakを必ず1回は使う必要あり)
  3. コルーチンを起動したタイミングでStartCoroutine()を使ってコルーチン本体を呼び出す
using System.Collections; // これ必要
using UnityEngine;

public class SampleCoroutine : MonoBehaviour
{
    private void Start()
    {
        // StartCoroutine()の引数に、コルーチン関数を呼び出した結果を渡すと
        // コルーチンが起動する
        StartCoroutine(NantokaCoroutine());
    }

    // IEnumerator型の関数を定義すればコルーチンになる
    // 関数名は好きに付けて良い
    private IEnumerator NantokaCoroutine()
    {
        Debug.Log("Hello!");

        // コルーチンにおいては
        // yield return という特殊なreturnが使える
        yield return null;

        Debug.Log("World!");

        // yield break でコルーチンを終了する
        // (関数末尾に到達すれば勝手に止まるので、この場合は省略してもOK)
        yield break;
    }
}

引数の追加、複数起動

また、コルーチンはUpdate()と違って複数個同時に実行したり、引数を渡したりもできます。
実行開始のタイミングも自由に設定できるため、必要なタイミングで必要な数だけコルーチンを起動させることができます。

using System.Collections; // これ必要
using UnityEngine;

public class SampleCoroutine : MonoBehaviour
{
    private void Start()
    {
        // 1秒後にHello!と表示
        StartCoroutine(WaitForSecondsCoroutine(1.0f, "Hello!"));
        
        // 2秒後にBye!と表示
        StartCoroutine(WaitForSecondsCoroutine(2.0f, "Bye!"));
    }

    private void OnCollisionEnter(Collision other)
    {
        // なにかに衝突したら0.5秒後に「Ouch!」と表示
        StartCoroutine(WaitForSecondsCoroutine(0.5f, "Ouch!"));
    }

    // 一定時間後にメッセージを表示するコルーチン
    private IEnumerator WaitForSecondsCoroutine(float seconds, string message)
    {
        // 指定秒数待つ
        yield return new WaitForSeconds(seconds);

        // メッセージ出力
        Debug.Log(message);
    }
}

コルーチンの停止

また、コルーチンは途中で止めることができます。
StartCoroutine()の結果を保存しておいて、それに対してStopCoroutine()することで停止させることができます。
なお、StopCoroutine()で停止したコルーチンは破棄されます。

using System.Collections; // これ必要
using UnityEngine;

public class SampleCoroutine : MonoBehaviour
{
    // 今実行中のコルーチン
    private Coroutine _currentCoroutine;

    // 1秒ごとに1mずつ動くコルーチン
    private IEnumerator MoveCoroutine()
    {
        while (true)
        {
            transform.position += Vector3.forward;
            yield return new WaitForSeconds(1);
        }
    }

    // 移動開始
    public void StartMove()
    {
        if (_currentCoroutine == null)
        {
            // 実行中のコルーチンが無いなら新しく起動
            _currentCoroutine = StartCoroutine(MoveCoroutine());
        }
    }

    // 移動停止
    public void StopCoroutine()
    {
        if (_currentCoroutine != null)
        {
            // 実行中のコルーチンを停止(破棄)
            StopCoroutine(_currentCoroutine);
            _currentCoroutine = null;
        }
    }

    /*
    
    ↓これ相当の処理は自動的に走るので書かなくてもOK
    (コンポーネント破棄時に勝手にコルーチンは止まる)
     
    private void OnDestroy()
    {
        // すべてのコルーチンを停止する
        StopAllCoroutines();
    }
    */
}

それぞれの挙動

Update()は常に関数の最初から実行される

まずUpdate()ですが、常に関数定義の最初から(頭から)実行されます。

そのため「条件を満たしたときは別の処理に切り替える」「一定時間経ったら別の処理をする」といったものを書こうとすると、Update()だけではものすごく面倒くさいコードになってしまいます。

コルーチンは一時中断ができる

一方のコルーチンは関数の途中で一時停止して、次のフレームで続きから実行するということができます。

yield return」という命令がポイントです。
これを書くことで「処理をいったんここで止める」という操作が可能となるのです。

そしてこのyield returnで返す値によって、「どれくらい処理を止めるのか」を制御できます。

返す値 効果 補足
yield return null 1フレームだけ待つ 次のフレームから続きが実行される
yield return new WaitForSeconds(秒数) 指定した秒数だけ待つ 指定秒数経過した後のフレームから続きが実行される
yield return new WaitForFixedUpdate 次のFixedUpdateを待つ 続きがFixedUpdate()と同じタイミングから実行される
他のコルーチン 他のコルーチンが終わるまで待つ

他にもコルーチンで使えるオブジェクトがありますが、いったんはこれくらいで説明を切り上げます。
重要なことはコルーチンを使えば処理を一時停止できるという点です。

実装例: 攻撃ボタンを押したら攻撃するが、その後1秒間攻撃できなくする例

次のような処理があったとします。

  • 攻撃可能な状態ならボタンを押した瞬間に攻撃する
  • 1度攻撃したら1秒以上待たないと次の攻撃ができない

これを簡単なフローチャートで書くと次のようになります。

この処理をUpdate()とコルーチンの2つで実装し、どちらのほうが簡単にかけるか比較してみます。

Update()で書く

Update()は「状態を覚える」ということができないため、かならず「現在の状態」をフィールド変数として定義する必要があります。

この程度の処理ならまだいいのですが、やりたいことがより複雑になってくるとUpdate()処理の中身が肥大化し、それに伴ってフィールド変数の数もどんどん増えてしまいます。
そのためUpdate()のみで複雑な処理を書くのはすぐに限界がきてしまいます。

using UnityEngine;

public class WaitOnUpdate : MonoBehaviour
{
    // 待ち時間
    private float _waitTime;

    // 待ち時間がゼロ以下なら攻撃可能
    private bool CanAttack()
    {
        return _waitTime <= 0;
    }

    private void Update()
    {
        // 待ち時間の計算
        if (_waitTime > 0) _waitTime -= Time.deltaTime;

        // Aボタンで攻撃する
        if (CanAttack() && Input.GetKeyDown(KeyCode.A))
        {
            Attack();
        }
    }

    // 攻撃処理
    private void Attack()
    {
        Debug.Log("Attack!");

        // 1秒間攻撃無効化
        _waitTime = 1.0f;
    }
}

コルーチンで書く

同じ処理をコルーチンで書くとこんな感じになります。

using System.Collections;
using UnityEngine;

public sealed class WaitOnCoroutine : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(InputCoroutine());
    }

    private IEnumerator InputCoroutine()
    {
        while (true)
        {
            if (Input.GetKeyDown(KeyCode.A))
            {
                // 攻撃キーが押されたら攻撃したあと、1秒待つ
                Attack();
                yield return new WaitForSeconds(1);
            }
            else
            {
                // 攻撃キーが押されてないなら1F待つ
                yield return null;
            }
        }
    }

    // 攻撃処理
    private void Attack()
    {
        Debug.Log("Attack!");
    }
}

yield returnと無限ループをうまく組み合わせることで「1回処理を実行したらしばらく待って、繰り返す」という処理をフィールド変数なしで実現できました。

また、さきほどのフローチャートと比較してみてください。
フローチャートの内容がほぼそのままコードに反映されていることが分かるかと思います。


(「1秒待つ」「1F待つ」がそのままコードに反映されている)

このようにコルーチンは「ちょっと待つ」といった処理をそのままコードに反映できるのです。

まとめ

  • Update()は毎フレーム最初から実行される
  • コルーチンは前のフレームの続きから関数を実行できる
  • コルーチンを使えば「しばらく待つ」といった処理が簡単にかける

コルーチンの方が記法がちょっと複雑になるため手が出しにくいと思っている人もいるかもしれません。
ですがコルーチンはUpdate()のみでゴリ押しで書くよりも圧倒的にコードがきれいになる可能性を秘めています。
コルーチンを使ったことがないという方はぜひこの機会に試してみることをオススメします。

(補足)

外部ライブラリやちょっと複雑な機能に手を出すのは「コルーチンを使いこなせるようになってから」をオススメします。

「時間を管理するのにUniRxがいいと聞いた」
async/awaitってやつを使うとコルーチンを使わなくてもいいらしい」

みたいな声をたまに聞きますが、コルーチンの使い方もわかってない状態でこれらに手をだしても使いこなせるわけがありません。

コルーチン、async/awaitUniRxとで比較をすると、コルーチンがもっともシンプルです。
コルーチンでは複雑なことはできないですが、その分挙動が単純なため初級者でもまだ使うことができるというメリットがあります。

変に背伸びせず、まずは「コルーチン」から使ってみて、コルーチンの機能に物足りなさを感じたらこれらライブラリに手を出すという流れをオススメします。

Discussion