Unity でゲームをポーズするには一般的に Time.timeScale = 0; とします。すると FixedUpdate()は呼ばれず、物理エンジンの時間が止まります。 Update() は呼ばれますが Time.deltaTime == 0 となります。よって、ゲームの動きを FixedUpdate()内で行うか、または Update()内で Time.deltaTime を動きの速さに掛け算して行えば、 Time.timeScale = 0; にすることでゲームのポーズを実現することができます。
Unityドキュメントにも以下のように書かれています。
TimeScale がゼロに設定されている場合、FixedUpdate 関数は呼び出されません。
しかし Time.timeScale = 0; にするタイミングによっては、 Time.timeScale == 0 にもかかわらず FixedUpdate() が呼ばれたり物理エンジンが1フレームだけ動いたりします。
今回私がハマったのはこの点です。
結論
Time.timeScale = 0; にしたのが OnEnable() や FixedUpdate() なら、その直後の 1フレームは FixedUpdate() や物理エンジンが動く。
実験環境
GameObject MySphere には Rigidbody と MySphere (後述) コンポーネントが追加されています。MySphere コンポーネントは球体を往復運動させます。
GameObject GameMain には GameMain (後述) コンポーネントが追加されています。GameMain コンポーネントはキー操作に応じていろいろなタイミングでポーズをかけます。
また Script Execution Order を設定して、 GameMain スクリプトは MySphere よりも前に実行されるようにしています。
MySphere コンポーネント
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 コンポーネント
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出力は以下のようになります。
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; にします。
この場合、FixedUpdate() は呼ばれず、物理エンジンも止まっているようです。
FixedUpdateでポーズ
Play して、スペースキーを押してポーズ解除し、1秒ほど経ってから Escキーを押しました。
この場合、 GameMain.Update() で Escキー押下を検知して _requestPause=true; にし、次のフレームの GameMain.FixedUpdate() で Time.timeScale = 0; にしています。
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 が違う値になったので、物理エンジンも動いたことがわかります。
考察
実験結果をこの図と比べると、図の薄い灰色背景に囲まれた Physics (右側) から上で Time.timeScale = 0; にしても即座に効果は出ず、それより後の OnMouseXXX, Update などから Time.timeScale = 0; にすれば即座に効くのではないか、と推測します。