Physics update loop

As you’re already familiar with, the illusion of movement in games and movies is achieved by displaying many images (frames) per second in quick succession. Persistence of vision (our brain and eye’s ability to retain a visual impression for about 1/30th of a second) allows us to perceive this stream of images as continuous motion.

Unlike movies, games have to generate (render) each frame on the fly since the player is able to affect what’s happening on the screen as the game progresses. The content of each frame and the amount of computing power required to render it is different - some frames are more complex than others - so in an actual game not every frame has the same duration: the amount of frames displayed per second (FPS) fluctuates over time.

Making objects move requires updating our game’s physics. The obvious choice is to update physics once per frame, advancing the state or our simulation forward in time as many seconds as it took to render the previous frame. Object trajectories are only re-evaluated during updates, so objects are assumed to move in a straight line from one update to the next.

This piece-wise linear approximation of object trajectories becomes more accurate when the amount of time the simulation is advanced by with each update is small, and less so when advanced a lot of time. So allowing each update to advance the simulation a different amount of time will lead to wobbly object trajectories, as the accuracy of the simulation changes constantly. Let's illustrate this with a projectile following a ballistic trajectory:

Advancing physics a different amount of seconds each time leads to erratic object movement.
Advancing physics the same amount of seconds every time results in smooth, predictable trajectories.

The takeaway is that conflating physics with frame rendering leads to weird, inconsistent behavior unless we can guarantee rock-solid FPS. But this is seldom the case as we can’t control how well our game will perform on all devices that may possibly run it, so how do we reconcile physics with the fact that each frame may require a different amount of time to be generated and displayed on a screen?

Fixed timestepping

One common solution - and the one Unity uses - is to decouple rendering and physics: record how much time it took to render the previous frame, and “chop” this amount of time into chunks of equal duration. Then, we update physics as many times as chunks we have.

Each of these equally sized chunks is called a timestep, and we refer to their duration as the timestep length/size (Time.fixedDeltaTime in Unity). This timestepping scheme is called fixed timestepping - because each timestep moves the simulation forward a fixed amount of time.

Let’s see a practical example: suppose the last frame took 16 milliseconds to send to the screen. If using a timestep length of 6 ms, each physics update moves the simulation 6 ms forward so we have to update physics 16/6 = 2.6 times this frame so that the same amount of time has passed in the simulation as it has in the real world. This is a fractional amount of updates, since we can’t do partial updates (we either update physics or not) we just round this down to an integer number (2) and accumulate the remaining time (16 - 6x2 = 4 ms) for the next frame. This is how this would look in pseudocode:

float accumulator = 0;

DoFrame(float deltaTime)
{
	// accumulate the duration of the last frame:
	accumulator += deltaTime;

	// while there’s at least one timestep worth of time accumulated, do a timestep.
	while (accumulator >= fixedDeltaTime)
	{ 
		// Unity calls FixedUpdate() here, right before updating physics.
		UpdatePhysics(fixedDeltaTime); 
		accumulator -= fixedDeltaTime;
	}

	// Unity calls Update() here, after all time steps have been performed.
}

By doing this we accomplish our goal of robust physics even when the amount of frames rendered per second fluctuates. Looking at the above code, it's obvious physics may be updated zero, one or multiple times during a frame. In the following example, frame duration varies between 0.1 and 0.3 seconds. Timestep length is 0.2 seconds. As a result, some frames we get no timestep, some frames we get one, and some other frames we get two timesteps. Regardless of this, the trajectory of our object is consistent and predictable:

We use fixed timestepping for simulation consistency, but it also makes the simulation's temporal resolution independent of FPS, allowing it to be finer or coarser. So if we want better quality physics, we can just reduce our timestep length (fixedDeltaTime). Conversely, if we want to spend less resources on physics and can live with lower quality output, we can increase our timestep length. Being able to tune simulation quality without having to worry about consistency is very convenient.

Interpolation

You may have noticed a negative consequence of fixed timestepping: when the duration of a frame is not an exact multiple of the timestep length, we will be rendering objects at a position that’s slightly behind where they should be. Worse still, if our timestep length is larger than the duration of the frame we won’t update physics at all during that frame, which leads to a stop-motion effect as objects stay where they are until the next timestep.

In the following example every frame takes 0.1 seconds, but timestep length is 0.4. So the object only moves every 4 frames:

There’s a couple ways to get smoother motion under such circumstances: one is to extrapolate object positions forward in time, that is, move the object in a straight line towards its predicted position at the end of the frame. The downside to this is that if our prediction is wrong (and it often will be since we can only use available information such as current position/velocity), the object will abruptly move from the predicted position to the correct one once the next timestep takes place which may be visually jarring.

Another alternative is using interpolation: we take the current and previous position of the object, and interpolate between them based on the ratio between the accumulated value and the duration of a full timestep. This introduces a delay in visuals with respect to physics, but ensures smooth motion regardless of timestep and frame duration.

