Javascript Game Foundations - The Game Loop
Wed, Dec 4, 2013Ten Essential Foundations of Javascript Game Development
- A Web Server and a Module Strategy
- Loading Assets
- The Game Loop
- Player Input
- Math
- DOM
- Rendering
- Sound
- State Management
- Juiciness
The Game Loop
In board games, card games, and text-adventure games, the game updates only in response to player input, but in most video games the state updates continuously over time. Entities move, scores increase, health decreases, and objects fall, speed up, and slow down.
Therefore we need a game loop that can update() and render() our game world.
When running in the browser, all javascript code is executed in a single thread, the UI thread (not counting WebWorkers). Which means we can’t run a naive infinite game loop:
while(true) { // UH OH! - blocks UI thread
update();
render();
}
This would block the UI thread, making the browser unresponsive. Not good.
RequestAnimationFrame
Instead, the browser provides asynchronous mechanisms for us to “do a little work”, then let the browser UI do it’s job, then have it callback to us at a later time to “do a little more work”.
In older browsers, we might have used setInterval
or setTimeout
to call our update
method a
set number of frames per second, but in modern browsers we should be using requestAnimationFrame
to hook into the browser’s native refresh loop:
function frame() {
update();
render();
requestAnimationFrame(frame); // request the next frame
}
requestAnimationFrame(frame); // start the first frame
Timestamps
While requestAnimationFrame
gives us our asynchronous loop, it doesn’t guarantee exactly how
frequently it will execute. In most modern GPU accelerated browsers it will be close to 60fps, but
it might be a little more, might be a little less. If our update or render methods are slow we
might cause it to drop drastically below 60fps.
We cannot assume that 1/60th of a second passes in each frame. Therefore we need to measure
exactly how much time has passed between subsequent iterations of the loop. In modern browsers we
can use the high resolution timer
and in older browsers fallback to using a Date
object:
function timestamp() {
return window.performance && window.performance.now ? window.performance.now() : new Date().getTime();
},
Now that we have a utility method for getting a timestamp (in milliseconds) we can extend our
game loop to provide that information to the update
and render
methods.
var now, dt,
last = timestamp();
function frame() {
now = timestamp();
dt = (now - last) / 1000; // duration in seconds
update(dt);
render(dt);
last = now;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
One additional note is that requestAnimationFrame
might pause if our browser loses focus,
resulting in a very, very large dt
after it resumes. We can workaround this by limiting the
delta to one second:
...
dt = Math.min(1, (now - last) / 1000); // duration capped at 1.0 seconds
Fixing Our Timestep
Now that we know exactly how long it has been since the last frame, we can use that information
for any math calculations within our update
method.
Having a variable timestep can work for many simple games. However we gain additional
benefits if we can guarantee that the update
method is called at a fixed, known, interval.
Replayable - if the timestep is variable (and unpredictable) then we cannot predictably replay the level. If we want to be able to replay what happened we need the timestep to be fixed and predictable
Predictable Physics - if we have a physics engine in our game, then variations in the timestep would make it unpredictable, which might make it hard to create predictable level design
Mitigate bullet-through-paper - depending on our collision detection scheme, we might find that fast moving objects can pass through small objects, this can be mitigated if our fixed timestep is set relative to our entities maximum speed and minimum size.
The idea behind a fixed timestep gameloop has been written about thoroughly by gafferongames.com
The basic idea is to accumulate our dt
until it is greater than our desired fixed timestep,
call update
with the fixed timestep, and carry over the remainder to accumulate for the next
time.
We should also pass any remainder dt
to our render function so it can perform smoothing LERP
calculations (if required)
var now,
dt = 0,
last = timestamp(),
step = 1/60;
function frame() {
now = timestamp();
dt = dt + Math.min(1, (now - last) / 1000);
while(dt > step) {
dt = dt - step;
update(step);
}
render(dt);
last = now;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
For a detailed explanation of fixed timestep game loops, I highly recommend you read the articles by gafferongames.com, gamesfromwithin.com and koonsolo.com
Adding an FPS Meter
One last nice to have is to integrate an FPS counter into the loop. These days I favor FPSMeter which can be integrated in easily:
var fpsmeter = new FPSMeter({ decimals: 0, graph: true, theme: 'dark', left: '5px' });
function frame() {
fpsmeter.tickStart();
...
fpsmeter.tick();
}
BONUS: Slow Motion
As a bonus, we can (optionally) adjust the step
variable and play our game in slow motion. This
can be very helpful when debugging. If we multiply step
by 5, the game will play 5 times slower
(12.5 fps instead of 60fps).
Putting this all together into a final version that can be re-used by different games, we can
build a general purpose Game.run
method that allows the caller to override various options:
Game = {
...
run: function(options) {
var now,
dt = 0,
last = timestamp(),
slow = options.slow || 1, // slow motion scaling factor
step = 1/options.fps,
slowStep = slow * step,
update = options.update,
render = options.render,
fpsmeter = new FPSMeter(options.fpsmeter || { decimals: 0, graph: true, theme: 'dark', left: '5px' });
function frame() {
fpsmeter.tickStart();
now = timestamp();
dt = dt + Math.min(1, (now - last) / 1000);
while(dt > slowStep) {
dt = dt - slowStep;
update(step);
}
render(dt/slow);
last = now;
fpsmeter.tick();
requestAnimationFrame(frame, options.canvas);
}
requestAnimationFrame(frame);
},
...
}
I’ve used a variation of this game loop for all my javascript games so far. It’s worked very well.