Keep Your Framerate Out of My Physics - Part I

Never let your physics simulation depend on something as unreliable and capricious as the machine’s framerate.

When I was building Electric Field Hockey I used window.requestAnimationFrame to control the time-step for all the physics calculations.

Simulation Tick Function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Simulation.prototype.tick = function(frame) {
  var ts = frame.timeDiff / 1000;
  var puck = this.puck;
  var force = puck.charge.calcForceFrom( this.charges );
  var newPosition = EFH.Physics.calcPosition(puck.charge,
              puck.velocity, force, ts);
  puck.velocity = EFH.Physics.calcVelocity(puck.velocity,
              force, ts);

  puck.moveTo(newPosition.x, newPosition.y);

};

var init = function() {
  /* SNIP */
  self.anim = new Kinetci.Animation(function(frame) {
      self.tick( frame );
  });
};

It’s easy and straight forward: every time the browser wanted a new frame, we can calculate the forces, apply them to the objects, and figure out the new positions.

But it’s also simplistic.

When the simulation involves many charges, both positive and negative, the proper motion of the puck will often be curved. However, using this algorithm, we notice a problem. Each run of the simulation yields a different result: the puck curves at a different radius each time. Sometimes it cuts sharp and fast; sometimes it swings out wide and slow.

An inconsistent framerate is leaking into the physics. Suddenly the mathematical models break down and what should be pristine, deterministic calculations appears to become wild and unpredictable.

Our algorithm is broken.

The renderer, Kinetic.Animation, which is a wrapper around window.requestAnimationFrame, produces a timestep which our physics engine “consumes” to produce the next position. Since the forces are only recalculated at an interval, the engine estimates the next position by pretending the puck moves linearly between steps: in a straight line at constant velocity.

If the renderer produces (by requesting a new frame) a 16.667ms timestep (60fps), the simulation calculates the state of the world if everything kept moving linearly for 16.667ms. If the computer hiccups and drops to 30fps, then the physics assumes everything moved linearly for 33.333ms. This will have dramatic consequences if the object is supposed to be moving along a curve.

At 60fps, the puck curves dramatically as we recalculate the forces more frequently.
With a larger time step, it moves farther between calculations and curves gradually.

Non-deterministic physics make for a terrible game experience.

We need a scheme where we can enforce a constant simulation rate, no matter what happens with the framerate. Depending on requestAnimationFrame has proved unreliable. But what about other options like setTimeout or setInterval? Ultimately they fail for the same reasons: they do not guarantee we will get an event in perfect intervals.

We need something better, which we will begin to develop next time.

Comments