hazzens scribe

links

JS Game Code Fundamentals: The Game Loop

28 Jan 2014

This is the first post in a series on fundamentals for coding your own games from scratch.

Every game, just like every program, has to start somewhere. Unlike a command-line program, however, games don’t have a single output to produce. Instead, the game sits in a loop, handling user input, and doing things with it. One of those inputs might start a new game, and then subsequent inputs are used to control a character. Another of those inputs might show an options screen, and yet another quit the game.

While each game is different in its play, most games share the same core: a game loop.

A Basic Loop

The game loop has to accomplish two things: updating the state of the game based on anything that has happened, and then rendering the state. At some point, it may have to stop the loop. The simplest loop one can imagine is:

  while (true) {
    updateGameWorld();
    renderGameWorld();
    if (shouldQuit()) {
      break;
    }
  }

How fast will this loop run? As fast as your computer can chug along, which can be suprisingly fast. In C++ game development, one can fix this by deciding on an update rate and sleeping between each loop iteration:

  // Run at ~50fps. Each frame should be (1000 ms) / 50 long.
  int64 frame_duration_ms = 20;
  int64 last_time_ms = currentTimeInMillis();
  steady_clock::time_point last_time = std::chrono::steady_clock::now();
  while (true) {
    updateGameWorld();
    renderGameWorld();
    if (shouldQuit()) {
      break;
    }
    int64 cur_time_ms = currentTimeInMillis();
    int64 elapsed_ms = cur_time_ms - last_time_ms;
    if (elapsed_ms < frame_duration_ms) {
      sleepForMs(frame_duration_ms - elapsed_ms);
    }
  }

Presto!

And in JavaScript?

In JS, we can’t just write an infinite loop and expect your code to run in a browser. This is because the browser UI doesn’t waits until your code has finished running before it does anything else. Instead of an infinite loop, we instead need a function that is called every frame, and isn’t called again until the next frame.

In the olden days, this was accomplished using window.setTimeout which has a host of annoyances (imprecise timing, possible delays, requiring you to handle the sleep duration). These days, we get the wonderful window.requestAnimationFrame. You can find the docs on MDN but I’ll sum up the important bits:

  • requestAnimationFrame takes a function as an argument and runs it during the next refresh of the display.
  • requestAnimationFrame only runs the function once.
  • requestAnimationFrame calls the provided function with a single argument, a decimal timestamp in milliseconds.

The simplest game loop then becomes:

var doOneFrame = function(curTimeMs) {
  updateGameWorld();
  renderGameWorld();
  // Request another run of this function when the browser can.
  window.requestAnimationFrame(doOneFrame);
};

// And also make sure to request an initial run of our function.
window.requestAnimationFrame(doOneFrame);

We have one glaring issue with this loop, however: nothing guarantees that each frame will take the same amount of time. The first time we update the game world, 10ms may have passed. The next time, 20ms. Our game will be jumpy and feel all over the place.

Fixing the Timestep

The easiest way to get around the inconsistent time step problem is to decide how much time your updateGameWorld function simulates. If it simulates one second of time, you only need to call it once a second (but your game won’t change very often). How often should it update? Well, requestAnimationFrame is called roughly 60 times per second, so 1/60th of a second is the obvious answer.

We are going to do two changes: the updateGameWorld function now takes an additional param (the amount of time to simulate) and we also need to track the time between frames.

var lastTickMs;
var FRAME_RATE_MS = 1000 / 60;
var doOneFrame = function(curTimeMs) {
  var elapsedTimeMs;
  if (lastTickMs) {
    elapsedTimeMs = curTimeMs - lastTickMs;
  } else {
    // This is our first frame, so choose a simple initial value.
    elapsedTimeMs = FRAME_RATE_MS;
  }

  var ticksToSimulate = Math.floor(elapsedTimeMs / FRAME_RATE_MS);

  // Tick the game world as many frames as we need.
  for (var i = 0; i < ticksToSimulate; i++) {
    updateGameWorld(FRAME_RATE_MS);
  }

  // ...but only render the world once.
  renderGameWorld();

  // Setting lastTickMs to curTimeMs would be wrong! If 40ms elapsed
  // between frames and we simulate ~16ms per tick, we only simulated
  // ~32ms of the 40ms elapsed. We've lost a whole 8ms!
  //
  // Instead, we set the last tick time to the current time, minus
  // any leftover from this frame.
  var leftoverMs = elapsed_ms - ticksToSimulate * FRAME_RATE_MS;
  lastTickMs = curTimeMs - leftoverMs;

  // Request another run of this function when the browser can.
  window.requestAnimationFrame(doOneFrame);
};

