UnityのCoroutineのつまづきやすいところまとめ

本記事は[Craft Egg Advent Calendar 2021](https://qiita.com/advent-calendar/2021/craftegg)の12/19の記事です。（投稿が遅れ申し訳ありません）
12/18の記事は@arumaniさんの「iPhoneで3D空間に入って撮影できる『Unity Virtual Camera』」でした。


# はじめに
株式会社Craft EggでUnityクライアントエンジニアをしている鈴木です。
UnityにおけるCoroutineのよく「どうだったっけ？」ってなるつまづきやすいところをまとめました。
基本的な項目が多めになっていますが、非同期処理ということもあり、意外と直面した時に気づくことができないこともありますので、Coroutine周りでつまづいた時にチェックするリスト代わりに使えるものになればと思います。

# Coroutineのつなぎ方
- 内部でStartCoroutineする必要はない
- yield returnでつなぐと内部で即時breakしていても1フレーム経過する
- movenextでつなぐとフレーム経過しない
    - 参考記事：[【Unity】 StartCoroutine以外でコルーチンを回す方法](http://posposi.blog.fc2.com/blog-entry-227.html)

## yield returnとmovenext検証
```c#
    void Hoge()
    {
        Debug.Log($"メインコルーチン開始 :  {Time.frameCount}");
        //任意のコルーチンを実行
        //StartCoroutine(MoveNextCoroutine());
    }

    
    private IEnumerator YieldReturnCoroutine()
    {
        Debug.Log($"YieldReturnCoroutine開始 :  {Time.frameCount}");

        yield return SubCoroutine();

        Debug.Log($"YieldReturnCoroutine終了 : {Time.frameCount}");
    }
    
    private IEnumerator MoveNextCoroutine()
    {
        Debug.Log($"MoveNextCoroutine開始 :  {Time.frameCount}");

        IEnumerator coroutine = SubCoroutine();
        while(coroutine.MoveNext()) { yield return coroutine.Current; }

        Debug.Log($"MoveNextCoroutine終了 : {Time.frameCount}");
    }
    
    private IEnumerator SubCoroutine()
    {
        Debug.Log($"SubCoroutine開始 : {Time.frameCount}");
        Debug.Log($"SubCoroutine終了 : {Time.frameCount}");
        yield break;
    }
```

```plaintext:YieldReturnCoroutine
メインコルーチン開始 :  1
YieldReturnCoroutine開始 :  1
SubCoroutine開始 : 1
SubCoroutine終了 : 1
YieldReturnCoroutine終了 : 2
```

```plaintext:MoveNextCoroutine
メインコルーチン開始 :  1
MoveNextCoroutine開始 :  1
SubCoroutine開始 : 1
SubCoroutine終了 : 1
MoveNextCoroutine終了 : 1
```

# Coroutineの止め方
- StopCoroutineだけだと破棄されない
    - 一度null埋めをすることで破棄される
    - null埋めしなかった場合止めた部分から再開される
- 開始したMonoBehaviorからしか止められない
- GameObjectが非アクティブになったタイミングでCoroutineも止まる
    - 非アクティブなGamerObjectでは開始もできない
- startしたときと同じ記法でないと止めることができない
    - 参考記事：[【Unity】StopCoroutineメソッドに潜まれた罠](https://www.hanachiru-blog.com/entry/2019/07/17/181223)


## Coroutineのnull埋め検証
```c#
    private IEnumerator mainCoroutine;
    
    void Start ()
    {
        Debug.Log($"メインコルーチン開始");
        mainCoroutine = MainCoroutine();
        StartCoroutine(mainCoroutine);
        // 任意のサブコルーチンを実行
        StartCoroutine(SubCoroutineB());
    }

    private IEnumerator MainCoroutine()
    {
        Debug.Log("MainCoroutine 1");
        yield return null;
        Debug.Log("MainCoroutine 2");
        yield return null;
        Debug.Log("MainCoroutine 3");
    }
    
    private IEnumerator SubCoroutineA()
    {
        yield return null;
        StopCoroutine(mainCoroutine);
        Debug.Log("Stop MainCoroutine");
        yield return mainCoroutine;
    }
    
    private IEnumerator SubCoroutineB()
    {
        yield return null;
        StopCoroutine(mainCoroutine);
        mainCoroutine = null;
        Debug.Log("Stop MainCoroutine");
        mainCoroutine = MainCoroutine();
        yield return mainCoroutine;
    }
```

```plaintext:SubCoroutineA
メインコルーチン開始
MainCoroutine 1
MainCoroutine 2
Stop MainCoroutine
MainCoroutine 3
```

```plaintext:SubCoroutineB
メインコルーチン開始
MainCoroutine 1
MainCoroutine 2
Stop MainCoroutine
MainCoroutine 1
MainCoroutine 2
MainCoroutine 3
```

## Coroutineの実行者の検証
```c#
    private IEnumerator mainCoroutine;
    
    private MonoBehaviour cachedCoroutineOwner;
    
    void CoroutineStart(MonoBehaviour coroutineOwner)
    {
        Debug.Log($"メインコルーチン開始");
        mainCoroutine = MainCoroutine();
        coroutineOwner.StartCoroutine(mainCoroutine);
        // 任意のサブコルーチンを実行
        StartCoroutine(SubCoroutineB());
        cachedCoroutineOwner = coroutineOwner;
    }

    private IEnumerator MainCoroutine()
    {
        Debug.Log("MainCoroutine 1");
        yield return null;
        Debug.Log("MainCoroutine 2");
        yield return null;
        Debug.Log("MainCoroutine 3");
    }
    
    private IEnumerator SubCoroutineA()
    {
        yield return null;
        StopCoroutine(mainCoroutine);
        Debug.Log("Stop MainCoroutine");
    }
    
    private IEnumerator SubCoroutineB()
    {
        yield return null;
        cachedCoroutineOwner.StopCoroutine(mainCoroutine);
        Debug.Log("Stop MainCoroutine");
    }
    
```

```plaintext:SubCoroutineA
MainCoroutine 1
MainCoroutine 2
Stop MainCoroutine
MainCoroutine 3
```

```plaintext:SubCoroutineB
MainCoroutine 1
MainCoroutine 2
Stop MainCoroutine
```

# その他
- Coroutineから戻り値を受け取ることもできる
    - 明示的に型変換する必要あり
- StopAllCoroutineでMonoBehaviorが保持しているCoroutineをすべて止めることができる
    - StartをIEnumratorにしていたらそれも止まるので注意
- DoTweeenの終わりを待つ方法
    - 参考記事：[コルーチンやTaskでDOTweenを待機する方法](https://zenn.dev/ohbashunsuke/books/20200924-dotween-complete/viewer/22)

## Coroutineから戻り値を受け取る
```c#
    private void CoroutineStart()
    {
        StartCoroutine(MainCoroutine());
    }

    private IEnumerator MainCoroutine()
    {
        IEnumerator subCoroutine = SubCoroutine();
        yield return subCoroutine;
        Debug.Log($"subCoroutine.Current : {(string)subCoroutine.Current}");
    }

    private IEnumerator SubCoroutine()
    {
        yield return "SubCoroutine is End";
    }
```

```plaintext
subCoroutine.Current : SubCoroutine is End
```

# 参考記事
- [【Unity】 StartCoroutine以外でコルーチンを回す方法](http://posposi.blog.fc2.com/blog-entry-227.html)
- [【Unity】StopCoroutineメソッドに潜まれた罠](https://www.hanachiru-blog.com/entry/2019/07/17/181223)
- [コルーチンやTaskでDOTweenを待機する方法](https://zenn.dev/ohbashunsuke/books/20200924-dotween-complete/viewer/22)

# 終わりに
今回はCoroutineのつまづきやすいところをまとめてみました。最近は非同期処理でCoroutineを採用することは少ないですが、ちょっとした個人開発などではまだまだ現役かと思いますので参考になれば幸いです。