C#
Unity

親のCoroutineからネストしたCoroutineを制御したい

概要

UnityのCoroutineは基本StopCoroutine(),StopAllCoroutineAll()で一時停止する仕様になっているが、ネストしたCoroutineも含めて親のCoroutineからStopCoroutineするとネストしたCoroutineが終了してからしか一時停止しません。

実例

例えば以下のようなコードで

IEnumerator _coroutine;

void Start()
{
  _coroutine = Coroutine();
  StartCoroutine(_coroutine);
}

IEnumerator Coroutine()
{

    yield return NestCoroutine();

}

IEnumerator NestCoroutine()
{
    var time = 0f;
    while(time < 2f){
       yield return null;
       time += Time.deltaTime;
    }
}

Coroutineを実行中に以下のように親のCoroutineをStopしようとしてもNestCoroutineの終了を待ってからしかStopしてくれません。

void Stop()
{
    StopCoroutine(_coroutine);
}

これが結構不便で、例えばゲームのポーズを実装したい場合にStopAllCoroutineで全部停止して済むなら良いのですが、特定のCoroutineだけ止めたい場合もあるわけで。
こういう場合に親のCoroutineをStopCoroutineさせるだけではネストしたCoroutineが止まってくれないので、ネストした全てのCoroutine中にフラグを設定してフラグで制御するとか、すべてのネストしたCoroutineを変数に格納しておいてStopCoroutineするなど面倒な制御を要求されます。

こういった細かい制御をしたい場合IEnumeratorを自前でMoveNext()していく方法があります。
MoveNestでCoroutineを実行していく場合、以下のように書きます。UpdateでMoveNextを回して行くのが一般的ですが、あえてStartCoroutine中でMoveNextを回していきます。理由は後述。

void Start()
{
   StartCoroutine(TaskCoroutine(Coroutine()));
}

IEnumerator TaskCoroutine(IEnumerator task)
{
    while(true)
    {
        if(task.MoveNext()){
            yield return null;
        }
        else {
            break;
        }
    }
}

IEnumerator Coroutine()
{
  yield return NestCoroutine();
}

IEnumrator NestCoroutine()
{
   yield return null;
}

ただし、MoveNext()を単純に回して行くだけだとネストしたコルーチンには対応できません。
ではどうするかというと、実はネストしたコルーチンの情報はIEnumeratorのCurrentの部分に含まれているのでこれをまたMoveNext()で回して行けばよいのです。

    public interface IEnumerator
    {
        object Current { get; }

        bool MoveNext();
        void Reset();
    }
}

後は、currentの型を判断してネストしたコルーチンに何が含まれているかで場合分けして処理を分けます。typeがIEnumeratorの場合はネストしたコルーチンなので、再帰的にMoveNextを実行します。
前述したようにStartCoroutineでMoveNextを実行しているのは、IEnumerator以外の型の場合はUnity側に処理を任せるためです。

後は、唯一設定したフラグ_isPauseを制御することで、ネストしたコルーチン含めて一時停止、一時停止解除することが出来ます。

    bool _isPause = false

    void Start()
    {
      var task = Coroutine();
      StartCoroutine(TaskCoroutine(task));
    }


    IEnumerator TaskCoroutine(IEnumerator task)
    {
        while (true)
        {
            var ie = MoveNext(task);
            yield return ie;
            if (ie.Current is bool)
            {
                if (!(bool)ie.Current)
                {
                    break;
                }
            }
        }
    }

    IEnumerator MoveNext(IEnumerator task)
    {
        if (_isPause)
        {
            yield return null;

        }
        else
        {
            if (task.MoveNext())
            {
                var current = task.Current;

                if (current == null)
                {
                    yield return null;
                }
                else
                {
                    var type = current.GetType();
                    //IEnumeratorはcurrent isで判断する
                    if (current is IEnumerator)
                    {

                        while (true)
                        {
                            //currentがIEnumeratorの場合再帰的に実行
                            var ie = MoveNext((IEnumerator)current);
                            yield return ie;
                            if (ie.Current is bool)
                            {
                                if (!(bool)ie.Current)
                                {
                                    break;
                                }
                            }
                        }
                    }
                    //IEnumerator以外はcurrent.GetType()で判断する
                    //下記は説明用の分岐。
                    else if (type == typeof(Coroutine))
                    {
                        yield return current;

                    }
                    else
                    {
                        yield return current;
                    }
                }
            }
            else
            {
                yield return false;
            }
        }

    }

    IEnumerator Coroutine()
    {
        yield return NestCoroutine();
    }

    IEnumerator NestCoroutine()
    {
        var time = 0f;
        while (time < 2f)
        {
            yield return null;
            time += Time.deltaTime;
            Debug.Log(time);
        }
    }

補足

上記のコードを見ると分かりますが一時停止できるのはネストしたコルーチンがyield return (IEnumerator関数)の時だけで、yield return new WaitForSecound(1.0f)とか、yield return StartCoroutine(Coroutine()) とかの場合には無理です。てか、StopCoroutineがそもそもネストしたコルーチンも含めてStopできないのが問題なのです。