【Unity】Input.GetMouseButtonDown()などのフラグがフレーム内のどのタイミングで更新されるか調べた

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">GetKeyDownとかGetButtonUpとかがtrueになるタイミングの遷移について <a href="https://t.co/TmLfYhMJEA">https://t.co/TmLfYhMJEA</a> <a href="https://twitter.com/hashtag/Qiita?src=hash&amp;ref_src=twsrc%5Etfw">#Qiita</a> <br><br>これもっと具体的にtrueになるタイミングって1フレーム内のどのタイミングなんだろうか？🤔</p>&mdash; su10@ハイパーカジュアルゲーム開発 (@su10_dev) <a href="https://twitter.com/su10_dev/status/1344973819333595141?ref_src=twsrc%5Etfw">January 1, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# はじめに
`Input.GetMouseButtonDown()` とかを `IObservable` にしたいなと思って

* [キー入力、マウス入力をIObservable<T>に変換して使う](https://qiita.com/Euglenach/items/29833548294a8b822b79)

の記事を読んで

*  `Observable.EveryUpdate()` のタイミングで値がpushされるのがベストかはわからない
* できれば使う側が値のpushタイミングを選べたほうが良さそう
*  `Input.GetMouseButtonDown()` みたいに好きなときにフラグを確認したい
* `IReadOnlyReactiveProperty<bool>` を公開＆ `BatchFrame()` で使う側がpushタイミングを選べればいいのでは？

と思ったのですが、 `ReactiveProperty` のフラグを更新するベストなタイミングが１フレームの中のどこなのかわからなかったので調べました。

# 確認環境

* macOS Catalina（10.15.6）
* Unity2019.4.16

にて確認。

# `Input.GetMouseButtonDown()` の値が更新されるタイミング
以下のスクリプトを使って同一のフレーム内での値の変化を観察しました。 `UniTask.Yield()` を使うとUnityのプレイヤーループの中に独自の実行タイミングを挟み込めるのでそれを使ってます。

また、[UniTaskのREADME](https://github.com/Cysharp/UniTask)によると

>await UniTask.WaitForEndOfFrame() is not equilavelnt to coroutine's yield return new WaitForEndOfFrame(). Coroutine's WaitForEndOfFrame seems to run after the PlayerLoop is done. Some methods that require coroutine's end of frame(ScreenCapture.CaptureScreenshotAsTexture, CommandBuffer, etc) does not work correctly when replace to async/await. In that case, use a coroutine.

とのことなのでコルーチンの `EndOfFrame` のタイミングでの値も合わせて確認しています。

```InputTest.cs
using System.Collections;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class InputTest : MonoBehaviour
{
    private bool flag => Input.GetMouseButtonDown(0);

    IEnumerator Start()
    {
        this.MainLoopTask(this.GetCancellationTokenOnDestroy()).Forget();

        while (true)
        {
            yield return new WaitForEndOfFrame();
            Debug.Log($"EndOfFrame: {flag}");
        }
    }

    private async UniTask MainLoopTask(CancellationToken ct)
    {
        while (ct.IsCancellationRequested == false)
        {
            await UniTask.Yield(PlayerLoopTiming.Initialization, ct);
            Debug.LogWarning($"*** frame: {Time.frameCount} ***");
            Debug.Log($"Initialization: {flag}");

            await UniTask.Yield(PlayerLoopTiming.LastInitialization, ct);
            Debug.Log($"LastInitialization: {flag}");

            await UniTask.Yield(PlayerLoopTiming.EarlyUpdate, ct);
            Debug.Log($"EarlyUpdate: {flag}");

            await UniTask.Yield(PlayerLoopTiming.LastEarlyUpdate, ct);
            Debug.Log($"LastEarlyUpdate: {flag}");

            await UniTask.Yield(PlayerLoopTiming.PreUpdate, ct);
            Debug.Log($"PreUpdate: {flag}");

            await UniTask.Yield(PlayerLoopTiming.LastPreUpdate, ct);
            Debug.Log($"LastPreUpdate: {flag}");

            await UniTask.Yield(PlayerLoopTiming.Update, ct);
            Debug.Log($"Update: {flag}");

            await UniTask.Yield(PlayerLoopTiming.LastUpdate, ct);
            Debug.Log($"LastUpdate: {flag}");

            await UniTask.Yield(PlayerLoopTiming.PreLateUpdate, ct);
            Debug.Log($"PreLateUpdate: {flag}");

            await UniTask.Yield(PlayerLoopTiming.LastPreLateUpdate, ct);
            Debug.Log($"LastPreLateUpdate: {flag}");

            await UniTask.Yield(PlayerLoopTiming.PostLateUpdate, ct);
            Debug.Log($"PostLateUpdate: {flag}");

            await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate, ct);
            Debug.Log($"LastPostLateUpdate: {flag}");
        }
    }
}
```

## 結果
```
...
LastPreLateUpdate: False
PostLateUpdate: False
LastPostLateUpdate: False
EndOfFrame: False
*** frame: 28 ***
Initialization: True
LastInitialization: True
EarlyUpdate: True
LastEarlyUpdate: True
PreUpdate: True
LastPreUpdate: True
Update: True
LastUpdate: True
PreLateUpdate: True
LastPreLateUpdate: True
PostLateUpdate: True
LastPostLateUpdate: False
EndOfFrame: False
*** frame: 29 ***
Initialization: False
LastInitialization: False
EarlyUpdate: False
...
```

何回か適当にクリックして他のタイミングのログも見ましたが、 `true/false` の切り替わるタイミングはどれも同じで `PostLateUpdate ` と `LastPostLateUpdate ` の間でした。

# `Input.GetMouseButton()` の値が更新されるタイミング
せっかくなので「押しっぱなしの間 `true` 」になる方の値も確認してみます。 `flag` プロパティをちょっとだけいじりました。

```InputTest.cs
..
public class InputTest : MonoBehaviour
{
    private bool flag => Input.GetMouseButton(0); // Downを消した
..
```

## 結果
```
..
LastPostLateUpdate: False
EndOfFrame: False
*** frame: 65 ***
Initialization: True
LastInitialization: True
EarlyUpdate: True
LastEarlyUpdate: True
PreUpdate: True
LastPreUpdate: True
Update: True
LastUpdate: True
PreLateUpdate: True
LastPreLateUpdate: True
PostLateUpdate: True
LastPostLateUpdate: True
EndOfFrame: True
*** frame: 66 ***
Initialization: True
LastInitialization: True
EarlyUpdate: True
LastEarlyUpdate: True
PreUpdate: True
LastPreUpdate: True
Update: True
LastUpdate: True
PreLateUpdate: True
LastPreLateUpdate: True
PostLateUpdate: True
LastPostLateUpdate: True
EndOfFrame: True
*** frame: 67 ***

..

LastPostLateUpdate: True
EndOfFrame: True
*** frame: 101 ***
Initialization: True
LastInitialization: True
EarlyUpdate: True
LastEarlyUpdate: True
PreUpdate: True
LastPreUpdate: True
Update: True
LastUpdate: True
PreLateUpdate: True
LastPreLateUpdate: True
PostLateUpdate: True
LastPostLateUpdate: True
EndOfFrame: True
*** frame: 102 ***
Initialization: False
LastInitialization: False
EarlyUpdate: False
..
```

今度はきれいに「フレームの最初から最後まで `true/false` 」で揃いました。

# Unityのプレイヤーループで何が起きているのか
* [neuecc/PlayerLoopList.md](https://gist.github.com/neuecc/bc3a1cfd4d74501ad057e49efcd7bdae)

↑にリストがあって `Input` とついてるのがいくつかありますが、後ろの方の

* InputEndFrame
* ResetInputAxis

あたりでフラグを更新してそうです。これらの[イベントを無効化](http://tsubakit1.hateblo.jp/entry/2018/04/17/233000)すればさらに詳細な挙動がわかりそうですが、面倒なのでここまでにしておきます。

# 結論
`Input.GetMouseButtonDown()` と `Input.GetMouseButton()` でなぜか値の更新タイミングが異なるのが気になりますが、「１フレームの最初に値が決まって最後まで値は変わらない」とみなしてよさそうでした。

# 参考
* [GetKeyDownとかGetButtonUpとかがtrueになるタイミングの遷移について](https://qiita.com/RyotaMurohoshi/items/a5cde3c17831adda12db)
  * もうちょっと大雑把なタイミングでの値の変化の話