Draw game loop lifecycle with request animation frame

- 5 mins
Hello1
ViewSource code

Fixing an inneficient draw game loop with requestAnimationFrame

An important aspect of any game loop is it's draw phase where the screen UI is updated to reflect the current state of the game. This can also be quite resource intensive so we want to ensure we are only drawing when things have changed.

Our current draw phase is coupled with our game loop however the browser has it's own lifecycle for updating the UI. The browser lifecycle is complicated and smart in the way it optimises the user's browser experience. Two main phases of the browser lifecyle are the reflow and repaint phase. the reflow phase deals with changing position of html elements, such as our game elements and the repaint deals with UI related properties changing on these elements such as colour etc.

The browser only updates the UI when a reflow and repaint are triggered, if we are trying to draw our output for our game more frequently than this it is wasted compute effort and could effect the game's overall performance. In the next section we'll test this out in our game and also see how requestAnimationFrame can be used to fix this.

Our games current draw lifecycle

To see if our draw loop is currently suffering from unnecessary triggers of the draw method we can make a few simple changes to our code.

Before we make the changes let me introduce you to a javascript browser method called requestAnimationFrame which hooks into the browser lifecycle and is called before the browser triggers a reflow and repaint. We will use requestAnimationFrame to check how often the browser reflow and repaint's the page.

Add the following logRequestAnimationFrame method to GameRuntime which triggers whenever requestAnimationFrame is called.

static logRequestAnimationFrame = () => {
  console.log("Browser reflow repaint triggered.")
  window.requestAnimationFrame(GameRuntime.logRequestAnimationFrame)
}

The last line tell the browser to run the method we provide in this case GameRuntime.logRequestAnimationFrame before the next reflow repaint cycle.

To trigger the logRequestAnimationFrame method the first time add the following line to GameRuntime's setup method.

window.requestAnimationFrame(GameRuntime.logRequestAnimationFrame)

To know exactly when we are currently drawing add a simple log at the top of the GameRuntime's draw method.

console.log('Draw method triggered.')

Make sure to comment out the second loop in the setup method as we don't want it interferring with our test.

// const game2UpdateLoopsPerSecond = 1000 / 30
// setInterval(GameRuntime.loop2, game2UpdateLoopsPerSecond)

If our game is running correctly we would expect these logs to occur exactly the same amount of times interchangeably.

Try running the game and check the browser developer console window (Chrome, right click inspect -> console).

Here is the output from my console.

Draw request animation frame inneficient.

From the above image we can see that the draw method is being called too often consistently, in the short space of time depicted in the image the draw method was called unnecessarily 9 extra times!

Making our draw method more efficient with requestAnimationFrame

So how do we go about fixing this issue with our draw lifecycle? Well the answer uses similar code to the test we have implemented so far. In fact take a moment to see if you can optimise the draw loop and verify it via the developer console.

We can use the requestAnimationFrame method to call our draw method.

Add the following line of code GameRuntime's setup method.

window.requestAnimationFrame(GameRuntime.draw)

Unfortunately this means we need to remove draw's current method parameters as we did with the loop method previously.

Update the draw method to the following.

private static draw = () => {
  console.log('Draw method triggered.')
  Draw.resetCanvas(GameRuntime.context, GameRuntime.canvas.width, GameRuntime.canvas.height)
  const square1 = GameRuntime.gameState.squares.square1
  GameRuntime.context.fillRect(square1.pos.x, square1.pos.y, 30, 30)

  const square2 = GameRuntime.gameState.squares.square2
  GameRuntime.context.fillRect(square2.pos.x, square2.pos.y, 30, 30)
  window.requestAnimationFrame(GameRuntime.draw)
}

Note make sure to add the new line at the bottom window.requestAnimationFrame(GameRuntime.draw) which calls tells the browser to call the draw method before the next reflow repaint cycle.

Also make sure to remove the line that calls the draw method from the GameRuntime loop method! Or as a test keep it in and confirm the drastic performance hit overdrawing can have on a game cycle (Remember to remove it later though).

Re-run the app and see the change in results!

Draw request animation frame inneficient.

As we can see from the image above now the draw method is only being called when the browser triggers a reflow and repaint.

Before moving onto the next tutorial feel free to comment or remove the logRequestAnimationFrame method and console.log's we added in this tutorial as we won't need them anymore.

requestAnimationFrame properties worth knowing

requestAnimationFrame is only called when the browser tab is in focus so if we change tabs or change focus to another application our game will stop updating. This is a conscious choice of requestAnimationFrame to save resources however if we are creating an online multiplayer game we might want the game to continue updating.

This highlights some interesting questions when designing a game around how the game should behave when paused for example or out of focus. In a following tutorial we will look at a method to allow the game to continue running when inactive.

There are many different techniques for deciding how the game should behave when out of focus and the answer depends on what sort of game it is and simply how we want it to behave, we'll revisit this question later in the course when we look at dealing with client lag.