Creating a fixed delta time javascript game loop

- 5 mins
Hello1
ViewSource code

Fixed delta time game loop

In this tutorial we will look at what a fixed delta time game loop is and how it can help vanquish the problem we observed in the previous tutorial.

The problem with variable delta time game loops

The variable delta time game loop allowed us to progress the game in terms of time elapsed instead of frames which solved the problem of inconsistent game experience across slower and faster performing computers.

However the variable game loop does introduce a problem that was observed at the end of the previous tutorial. Due to it's variable nature the time between each frame is unpredictable which often leads to unexpected gameplay, as demonstrated by the player sticking to the wall or leaving the game area entirely. In game development we often need to guard against invalid game states occurring and this problem becomes far more difficult when the game uses a variable game loop to decide the magnitude at which a players position changes for example.

Imagine a game where there is a border of 16 pixels wide that blocks a player from getting past and a player that is only meant to move at 10 pixels / frame. Without a variable game loop the player will never move more than 10 pixels / frame and so long as we can check if the player is overlapping with the border each frame we can reset their position to be within the border. Now what happens if we introduce our variable game loop from the previous tutorial. Imagine that someones computer slows down and 3 frames worth of time has elapsed since the player was last updated. In this scenario the player can move up to 3 * 10 = 30 units in one frame and bypass our border.

This poses a common problem in games on how to deal with collisions of fast moving objects. There are more complicated techniques for dealing with this but one simple technique is to have a maximum speed each object can travl and ensure your game logic accommodates for this.

This simpler solution is not really possible with a variable game loop as we have no control over how delayed a player's frame cycle will be and therefore we have little control over the maximum speed a player can move or at how fast they achieve this speed which can have catastrophic effects on our game experience.

Thankfully we can take the theory used in implementing a variable game loop, tweak it slightly and end up with much more predictable game play.

What is a fixed delta time game loop

A fixed delta time game loop uses the idea of breaking up the elasped time that has occurred into a number of frames and applying the changes iteratively in smaller chunks. This allows us to move the player in a predictable and controlled way!

Lets say we have an extremely slow game that runs at 1 frame per second and a character that moves at 10 units / frame to the right starting at x=0.

If the game stalls for 3 seconds we have missed 3 frames worth of game play. To apply this change using a variable game loop we would do something like.

const newPlayerXPosition = oldPlayerXPosition + 10 * 3 // 30
const colliding = checkObstacle(newPlayerXPosition) // false

However if there was an obstacle at position 10 or 20 we would have completely skipped past it. A fixed delta time game loop will instead split the changes into chunks and function similar to below.

const framesElapsed = 3;

for (let i = 0; i < framesElapsed; i++) {
  const newPlayerXPosition = oldPlayerXPosition + 10 // 10,20,30
  const colliding = checkObstacle(newPlayerXPosition) // true,true,false
}

As you can see a fixed delta time game loop still plays catchup when the game lags but because it splits the time it needs to catchup into predictable chunks we can perform collisions checks and adjust the player position accordingly in small adjustments rather than in one big bang change.

In the example this technique allows us to discover that the player is blocked at position x=10 where we can then decide to make the player bounce off the object, destroy it and slow down or any other action we want the player to perform. The important point is that the player has been prevented from skipping through an obstacle which in this game is considered an invalid game state.

If you're still having trouble picturing the difference between the two types of game loop it should become clearer when we implement one below.

Disabling game loop 2

Before implementing the fixed game loop we will first disable the second game loop to simplify our code.

Comment out the following lines in GameRuntime.ts's setup method.

// GameRuntime.gameState.engine2.startTime = now;
// GameRuntime.gameState.engine2.timePreviousLoop = GameRuntime.gameState.engine2.startTime;

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

Setting up state for a fixed delta time game loop

The way we implement a fixed game loop is quite similar to the example above where we will introduce a new variable to our game engine called lag which will store how many milliseconds the game is behind the current time.

Update GameState.engine1 by adding the lag field of type number so engine1 looks like below.

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

Update engine1 in GameConstants.ts to set lag to 0 as below.

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

Remove variable delta time

Up until now you may have been wondering why we have created a separate GameLoopEngine.ts class. This class will be used to store our game loop logic to handle when and how often to update the game state. We want this to be a concern of the game engine and separated from the core game logic. This allows us to reuse this logic in other games we create.

Before implementing the fixed game loop we need to remove the variable game loop code from the previous tutorial.

In the GameRuntime.ts updateGameState method remove the following lines.

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

Also update the const square1X to remove deltaTimeSecs as below.

const square1X = gameState.squares.square1.pos.x + square1Velocity

