Creating a variable delta time javascript game loop

- 5 mins
Hello1
ViewSource code

Variable delta time game loop

In this tutorial we are going to learn how to make our game and square speed not be effected when the game loop is running faster than normal.

In the first tutorial we created two game loops to simulate a game being played on two different computers, where one computer's performance and frame rate was double the other. This was simulated by calling the game loop associated with the faster computer twice as often. We saw that the player with the more performant computer moved twice as fast across the screen as the other player.

This is an unfair advantage that any multiplayer game needs to counteract to even the playing field. Even for one player games if we become used to certain timing for a game that we can rely on for certain animations or moves to take to complete and suddenly they are taking even 5% shorter or longer this will be noticeable and in any game that relies on fast reaction's such as a fighting game this randomness will be make the game frustrating to play.

What is delta time

The problem that we have is that the game's speed is determined by it's frame rate (different for different hardware) instead of the time that has elapsed (the same per player). In fact if we move a square 10 units to the right every time a frame ticks over that players speed is not 10 units / second.

Most games will run at about 60 frame per second so actually that square is moving at 60*10 = 600 pixels per second! We can also see that as the frame rate changes the new speed changes which is the effect we saw in the first tutorial.

So we have two problems

  • Hardware performance effects game play
  • Our measurements are not predictable so we can't set a desired speed such as 10 pixels per second.

The concept of delta time can help to solve both of these problems by helping us to create a variable game loop.

Up until now how much of the game we simulate per frame has remained constant, so when the game is running fast we simulate more game state changes, speeding up the game and the opposite is true.

If we assume that our game should be running at 60fps then 1 frame occurs every 1/60 = 0.01666667 seconds, this ratio can be set to a variable called deltaTime. If we multiple all of our physics calculations by deltaTime we will ensure that our player is not moving 600 units / second but instead 10 units / second as shown in the example below.

playerSpeed = 10
playerXPosition = 0
deltaTime = 1/60.0
//new frame occurs
new playerXPosition = playerXPosition + playerSpeed * deltaTime
//playerXPosition = 0 + 10 * (1/60) = 0.1666667 = how many units the player moves / frame
//In 60 frames the player moves =  0.1666667 * 60 = 10 units / 60 frames = 10 units / second as desired

As shown above deltaTime allows us to negate the effect of a frame occurring every 1/60th of a second. Now this will work in a perfect world when our game is running at 60 frame per second but as we know the frame rate can vary and so this is why we should use a variable deltaTime.

This may lead us to the question, what should we set our deltaTime variable to each game loop? Well the answer is in the name deltaTime just refers to the change in time that has occurred so we can determine the deltaTime each game loop by storing the time the previous game loop occurred and taking the different between the current time and the previous game loop time.

Next we will see all of this theory in action as we implement the same process and introduce a variable delta time game loop into our game loop in the following section.

Adding a variable delta time game loop

First step is to start using both game loops again so that we can compare them. Make the following changes to the code.

Comment the intervalWorker code in GameRuntime.

//let intervalWorker = new Worker('web-worker.js');
//intervalWorker.onmessage = GameRuntime.loop

Uncomment game loop1

const game1UpdateLoopsPerSecond = 1000 / 60
setInterval(GameRuntime.loop, game1UpdateLoopsPerSecond)

Uncomment game loop 2

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

Now run the app and confirm both squares move side to side across the screen and that the second square is moving half the speed of the first.

Introduce engine state to game state

We want to capture the time the game started as well as the time during the previous loop for each game loop so that we can calculate how much time has passed (deltaTime). We can store this in a metadata game state object relating to our game engine and game loop. Lets create two fields on GameState called engine1 and engine2 that will hold the startTime and timePreviousLoop. The engine property will represent the game engine meta data for each game loop. Make the changes to types.ts by adding the following fields to GameState as below.

engine1: {
  timePreviousLoop: number,
  startTime: number
},
engine2: {
  timePreviousLoop: number,
  startTime: number
}

Next update the InitialGameState in GameConstants.ts to include the new engine property as below.

engine1: {
  timePreviousLoop: 0,
  startTime: 0
},
engine2: {
  timePreviousLoop: 0,
  startTime: 0
}

Use timePreviousLoop to calculate variable deltaTime

The startTime should be set when the game begins. In GameRuntime's setup method add the following code below the GameRuntime.context declaration.

const now = Date.now();
GameRuntime.gameState.engine1.startTime = now;
GameRuntime.gameState.engine1.timePreviousLoop = GameRuntime.gameState.engine1.startTime;
GameRuntime.gameState.engine2.startTime = now;
GameRuntime.gameState.engine2.timePreviousLoop = GameRuntime.gameState.engine2.startTime;

Now we will use the timePreviousLoop to calculate deltaTime for game loop 1.

In the updateGameState method in GameRuntime.ts add the following to the top of the method.

const currentTime = Date.now()
const deltaTimeMillis = currentTime - gameState.engine1.timePreviousLoop
const deltaTimeSecs = deltaTimeMillis / 1000.0
GameRuntime.gameState.engine1.timePreviousLoop = currentTime;

First we determine the current time, this is an epoch time which returns the number of milliseconds since Jan 1970 UTC. Next we determine how many milliseconds have elapsed since the current time and the previous loop time. Finally we convert this to be in terms of seconds not milliseconds which will allow us to use it as a multiplier in all of our physics calculations that require things to move a certain amount of units / second. We also need to update the timePreviousLoop to the current time.

Lets use deltaTimeSecs where we calculate the square1X, replace the old line with the new line below.

const square1X = gameState.squares.square1.pos.x + (square1Velocity * deltaTimeSecs)

Restart the app and check to see the result. Huh, is that first square even moving, if you stare at the screen long enough it's clear that it is, but why soo slow? well now that we are multiplying deltaTimeSecs our player has been slowed down as deltaTime is currently set to 1/60 = 0.016666667 seconds.

Before using deltaTime we set our velocity to 1 as anything faster meant our square was moving too fast. Now we are transforming player 1's position based on time we can set our velocity to whatever units per second we want. Try 80 by changing the value in the InitialGameState as below.

x: 80

Run the app again and checkout the result. You might be wondering why did I pick 80, well the canvas width is 800 so now we can check, count how long it takes player 1 square to move from one side of the canvas to the other. It should take 10 seconds as we are moving 80 units / second and the canvas is 800 units wide.

Hopefully you can see how having predictable speeds will help when designing a game and making in game decisions.

Update player 2 to use deltaTime

To update game loop 2 to use deltaTime we can follow a very similar process as we did for game loop 1 but instead update the loop2 method. I will leave this as a challenge for the reader but if you get stuck remember you can see the finished code above.

Once you have finished set player 2's velocity to 80 and run the app to see the results, what do you observe? If you paid close attention to the beginning of the tutorial you may notice that the squares are more consistent with their set speeds than before. Even when player 2 was moving at half the pace of player 1 if you run the app for long enough you might notice that player 1 was already bouncing the starting wall before player 2 got to the right wall ie it was covering more than 2 time the distance. This is probably due to a rounding error that gets magnified over time. By multiplying our calculations by deltaTime we are reducing the magnitude of this error.

What you should also notice is now the two squares are moving at the same speed despite game loop 1 being called twice as much as game loop 2. This is simulating a faster and slower computer and shows that both players can have the same experience regardless of hardware.

Problem solved... or is it? Did you notice how the squares can eventually become stuck around the edges of the screen and get out of sync with each other?

Unfortunately (or fortunately if you are enjoying this course) our game loop design journey does not step here. There is a good reason for why this is happening and we will look to resolve it in the next tutorial.