Sidequest

Delta Time

What is Delta Time?

Delta time is literally the change (delta) in time between two events. In this case the change in time between executions of the frame() function. But why do we need this?

If you recall our grounds scrollSpeed value, it's a relatively low number of 2, and represented the velocity in pixels per frame. We're counting on the fact that our game only ever runs at a fixed frame rate, say 60fps, to deliver a consistent experience to all our players. This will not always be the case. Monitors running at 120hz (and higher) aren't uncommon. That would mean our game would play (at least) twice as fast! Now players are experiencing the game differently depending on their hardware. Not good.

Another issue is dropped frames. What if our game was more intense and lower end PCs couldn't execute our frame() function within ~16ms? What if frames are dropped due to the number of applications clawing for resources on our machines?

Last, but not least, thinking of our velocity in terms of pixels per frame isn't the most intuitive mental model, especially now that we know frame rates could vary. Instead it would be nice to think of velocity in terms of pixels per second, or pixels per unit of time.

We're going to change the scrollSpeed to 120, but in terms of pixels per second. We'll need to calculate the delta time between each frame to handle this.

Accounting for Delta Time

Let's talk about how we're going to calculate and use delta time. Something we haven't mentioned is that the callback function in requestAnimationFrame() is passed a single argument of type DOMHighResTimeStamp, which we'll abbreviate to hrt. hrt is a millisecond value that is constantly increasing from the start of the documents lifetime (time origin). So each time our callback is called we can expect hrt to have increased. If we store the last (previous frame) hrt since our callback was called, and use the current (current frame) hrt, we can determine the delta time in milliseconds within a given frame: const dt = hrt - last. Now remember we wanted to think of velocity in terms of seconds, not milliseconds, so lastly we need to normalize this delta time into seconds: const dt = (hrt - last) / 1000;

You might be wondering, what should we set last to for the first time? After all, we need a single frame from the past to calculate the current frames delta time. We can leverage performance.now() to set the initial value outside of our frame function. It also returns a DOMHighResTimeStamp, so we'll just use it to capture the current point in time in the documents lifetime before the first call to requestAnimationFrame().

Once we have calculated the delta time, it's pretty easy to leverage it. We'll apply delta time to the values in our game that cause change between frames, in this case the scrollSpeed.

Update the scrollSpeed back in main.ts:

1
const ground = new Ground({
2
position: new Vector2d(0, config.gameHeight - spriteMap.ground.height),
3
frame: spriteMap.ground,
4
spriteSheet,
+
scrollSpeed: 120,
6
});

Then modify the update() method of the Ground class:

1
export class Ground {
2
// ...
3
+
public update(delta: number) {
+
this.scrollPositionX -= this.scrollSpeed * delta;
6
7
// Once the whole image is offscreen, reset the x position to 0 to
8
// create the loop effect.
9
if (this.scrollPositionX <= -this.frame.width) {
10
this.scrollPositionX = 0;
11
}
12
}
13
14
// ...
15
}

That's it for the Ground class.

Next, we need to calculate delta time in the game loop and pass it along:

+
let last = performance.now();
2
+
const frame = (hrt: DOMHighResTimeStamp) => {
+
const dt = Math.min(1000, hrt - last) / 1000;
5
6
context.clearRect(0, 0, canvas.width, canvas.height);
7
+
ground.update(dt);
9
10
// Draw the background
11
context.drawImage(
12
spriteSheet,
13
spriteMap.background.sourceX,
14
spriteMap.background.sourceY,
15
spriteMap.background.width,
16
spriteMap.background.height,
17
0,
18
0,
19
spriteMap.background.width,
20
spriteMap.background.height
21
);
22
23
ground.draw(context);
24
+
last = hrt;
26
27
requestAnimationFrame(frame);
28
};

There you have it! We've now accounted for delta time in our Ground class and our game loop.