【誤解でした】UniTask.Yield(PlayerLoopTiming.Update) が思ったとおりのタイミングで動いていないようだ

UniTask の GitHub に質問を投げたら Tim Cassell さんが答えてくれて、解決しました。私が誤解していました。記事の最後に追記します。(見出し「概要」～「考察」が最初の疑問で、「解決」が追記です)

# 概要
UniTask.Yield() の引数に enum PlayerLoopTiming を与えると、そのタイミングまで待ってから動作を再開する…と一般には理解されています。例えば

```cs
  await UniTask.Yield(PlayerLoopTiming.Update);
  Debug.Log("OK");
```
と書けば、Update のタイミングで OK が Console に表示されるはずです。

ところが実験したところ、そうでもないような気がします。しかも実行するたびにタイミングが変わるようです。

  
# 環境
Unity 2023.2.11f1
UniTask 2.5.4

# 実験
Scene に "Create Empty" で GameObject を作り、これに MyUniTaskTest コンポーネント (後述のソースコード) を加えてあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747648/0c94ab4d-9fe4-f31d-55a5-5c9c42c87bed.png)


MyUniTaskTest コンポーネントの実装は以下のようになっています。
_phase に現在のタイミングを記録しています。
Update() の中で最初の1回だけ MyTaskAsync() を起動しています。

```cs:MyUniTaskTest.cs
using Cysharp.Threading.Tasks;
using System.Threading;
using UnityEngine;

public class MyUniTaskTest : MonoBehaviour
{
    string _phase;
    bool _done;

    void OnEnable()
    {
        _done = false;
    }

    void FixedUpdate()
    {
        _phase = "FixedUpdate_Begin";
        _phase = "FixedUpdate_End";
    }

    void Update()
    {
        _phase = "Update_Begin";
        if (!_done)
        {
            _done = true;
            MyTaskAsync(destroyCancellationToken).Forget();
        }
        _phase = "Update_End";        
    }

    void LateUpdate()
    {
        _phase = "LateUpdate_Begin";
        _phase = "LateUpdate_End";        
    }

    async UniTaskVoid MyTaskAsync(CancellationToken cancel)
    {
        print($"MyTaskAsync");
        for (int i = 0; i<30; i++)
        {
            print($"{i}: {Time.frameCount} {_phase}");
            await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken: cancel);
        }
    }
}
```

# 結果
これを実行し、 Console に表示されたものを貼り付けます。

## 1回目
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747648/65fdd14e-c696-8821-8a96-c92b173f0289.png)

## 2回目
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747648/7cfea303-79d4-5aa7-3900-42d0099afa66.png)

## 3回目
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747648/f4883e47-5f04-78bc-bb64-cddb51f7c30b.png)

## 表にすると
|   | 1回目         | 2回目          | 3回目         |
|:-:|:-:|:-:|:-:|
| 0 |Update_Begin   |Update_Begin   |Update_Begin   |
| 1 |FixedUpdate_End|FixedUpdate_End|FixedUpdate_End|
| 2 |FixedUpdate_End|FixedUpdate_End|FixedUpdate_End|
| 3 |FixedUpdate_End|FixedUpdate_End|FixedUpdate_End|
| 4 |FixedUpdate_End|LateUpdate_End |LateUpdate_End |
| 5 |FixedUpdate_End|LateUpdate_End |LateUpdate_End |
| 6 |LateUpdate_End |LateUpdate_End |FixedUpdate_End|
| 7 |FixedUpdate_End|FixedUpdate_End|LateUpdate_End |
| 8 |LateUpdate_End |LateUpdate_End |LateUpdate_End |
| 9 |LateUpdate_End |FixedUpdate_End|LateUpdate_End |

# 考察
わかりません。Update のタイミングであれば、_phase の内容は FixedUpdate_End, Update_Begin, Update_End のいずれかだと思います。なぜ LateUpdate_End が表示されることがあるのか。

# 追記
Tim Cassell さんによると、 PlayerLoopTiming.Update は MonoBehaviour の Update よりも前に実行されるそうです。また FixedUpdate は毎フレーム実行されるわけではないので、

第 n フレームの最後まで実行する。変数 _phase には LateUpdate_End が入る。
第 n+1 フレームの実行が始まる。このフレームでは FixedUpdate が呼ばれないとする。`MyUniTaskTest.Update` よりも前に、`MyTaskAsync` の実行が再開される。だから (第 n フレームで変数 _phaseに代入された) LateUpdate_End が表示される。

という順に実行される、ということでした。