We are going to move these concepts over the GameLoopEngine. Add the following lines to the top of the GameLoopEngine's loop method.

const currentTime = Date.now();
const timeElapsed = currentTime - GameRuntime.gameState.engine1.timePreviousLoop;
const newLag = GameRuntime.gameState.engine1.lag + timeElapsed;
GameRuntime.gameState.engine1.timePreviousLoop = currentTime;
GameRuntime.gameState.engine1.lag = newLag;

Run the app and checkout the results! The player 1 square is moving at break neck speed, curiously though it seems to be staying within the game bounds. The reason the player is moving so fast is we are no longer taking into account the deltaTime ratio meaning we are moving as fast as we should in one second within 1 frame ie 60 times the speed at which player 1 should be moving.

The reason the player stays within the bounds is because after it has looped once and stayed within the bounds this logic and positioning is essentially repeating, their is no variation in the calculations and so it should stay in the bounds forever.

Applying game frame updates based on game lag

To account for the lag we will update the GameLoopEngine's updateFrame method to take gameState: GameState as an argument as below.

In GameLoopEngine update where we call updateFrame in the loop method to pass the gamestate.

const newGameState = GameLoopEngine.updateFrame(GameRuntime.gameState)

Next update the updateFrame method to be the same as below.

static updateFrame = (gameState: GameState): GameState => {
  const fixedDeltaTime = 1000.0 / 60.0
  if (GameRuntime.gameState.engine1.lag >= fixedDeltaTime) {
      const newGameState = GameRuntime.updateGameState(GameRuntime.gameState);
      newGameState.engine1.lag = Math.max(newGameState.engine1.lag - fixedDeltaTime, 0);
      return GameLoopEngine.updateFrame(newGameState);
  }
  else {
    return gameState;
  }
};

I will walk through the code above. First we check if the current lag is greater than some time increment, this is our fixed delta time hence why this type of loop is referred to as a fixed delta time game loop.

If the lag is greater than our fixed delta time it means that at least one frame has occurred. We call our GameRuntime.updateGameState to receive the new game state, this should perform all the calculations and logic necessary to return the new valid game state. We then reduce the lag parameter by fixed delta time making sure we set it to 0 if it goes below 0 to signfiy that we have caught up.

If you've never seen recursion before the next line may be somewhat confusing. We now proceed to call the function we are currently in updateFrame again. When a function calls itself this is called a recursive function. We can use a recursive function instead of a traditional for loop to repeat some logic until an exit clause is met and we stop calling the function or recursing. As an aside and interesting rabbit hole to explore this technique is often used in functional programming which I model some of my game code design around.

The function will continue to recurse until the lag is smaller than our fixed delta time. This means that whenever the game lags behind it will apply this catchup mechanism.

If the lag is smaller than fixed delta time we return the newest gamestate.

Go ahead and run the game, the player is still moving very fast. We are not accomodating for the fact that only a fraction of a second has occurred between frames. We can either multiply our position calculation by fixed delta time as we did before or adjust the players speed to take this into account.

Lets reduce the players speed from 80 to 5 and rerun the app.

Simulating slower and faster computers

Lets simulate a very slow computer 5 fps, update the line that sets how often we call the game loop 1 method in GameRuntime's setup method as below.

const game1UpdateLoopsPerSecond = 1000 / 5.0

Run the app, what do you notice, the game has become choppy, the player is no longer moving smoothly but importantly the players speed has not slowed down. It's taking the square the same amount of time to move back and forth across the screen!

This jerkyness reminds me of how the tetris pieces fall or playing a game online on a slow internet connection, it's not a great experience but it's better than the game not being able to be played at all.

Now lets simulate a fast computer 120 fps by changing the same line as below.

const game1UpdateLoopsPerSecond = 1000 / 120.0

Run the app, notice how the square still moves at a constant speed. You might also notice that the square does not appear to move any more smoothly than when we were running at 60fps. This is due to two things, the setInterval method seems to have a minimum interval of about 60 calls per second this may be different for you. The other reason is that we are using a fixed delta time of 1000 / 60.0 to represent the number of milliseconds per frame when the gaem is running at 60 fps. We could adjust this to be 1000 /120.0 and if setInterval was called more frequently we would notice an even smooth movement for our player at higher frame rates which is what happens in modern high performant games of course the human eye has it's limitations and some studies say that we can't really perceive things above 60 fps anyways but this sort of argument is probably akin to the need for 4K resolution TV's.

Unfortunately introducing fixed delta time and the concept of game lag has solved one problem but created another. Join me in the next tutorial where we will discuss the ominous game loop spiral of death!