Advanced game loop design intro

- 5 mins
Hello1
ViewSource code

Designing an advanced game loop

In this course we will look at different techniques to implement a more advanced game loop that can help to deal with some of the common pitfalls when trying to update games via a game loop.

Up until now we have implemented a very basic game loop that has some inherent flaws that may lead to erratic gameplay and definitely make it unsuitable for multiplayer online games. Our current game loop is using Javascript's setInterval method which is known to have issues for small intervals, drift out of sync over time and pause when the browser tab is not in focus, so not ideal relying solely on it for something as crucial as a game clock! It also has issues overtime running out of sync and can perform differently on different browsers. The problem is that our current game loop is coupled to setInterval. Each time setInterval calls our loop method relates to another frame occurring in our game. However as mentioned before what if the interval is slightly off? It would be great if we could decouple setInterval from when we update our game based on some decision we make.

In this tutorial I will simulate two different players experiences of a game when there setInterval are operating at different speeds to show first hand the inconsistent undesirable behaviour this causes. This should hopefully whet your appetite for a more advanced game loop which we will cover in the following tutorials in the course.

Setting up a basic game loop at two different frame rates

In this example we are going to have two oscillating squares representing the two players. The squares will move back and forth across the screen at a constant speed each time the frame is updated.

To keep track of where the squares are we will introduce some new state fields to our GameState type.

For this course we will use the typescript game dev input starter code which is linked below.

Add the following new top level squares field to types/types.ts inside the GameState type.

squares: {
  square1: {
    pos: Vector2DType,
    velocity: Vector2DType
  },
  square2: {
    pos: Vector2DType,
    velocity: Vector2DType
  } 
}

Also update the GameConstants.ts file's InitialGameState type to include default values for the two squares.

squares: {
  square1: {
    pos: {
      x: 0, y: 200
    },
    velocity: {
      x: 1,
      y: 0
    }
  },
  square2: {
    pos: {
      x: 0, y: 300
    },
    velocity: {
      x: 1,
      y: 0
    }
  }
}

Next lets create a new method updateGameState in GameRuntime which will update the gamestate and return the new gamestate.

static updateGameState = (gameState: GameState): GameState => {
  const squareSize = 30
  const canvasXBound = GameRuntime.canvas.width - squareSize
  const square1Velocity = (gameState.squares.square1.pos.x > canvasXBound || gameState.squares.square1.pos.x < 0) ? -1 * gameState.squares.square1.velocity.x : gameState.squares.square1.velocity.x 
  const square1X = gameState.squares.square1.pos.x + square1Velocity
  
  gameState.squares.square1.pos.x = square1X
  gameState.squares.square1.velocity.x = square1Velocity

  return gameState
}

Create a new file called GameLoopEngine.ts and add a static updateFrame method with the code below.

import GameRuntime from "./GameRuntime.js"
import { GameState } from "./types/types.js"

export class GameLoopEngine {
  static updateFrame = (): GameState => {
    const newGameState = GameRuntime.updateGameState(GameRuntime.gameState)
    return newGameState
  }
}

Next add a loop method to the GameLoopEngine class with the following code.

static loop = (): GameState => {
  const newGameState = GameLoopEngine.updateFrame()
  return newGameState
}

Inside the loop method in GameRuntime remove all of the body and replace it with the code below.

static loop = () => {
  GameRuntime.gameState = GameLoopEngine.loop()
  GameRuntime.draw(GameRuntime.context, GameRuntime.canvas.width,  GameRuntime.canvas.height, GameRuntime.gameState)
}

Okay so we've added a few methods and you might think some of them seem kind of redundant. Rest assured the extraction of these different methods will serve a purpose as our game loop becomes more complex. The responsibilities of each method are described below.

GameRuntime

  • loop: This is the loop method that our setInterval time calls it will retrive the new gameState and draw the new state.
  • updateGameState: This is responsible for updating our game's specific state that is unique to our game. This is not generic code and so needs to be separated from the generic game loop library code.

GameLoopEngine

  • loop: This is a the loop method relating to our game engine loop framework. It will take care of all the important game loop concepts that our regular game code should not need to be aware of. The goal of this extraction will be to separate the generic game framework code into a library that we can use for whatever game we create as we have done in previous tutorials with our input code.
  • updateFrame: This code will be responsible for deciding whether to update the gamestate and incrementing the games current frame.

Updating the game setup

Next we are going to move some setup code from index.ts into our GameRuntime.

Add the following two static variables to GameRuntime for the canvas and context.

static canvas: HTMLCanvasElement
static context: CanvasRenderingContext2D

Update GameRuntime's setup method to look like below.

static setup = (canvas: HTMLCanvasElement, context: CanvasRenderingContext2D) => {
  new KeyboardManager()

  GameRuntime.canvas = canvas
  GameRuntime.context = context

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

Inside index.ts change the window.onload method to look as below.

window.onload = () => {
  if (context) {
    GameRuntime.setup(canvas, context)
  }
}

This just moves the responsiblity for setting up our game from the index file into the GameRuntime which I prefer.

Drawing the square

Update the Gameruntime's draw method with the following code to draw the square.

private static draw = (context: CanvasRenderingContext2D, canvasWidth: number, canvasHeight: number, gameState: GameState) => {
  Draw.resetCanvas(context, canvasWidth, canvasHeight)
  const square1 = gameState.squares.square1
  context.fillRect(square1.pos.x, square1.pos.y, 30, 30)
}

Run the game npm run serve and head to localhost:8080 and you should see the square moving forward and back accross the screen.

Updating the frame rate

The setInterval interval currently simulates our game's frame rate. Try updating the interval to be shorter or larger as below and rerun the game.

const frameRate = 120 // Set to what you want the frame rate to be.
const game1UpdateLoopsPerSecond = 1000 / frameRate
setInterval(GameRuntime.loop1, game1UpdateLoopsPerSecond)

What do you notice, if we decrease the interval the square moves faster and slower if we increase the interval. This makes sense as currently we are moving the square by a consistent amount per frame. The faster the frames tick over the faster the square will move.

Adding a second square

Lets set the first square's frame rate back to 60 and now add a second square that will help us simulate this difference in speed.

We're going to add a simple loop2 method in GameRuntime, add the code below.

static loop2 = () => {

  const square2Velocity = (GameRuntime.gameState.squares.square2.pos.x > GameRuntime.canvasXBound || GameRuntime.gameState.squares.square2.pos.x < 0) ? -1 * GameRuntime.gameState.squares.square2.velocity.x : GameRuntime.gameState.squares.square2.velocity.x 
  const square2X = GameRuntime.gameState.squares.square2.pos.x + square2Velocity

  GameRuntime.gameState.squares.square2.pos.x = square2X
  GameRuntime.gameState.squares.square2.velocity.x = square2Velocity
}

Note we won't add all the extracted methods we did with the first loop as this code is temporary and purely for demonstration purposes.

In the setup method in GameRuntime add the following code.

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

Next update the Draw classes draw method to draw the second square as below.

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

Run the app again and notice how the first square is moving twice as fast as the second square. Wouldn't it be great if despite how quickly the setInterval method is calling the loop method and whether this is different between the two squares the squares still moved at the same speed. After all it's a bit of an advantage in a racing game if the other player is able to move twice as fast as you just because they have more performant hardware!

Tune into the next tutorial to see if square 2 can get its comeuppance as the next tutorials in this course will cover exactly this and other factors that should be considered when designing and advanced game loop.