Time.timeScale=0; にしても1フレだけ FixedUpdate()が呼ばれることがある

Unity でゲームをポーズするには一般的に Time.timeScale = 0; とします。すると FixedUpdate()は呼ばれず、物理エンジンの時間が止まります。 Update() は呼ばれますが Time.deltaTime == 0 となります。よって、ゲームの動きを FixedUpdate()内で行うか、または Update()内で Time.deltaTime を動きの速さに掛け算して行えば、 Time.timeScale = 0; にすることでゲームのポーズを実現することができます。


https://docs.unity3d.com/ja/2018.4/ScriptReference/Time-timeScale.html

Unityドキュメントにも以下のように書かれています。
> TimeScale がゼロに設定されている場合、FixedUpdate 関数は呼び出されません。


しかし Time.timeScale = 0; にするタイミングによっては、 **Time.timeScale == 0 にもかかわらず FixedUpdate() が呼ばれたり物理エンジンが1フレームだけ動いたりします**。

今回私がハマったのはこの点です。


# 結論
Time.timeScale = 0; にしたのが OnEnable() や FixedUpdate() なら、その直後の 1フレームは FixedUpdate() や物理エンジンが動く。

# 実験環境
バージョン Unity 2023.2.11f1
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747648/c77b8985-d9cb-e5ae-328e-a9e8f8bfa90a.png)

GameObject *MySphere* には Rigidbody と _MySphere_ (後述) コンポーネントが追加されています。MySphere コンポーネントは球体を往復運動させます。

GameObject *GameMain* には _GameMain_ (後述) コンポーネントが追加されています。GameMain コンポーネントはキー操作に応じていろいろなタイミングでポーズをかけます。

また Script Execution Order を設定して、 GameMain スクリプトは MySphere よりも前に実行されるようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747648/769fc7f9-8a4a-8a50-0645-dfe7a5c5bd5b.png)


## MySphere コンポーネント
```cs:MySphere.cs
using UnityEngine;

// Rigidbody を往復運動させる。デバッグ表示もする。
public class MySphere : MonoBehaviour
{
    Rigidbody _rigidbody;

    void OnEnable()
    {
        _rigidbody = GetComponent<Rigidbody>();
        print($"初期位置[{GameMain.FixedUpdateCount}]*{Time.timeScale} {_rigidbody.position}");

        // 初速度
        _rigidbody.velocity = Vector3.right * 10;
    }

    void FixedUpdate()
    {
        if (Time.timeScale == 0)
        {
            print($"FixedUpdate[{GameMain.FixedUpdateCount}]*{Time.timeScale} {_rigidbody.position}");
        }

        // 往復運動させる
        if (Mathf.Abs(_rigidbody.position.x) > 5)
        {
            _rigidbody.velocity *= -1;
        }
    }

    void Update()
    {        
        if (Time.timeScale == 0)
        {
            print($"Update[{GameMain.FixedUpdateCount}]*{Time.timeScale} {_rigidbody.position} d={Time.deltaTime}");
        }
    }
}
```



## GameMain コンポーネント
```cs:GameMain.cs
using UnityEngine;

// キー入力などによってポーズをかけるもの。
// シーン初期化時(OnEnable)にポーズ
// Escキーを押すと FixedUpdate()でポーズ
// Pキーを押すと Update()でポーズ
// スペースキーでポーズ解除
public class GameMain : MonoBehaviour
{
    public static int FixedUpdateCount;

    bool _requestPause;

    void OnEnable()
    {
        FixedUpdateCount = 0;
        _requestPause = false;

        print($"[{FixedUpdateCount}] シーン初期化時にポーズ");
        Time.timeScale = 0;
    }

    void FixedUpdate()
    {
        if (_requestPause)
        {
            _requestPause = false;
            print($"[{FixedUpdateCount}] FixedUpdateでポーズ");
            Time.timeScale = 0;
        }

        FixedUpdateCount++;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            print($"[{FixedUpdateCount}] Updateでポーズ解除");
            Time.timeScale = 1;
            _requestPause = false;
        }

        if (Input.GetKeyDown(KeyCode.Escape))
        {
            _requestPause = true;
        }

        if (Input.GetKeyDown(KeyCode.P))
        {
            print($"[{FixedUpdateCount}] Updateでポーズ");
            Time.timeScale = 0;
        }
    }
}
```

# 実験結果

## シーン初期化時
Unity Editor で Play すると Console出力は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747648/6ea9fdab-0101-2cc5-ba72-e479ea44f610.png)

GameMain.OnEnable() ← ここでポーズしてるつもり
MySphere.OnEnable()
GameMain.FixedUpdate() // FixedUpdateCount++ されたのでこれが呼ばれたとわかる
MySphere.FixedUpdate()
MySphere.Update() (Time.deltaTime=0.01999999)
MySphere.Update() (Time.deltaTime=0)

の順に呼ばれていることがわかります。しかも MySphere.FixedUpdate() と MySphere.Update() で _rigidbody.position が違う値になったので、物理エンジンも動いたことがわかります。

## Updateでポーズ
Play して、スペースキーを押してポーズ解除し、1秒ほど経ってから Pキーを押しました。GameMain.Update()の中で Time.timeScale = 0; にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747648/2b69cbfc-0674-72b8-fb46-bbd45d95a316.png)

この場合、FixedUpdate() は呼ばれず、物理エンジンも止まっているようです。

## FixedUpdateでポーズ
Play して、スペースキーを押してポーズ解除し、1秒ほど経ってから Escキーを押しました。
この場合、 GameMain.Update() で Escキー押下を検知して _requestPause=true; にし、次のフレームの GameMain.FixedUpdate() で Time.timeScale = 0; にしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747648/ab2b5b50-6566-6767-3e9d-bc275ace4c0b.png)

GameMain.Update()
+--------(次のフレーム)--------+
GameMain.FixedUpdate() ← Time.timeScale=0;
MySphere.FixedUpdate()
MySphere.Update() (Time.deltaTime=0.004245302)
MySphere.Update() (Time.deltaTime=0)

の順に呼ばれていることがわかります。しかも MySphere.FixedUpdate() と MySphere.Update() で _rigidbody.position が違う値になったので、物理エンジンも動いたことがわかります。


# 考察

https://docs.unity3d.com/ja/2018.4/Manual/ExecutionOrder.html

実験結果をこの図と比べると、図の薄い灰色背景に囲まれた Physics (右側) から上で Time.timeScale = 0; にしても即座に効果は出ず、それより後の OnMouseXXX, Update などから Time.timeScale = 0; にすれば即座に効くのではないか、と推測します。