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.
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!
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.