Unity timeScale

Unity timeScale – Managing time

Unity’s timeScale defines the speed at which the game runs.
With a value of 1, time in the game flows as usual; at 0.5, it’s twice as slow; at 2, it’s twice as fast.

This parameter allows you to make slow-motion effects, acceleration, or pausing the game.

How to modify timeScale on Unity?

You can edit the timeScale using the following code: Time.timeScale = 0.5f;
In this example, game speed is divided by two.

Its value must not be negative.

Unity timescale demonstration with physicstimeScale demonstration on physics

However, it’s important to note that timeScale only applies to some Unity methods.

What does timeScale do in Unity?

TimeScale acts on different systems in Unity:

  • Animations
  • Physics
  • Invoke and InvokeRepeating methods.
  • It influences the Time.deltaTime property.
  • Coroutines with the WaitForSeconds and WaitForFixedUpdate methods.
  • FixedUpdate call frequency: the call stops when timeScale is 0.
  • Coroutines with WaitForFixedUpdate pause if timeScale equals 0.

Note that timeScale does not influence the frequency of Update function calls or the use of coroutine with yield return null.

However, timeScale affects Time.deltaTime whose value varies.
Time.deltaTime becomes equal to 0 with a timeScale of 0.

I recommend using Time.deltaTime for time-based calculations to be frame-dependent and timeScale-sensitive.

It’s worth noting that the value of Time.fixedDeltaTime does not move as a function of timeScale.
The timeScale only influences the number of FixedUpdate calls.

To sum up, the timeScale acts on all parts of your game that are frame-dependent.

Pausing your Unity game with timeScale

TimeScale is the ideal candidate for pausing your game.
We need to set its value to 0: Time.timeScale = 0f;

Once again, the Update and Coroutine methods with yield return null continue to work.
Using Time.deltaTime makes our animations immobile (as we multiply by 0).

However, for some performance-intensive calculations, it might be interesting to avoid calculations with a condition :

private void Update()
{
	if (Time.timeScale == 0)
	{
		return;
	}
	
	// expensive calcul
}

Setting the timeScale to 0 when pausing a simple game works.

However, today’s games offer animations and effects, even while players pause the game.

Make way for our friend unscaledTime to solve this problem.

I’ve used pausing the game as an example, but all the functions presented below allow you not to depend on timeScale, whatever its value.

Coroutine with WaitForSecondsRealtime

To continue running our coroutines without taking timeScale into account, we use the WaitForSecondsRealtime function instead of WaitForSeconds.

private void Start()
{
	StartCoroutine(Animation());
}

private IEnumerator Animation()
{
	while(true)
	{
		// rotate object each 2.5 seconds
		Rotate();
		yield return new WaitForSecondsRealtime(2.5f);
	}
}

Animation with unscaledTime

To make our animations work independently of the timeScale, we associate the value “Unscaled Time” with the property “updateMode” of our animator.

Unity animator change updateMode to unscaledTime

Here’s how to do it by script:

private void Start()
{
	GetComponent<Animator>().updateMode = AnimatorUpdateMode.UnscaledTime;
}

Update function with Time.unscaledDeltaTime

To continue animating using an Update method without depending on timeScale, we can use Time.unscaledDeltaTime instead of Time.deltaTime.

private void Update()
{
	transform.Translate(Vector3.down * Time.unscaledDeltaTime);
}

Using physics with a paused game (timeScale = 0)

To use Unity’s physics when our timeScale is 0.
We have to go through several steps:

  1. Create a scene with your physics objects inside.
  2. Add your scene in the build settings.
  3. After the code that pauses the game, add the code to load the scene in additive mode with its local physics.
  4. Then, you switch the physics simulation to SimulationMode.Script to get the manual control.
  5. Simulate physics in a coroutine with Simulate, coupled with WaitForSecondsRealtime.
private Scene? _sceneMenu = null;

private Coroutine _physicsCoroutine = null;

private bool _isPause = false;

// Toggle function pause / unpause
private void SetPause()
{
	_isPause = !_isPause;

	Time.timeScale = _isPause ? 0f : 1f;

	if (_isPause)
	{
		// SceneParameter is mandatory, load phyMenu scene
		var operation = SceneManager.LoadSceneAsync("phyMenu", new LoadSceneParameters(LoadSceneMode.Additive, LocalPhysicsMode.Physics3D));

		operation.completed += (operationCompleted) =>
		{
			_sceneMenu = SceneManager.GetSceneByName("phyMenu");

			// If the scene has just loaded but we're no longer paused, we delete it directly.
			// Safety for click spammers
			if (!_isPause)
			{
				RemoveSceneMenu();
				return;
			}

			// Manual physics
			Physics.simulationMode = SimulationMode.Script;
			
			// Coroutine will simulate
			_physicsCoroutine = StartCoroutine(SimulatePhysics());
		};
		return;
	}

	// end pause, we remove the additive scene
	RemoveSceneMenu();
}

private IEnumerator SimulatePhysics()
{
	while(true)
	{
		// Physics run with a timeScale at 0
		_sceneMenu.Value.GetPhysicsScene().Simulate(Time.fixedDeltaTime);
		yield return new WaitForSecondsRealtime(Time.fixedDeltaTime);
	}
}

private void RemoveSceneMenu()
{
	if (!_sceneMenu.HasValue)
	{
		return;
	}

	// stop physics coroutine
	if (_physicsCoroutine != null)
	{
		StopCoroutine(_physicsCoroutine);
	}

	// back in default simulation mode
	Physics.simulationMode = SimulationMode.FixedUpdate;

	// remove the scene
	// todo, check Unload is completed before stopping the pause. 
	SceneManager.UnloadSceneAsync("phyMenu");

	_sceneMenu = null;
}

Leave a Reply

Your email address will not be published. Required fields are marked *