The Game Loop

by Peter N. Wood


Previous Step 1: Canvas Basics
Next Step 3: Control Schemes
Last Updated: 8 Jan, 2018

At the core of any video game code is the game loop. Any game with movement needs to update its physics at a regular interval, and all games need to draw to the screen at the same rate that the monitor refreshes it's display. It's like a film reel, in that we draw still images in quick succession so that they appear to move. Unlike a film reel, we must construct each still image the moment before we draw it to the screen, or “render” it.

Different monitors have different refresh rates, which is the number of times per second that the monitor refreshes all its pixels with whatever colors they're supposed to be at that moment. This is a major problem in game design, because different monitors have different refresh rates, and that rate doesn't always stay constant. Luckily we have good solutions for this problem.

For JavaScript, we have window.requestAnimationFrame() to help us. This function takes a callback to execute once the next frame is ready to be drawn. This way, we don't run our rendering code so infrequently that the game looks choppy, and we don't waste processing power doing it more often than we need to.

function frameStep(timestamp) {
  if (!start) {
    var start = timestamp;
  }
  if (timestamp - start < 2000) {
    window.requestAnimationFrame(frameStep);
  }
}
window.requestAnimationFrame(frameStep);

In order to keep the game loop going, we need to be sure to call requestAnimationFrame at the end of our callback function. It also helps to pause rendering if the user leaves the current browser tab; the recommended method is to only call requestAnimationFrame if less than two seconds (2000 milliseconds) have passed since the last frame. For a more detailed explanation of this function see the Mozilla documentation here.

This callback is where we want to put all of our code for rendering things in the game. Here's a simple example, with a ball bouncing around the edges of the canvas. First we define a few constants and an object to keep track of our ball's position and velocity.

To update the ball's position, we just add the x velocity to the x position, and the y velocity to the y position. Then, we check whether the ball has collided with the edges of the canvas; the x and y coordinates of the ball tell us it's center, so we have to add the ball's size to each coordinate to check whether it's hit an edge. If the ball hits an edge, we can make it bounce by inverting the velocity in that direction; E.G. if x velocity is 1, we change it to -1.

After we've updated the ball's position, we draw a circle centered on its coordinates.

// Canvas width and height
const CANVAS_W = 800;
const CANVAS_H = 450;
// Size of the ball in pixels
const BALL_SIZE = 16;
// Ball with x/y coordinates defining the center, and x/y velocity
var ball = {
  x: 2 * BALL_SIZE,
  y: 2 * BALL_SIZE,
  xVel: 1,
  yVel: 1
};

function updateBall() {
  // Move the ball
  ball.x += ball.xVel;
  ball.y += ball.yVel;

  // Check for collision with canvas bounds
  if (ball.x + BALL_SIZE > CANVAS_W) {
    ball.x = CANVAS_W - BALL_SIZE;
    ball.xVel = -1;
  }
  else if (ball.x - BALL_SIZE < 0) {
    ball.x = BALL_SIZE;
    ball.xVel = 1;
  }

  if (ball.y + BALL_SIZE > CANVAS_H) {
    ball.y = CANVAS_H - BALL_SIZE;
    ball.yVel = -1;
  }
  else if (ball.y - BALL_SIZE < 0) {
    ball.y = BALL_SIZE;
    ball.yVel = 1;
  }
}
// Render the ball
function renderBall(context) {
  context.beginPath();
  context.arc(ball.x, ball.y, BALL_SIZE, 0, 6.28);
  context.fillStyle = '#3AF';
  context.fill();
  context.closePath();
}

var context = document.getElementById('canvas').getContext('2d');

function frameStep(timestamp) {
  // Update first
  updateBall();
  // Then render
  renderBall(context);

  if (!start) {
    var start = timestamp;
  }
  if (timestamp - start < 2000) {
    window.requestAnimationFrame(frameStep);
  }
}

window.requestAnimationFrame(frameStep);