// And also make sure to request an initial run of our function.
window.requestAnimationFrame(doOneFrame);

The most important bit of the code above is accounting for the leftover time each frame. If we neglected to do this, the game could feel jumpy.

Advanced Considerations

The above game loop is enough for most everyone, but it has a few flaws. For one, consider what happens if the user tabs away from your game and comes back in twenty seconds. You will sit there, chugging along, simulating twenty seconds of game ticks and then jumping the world to the new state. The user is probably confused.

Another issue is the possibility of what is known as a “death spiral” - if you simulate 1/60th of a second each tick of the game, but doing so takes 1/30th of a second, you’ll never catch up to real time! In fact, your work will double each frame until the user quits in disgust.

We can solve both of these issues with a rather simple solution: define some maximum number of frames to simulate (say, 5). If you need to simulate more than 5 frames, just call it quits:

var MAX_TICKS_PER_FRAME = 5;
...
var doOneFrame = function(curTimeMs) {
  ...
  // Tick the game world as many frames as we need, but no more
  // than five.
  for (var i = 0; i < Math.min(MAX_TICKS_PER_FRAME, ticksToSimulate); i++) {
    updateGameWorld(FRAME_RATE_MS);
  }
  ...
};

Another consideration, though less common in the world of requestAnimationFrame and its fairly consistent framerate, is rendering extrapolation. In our game loop, we end up with some leftover time that we didn’t simulate, but has still occurred. Pretend every frame takes 10ms, but our tick rate is 8ms. Our first three rendered frames will be one tick after the previous. But that fourth frame has 2ms of surplus build up each previous frame, and now needs to simulate two ticks. Even though we update the screen at an exactly uniform rate - every 10ms - our game will appear to hiccup every fourth frame.

Solving this problem is annoying, and with a frame rate of 60fps it generally isn’t very noticeable. But if you care to fix it, you will need to do extrapolation of your rendering. Instead of rendering each object in the game world at its current position, you render it extrapolated some small timestep into the future.

In our worst case example, the first render call we extrapolate each object 2ms ahead, then 4ms ahead, then 6ms ahead, then finally back to 0. This requires your rendering code to make some guesses about how objects will behave. This code doesn’t need to be smart; it looks a little like this:

var doOneFrame = function(curTimeMs) {
  ...
  renderGameWorld(leftoverMs);
  ...
};

var renderGameWorld = function(deltaMs) {
  var deltaS = deltaMs / 1000;
  // Render the player where we think they will be in deltaS from now:
  renderSquare(
    player.position.plus(player.velocity.times(deltaS)),
    player.width, player.height, player.color);

  // And the same for all other objects...
};

One question you might have is “Why have a fixed timestep for updating the game world?”. The answer is physics and floating point. If you updated the game however much time had passed since the last frame, consider how a falling ball would move if you only tracked its velocity:

GRAVITY = new Vector(0, -9.8);
Ball.prototype.tick = function(timeElapsed) {
  // Apply gravity.
  this.velocity = this.velocity.plus(GRAVITY.times(timeElapsed));
  // Apply velocity.
  this.position = this.position.plus(this.velocity.times(timeElapsed));
};

Ball b1 = new Ball();  // Position/velocity of 0.
Ball b2 = new Ball();  // Position/velocity of 0.

// Simulate 0.3 seconds for each ball, but in different increments.
b1.tick(0.1); b1.tick(0.1); b1.tick(0.1);
b2.tick(0.3);

console.log(b1.position, b1.velocity);
// Outputs "(0, -0.588) (0, -2.94)"

console.log(b2.position);
// Outputs "(0, -0.882) (0, -2.94)"

This inconsistency in position based on the step size is the number one reason you should have a consistent step size. That isn’t to say you can’t vary your step size via means other than the system clock. Cutting your simulated step time in half is the easiest way to get slowmo, after all. But you should never, ever vary your step time based on time elapsed if you want your game to be consistent. Else that player with a slightly slower computer might never be able to make that jump because of phyics.

And that is game loops in a nutshell.