👏

[unity]コルーチンはなぜ再開できないのか

に公開

最適解を言えば「UniTaskを使え。コルーチンやめろ」となるのですが、

  • 会社の事情だったり
  • GitHub に置いておくサンプルだったり
  • Cysharp が今後も更新してくれるか不安だったり
  • 初心者だから新しいものを導入し(覚え)たくなかったり
  • あるいは宗教的な理由

様々な理由で Coroutine を使う必要があるかもしれません。

コルーチンは続きから再開できない

たとえば次のようなコルーチンです。

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

IEnumerator coroutine()
{
  for (int i = 0; ; i++)
  {
    Debug.Log($"{i}");

    yield return null;
  }
}

延々とコンソールに数字を垂れ流し続ける、このコルーチンの問題は

  • GameObject が無効化されると動作を停止
  • その後有効化されても動作は再開されない

という、いささか直感的とは言えない挙動です。

たとえばアプリが一旦落とされる……。

こういった場合、GameObject が無効化されるのでコルーチンは実行を止めます。
その後すぐアプリ再開(= GameObject 有効化)されても、コルーチンは再開しません。
実装側からしたら、そこは続きから再開して欲しいところですよね。

void OnEnable()
{
  StartCoroutine(coroutine());
}

無理やりこんなコードを入れてみても、コルーチンがまた最初から実行されるだけなので、かえって副作用や予想外の問題を引き起こすでしょう。

このせいで泣く泣く変数外出しにして Update() に全てを背負わせている人も多いんじゃないでしょうか。

StartCoroutine の戻り値を使って、続きから再開

実は、「続きから再開」を可能にすることはできます。それが次のコードです。

IEnumerator co;

void Start()
{
  co = StartCoroutine(coroutine());
}

void OnEnable()
{
  if (co != null) StartCoroutine(co);
}

IEnumerator coroutine()
{
  for (int i = 0; ; i++)
  {
    Debug.Log($"{i}");

    yield return null;
  }
}

通常なら個別 StopCoroutine() するための StartCoroutine() の戻り値を取っておき、OnEnable する際にはその戻りを使って StartCoroutine() します。

co が進行状況を持っているため、続きから再開できるというわけです。

Unity が公式で保証しているわけではない

私が Unity を始めてから10年近く、この挙動については現在までずっと使えるので問題ないと思いますが、この挙動はC# の列挙子の性質に依存した挙動と言えるもので、Unity が公式で保証しているわけではない、ちょっとした裏技のようなものだと思ってください。

コルーチンをバリバリに使いたいのであれば、余程のことがない限り UniTask を使っちゃいましょう!

Discussion