You'll notice we get this paintbrush or smear effect, as if the ball is leaving a trail behind it. Each time we draw the ball, we're drawing over the existing image, without erasing what we drew before. Maybe you've seen glitches in 3D games where the player goes outside of the level boundaries, and everything looks like an overexposed timelapse. The same thing is going on there. Normally, everything in the level would get rendered and cover the last frame's image, but there's nothing to render beyond the level bounds. Erasing the entire screen is a waste of processing power since the player isn't supposed to go out of bounds.

Usually, a game will have a background to render before everything else, so we can just render that first and avoid any issues. If we just need a simple background color or image, we could use CSS styles to apply that to the canvas element, and erase the canvas every frame. We could even take that a step further and only erase the area of canvas where the ball was last frame: a common technique in older video games when limited memory and processing power was a big concern.

Let's keep things simple and fill the canvas with a solid color background.

function frameStep(timestamp) {
  // Update first
  updateBall();
  // Render the background first
  context.fillStyle = '#30A';
  context.fillRect(0, 0, CANVAS_W, CANVAS_H);
  // Then render
  renderBall(context);

  if (!start) {
    var start = timestamp;
  }
  if (timestamp - start < 2000) {
    window.requestAnimationFrame(frameStep);
  }
}

Now that looks pretty good, but we still have a problem: our ball's logic only updates with the framerate. Depending on what monitor you're using, the ball will move faster or slower, and if the framerate isn't totally steady the game will stutter. In a bigger, more processor-intensive game, you might see the game speed change wildly with a setup like this, as the platform struggles to execute the game loop every frame. It's common to see mobile and console games designed this way, because the developers only have to worry about one platform with one set of specifications; it only becomes a problem if they want to port the game to PC.

Luckily, there's a simple solution for this: completely dissociate your rendering logic from your physics logic. We can keep track of how many milliseconds have passed since the last frame was rendered, and execute our physics updates an appropriate number of times. It’s tempting to use setInterval(), so that we get a constant update rate, but in the long run this is not reliable.

First, we need to decide how often we want our updates to happen; if it’s too infrequent, the game will stutter, but too often and it’ll tax the processor and slow everything down. Let’s assume the maximum framerate our game will encounter is 144hz, at which a frame gets drawn roughly every seven milliseconds (ms). We ought to update at least once per frame, so let’s go with five since it's easy to work with mathematically; that’ll be our Delta Time (Δt), meaning change in time.

Then, every frame we'll accumulate the number of milliseconds since the last frame, and subtract our Δt every time we run an update. At 60hz (the most common case), we’ll accumulate 16 2/3 milliseconds every frame on average (technically, the fraction carries over to the next frame, so we’d see a pattern of 16, 17, 17); so if we run our updates every five milliseconds, we’ll update three times the first frame, with 1 2/3 milliseconds left over. After a few more frames, those milliseconds will add up to another update, and in the long run the game will run smoothly.

const DELTA_TIME = 5;
var accumulator = DELTA_TIME;

// Set the initial value in milliseconds
var lastTimestamp = Date.now();

function frameStep(timestamp) {
  var now = Date.now();
  // Add the number of milliseconds since the last frame to the accumulator
  accumulator += now - lastTimestamp;
  // Don't forget to update this for the next frame
  lastTimestamp = now;
  // Update once for every Δt since the last frame was rendered
  while (accumulator >= DELTA_TIME) {
    updateBall(ball);
    accumulator -= DELTA_TIME;
  }

  // The rest of this function is the same...
}

Now that we have the basics of the game loop worked out, and our updates are decoupled from the framerate, we can add some interactivity; an animated image is nice, but we can't call it a game if it plays itself, can we? Next up: keyboard, mouse, and touch controls.

— Peter N. Wood

Previous Step 1: Canvas Basics
Next Step 3: Control Schemes

Resources

MDN Canvas API documentation
MDN article on optimizing canvas rendering