In the following example the blue object is moved every frame to the interpolated position. As you can see it's slightly behind the position calculated by the physics system - at most one timestep behind - but its motion is much smoother:

Rigidbodies in Unity and solvers in Obi both allow you to use interpolation and extrapolation to smooth out simulation results.

The Death Spiral

There's one remaining issue, one with an ominous name and even more ominous effects: death spiralling.

Take a look at the fixed timestepping pseudocode above one last time: what would happen if the UpdatePhysics() call was very expensive? that would cause the duration of this frame to increase... so next frame, accumulator could become much larger than fixedDeltaTime and we'd be stuck in the while loop for quite some time, doing multiple calls to our expensive UpdatePhysics()! That means accumulator would grow even more next frame, and we'd spend more time in the while loop. The amount of calls to UpdatePhysics() would multiply non-stop and performance would keep going downhill until the game eventually froze itself to death.

Here's it in action: each frame takes longer that the previous one to complete, and as a result, the next one needs to perform more timesteps which worsens the situation. Every frame, the game gets slower until it slows down to a crawl:

There's a few solutions to this. Simplest one - and also the one used by Unity - is to just put a limit to how large accumulator can become, allowing the game to advance in hopes of UpdatePhysics() becoming cheaper at some point and getting ourselves out of the spiral:

// clamp the accumulator to a maximum value:
accumulator = min(accumulator, maximumAllowedTimestep);

This results in robust simulation that can't completely freeze the game. The downside is that whenever accumulator exceeds maximumAllowedTimestep we are throwing away time, so less time passes in the simulation that in the real world and objects will appear to move in slow-motion.

Obi also uses a similar approach, allowing you to limit the amount of iterations spent on the while loop. This is the solver's max steps per frame parameter.

Bolting Obi onto Unity's timestepping

So, how does Obi hook itself onto Unity's fixed timestepping scheme? Obi is a multithreaded physics engine, which means that simulation may be performed in parallel with other tasks needed to complete the frame. For this reason solvers in Obi do not have a single UpdatePhysics() method, but two separate methods to start and complete the simulation:

StartSimulation:
Kicks off the simulation of a timestep. This may be performed on multiple threads.

CompleteSimulation:
Forces the main thread to wait for the previous StartSimulation() call to complete, if any. Then it setups any data required for rendering.

These are called at different times during Unity's frame, depending on the solver's synchronization mode. Here's pseudocode for what all 3 synchronization modes do:

Asynchronous mode

In this mode, simulation is started at the end of the frame once Unity has performed all timesteps. The main thread waits for the simulation to complete on a future frame before starting another timestep. This allows the simulation to run while this frame's rendering is taking place, which improves performance since the main thread usually doesn't have to wait on CompleteSimulation(). This however means we're always rendering simulation results of a previous frame, instead of the current's.

float accumulator = 0;

DoFrame(float deltaTime)
{
	// accumulate the duration of the last frame:
	accumulator += deltaTime;

	int steps = accumulator / fixedDeltaTime;
	if (steps > 0)
		CompleteSimulation();

	// while there’s at least one timestep worth of time accumulated, do a timestep.
	while (accumulator >= fixedDeltaTime)
	{ 
		// FixedUpdate()
		UpdatePhysics(fixedDeltaTime); 
		accumulator -= fixedDeltaTime;
	}

	// Update()

	if (steps > 0)
		StartSimulation(steps);
}

Synchronous mode

In this mode, simulation is started at the end of the frame once Unity has performed all timesteps, then the main thread waits for it to complete immediately after. This is less performant than asyncrhonous mode, but we can render this frame's simulation results with no delay.

float accumulator = 0;

DoFrame(float deltaTime)
{
	// accumulate the duration of the last frame:
	accumulator += deltaTime;

	int steps = accumulator / fixedDeltaTime;

	// while there’s at least one timestep worth of time accumulated, do a timestep.
	while (accumulator >= fixedDeltaTime)
	{ 
		// FixedUpdate()
		UpdatePhysics(fixedDeltaTime); 
		accumulator -= fixedDeltaTime;
	}

	// Update()

	if (steps > 0)
	{
		StartSimulation(steps);
		CompleteSimulation();
	}
}

Synchronous Fixed mode

In this mode, simulation is started and completed right before each timestep. This offers the tightest integration with Unity's own physics engine, but it is the costlier method.

float accumulator = 0;

DoFrame(float deltaTime)
{
	// accumulate the duration of the last frame:
	accumulator += deltaTime;

	// while there’s at least one timestep worth of time accumulated, do a timestep.
	while (accumulator >= fixedDeltaTime)
	{ 
		StartSimulation(1);
		CompleteSimulation();

		// FixedUpdate()
		UpdatePhysics(fixedDeltaTime); 
		accumulator -= fixedDeltaTime;
	}

	//Update()
}