Game Loops

At the core of all games, a permanent loop is running which repeatedly gathers data from the input devices, moves all active objects in the game world and then generates the visual and audible representation of that world on the screen and on your speakers.

Time to examine this game loop then!

This article will explain different designs of game loops and why time stepping is a good idea even in single player games.

A Naive Game Loop

A naive implementation of a game loop could not like this:

public void RunGame() {

  // Continue running until we are asked to terminate
  while(!this.endRequested) {

    // Loop over all entities in the game, update the positions
    // and render their audible and visual representations.
    foreach(Entity entity in this.activeEntities) {
      entity.Update();
      entity.Render();
    }

  }

}

Now we are about to encounter one of the most basic problems game programmers have had to cope with from the beginning: Not all PCs are equally fast, some will take longer to update the positions of your game’s objects, others will not be able to render them quite as fast. Performance might even change during the game because the number of things on the screen will vary greatly.

With the above game loop, the game would simply run slower, since the objects’ positions will be updated less often than on a faster PC. On the other side, if the game is run on a faster machine, it might run faster than you expected and is likely to become unplayable. The next chapter will show you how to avoid that.

Time-Scaled Game Loop

In order to make a game independent of how fast the PC can run the game loop, we need to scale the amount by which objects are moved by the time the previous iteration of the game loop took. In other words, measure how much time has passed since the last time your objects were updated, calculate the up-to-date positions of all objects based on that time and render another frame.

public void RunGame() {
  DateTime lastIterationTime = DateTime.Now;

  // Continue running until we are asked to terminate
  while(!this.endRequested) {

    // Temporary to avoid losing some microseconds between
    // calculating passed time and updating lastIterationTime.
    DateTime now = DateTime.Now;

    // Calculate how much time has passed since the last iteration
    // and update the counter to the current time.
    TimeSpan timePassed = now - lastIterationTime;
    lastIterationTime = now;

    // Loop over all entities in the game, update the positions
    // and render their audible and visual representations.
    foreach(Entity entity in this.activeEntities) {
      entity.Update(timePassed);
      entity.Render();
    }

  }

}

For example, consider a bullet that’s moving through the game world at a desired speed of 1 unit per second:
If the game loop took 0.5 seconds, we move the bullet by 0.5 units per iteration of the loop. If the game loop took 1.0 seconds, however, we move the bullet by 1 unit per iteration. In both cases, after a second has passed, the bullet will have moved by 1 unit.

Sounds nice. But wait! This game loop still has major problems!

Take above game loop which updates objects sequentially based on the time that has passed since the last loop iteration. Now assume you were programming a racing game and two cars were headed towards each other for a big frontal crash. Both cars are moving at 10 units per second and are 10 units apart (so after half a second, they will both have moved by 5 units towards each other, reducing their distance to zero – bang!).

Let’s observe this scene with on two different computers!

John’s Computer

John’s computer is rather slow and is running your game at a breathtaking 1 frame per second, so each iteration of the game loop takes 1 second.

At the beginning of the update, both cars are still 10 units apart:

Image of two cars, 10 units apart from each other

Now the first car’s position will be updated. Since 1 second has passed from when the last iteration was performed, the car will be moved by a full 10 units:

Image of the two cars from before, left car has driven 10 units, hitting the right

Bang! The updated car has reached its opponent before he even had the chance to be updated. But weren’t they supposed to meet in the middle?

Rick’s Computer

Rick’s computer is a real performance monster. It is running your game at no less than 4 frames per second, making each iteration of the game loop take 0.25 seconds. Here, the collision will occur like this:

We’ll start with both cars 10 units apart from each other as before:

Image of two cars, 10 units apart from each other

Now the first car’s position is updated. Since only 0.25 seconds have passed from when the last iteration of the game loop took place, the car will be moved by 2.5 units only:

Image of the two cars from before, left car has moved by 2.5 units

Then the second car’s position is updated, moving the car by 2.5 units as well:

Image of the two cars from before, both cars have moved by 2.5 units

Nothing happened yet. So the game will render another frame and, 0.25 seconds later, we will be in the update phase once more. Again, the first car is moved another 2.5 units:

Image of the two cars from before, left car has moved another 2.5 units

Then the second car is also moved another 2.5 units:

Image of the two cars from before, both cars have moved by 5 units in total

Bang! In Rick’s computer, the cars have hit each other in the middle.

Conclusion

Now assume John and Rick were playing a multiplayer game and continued driving to the finish line after their accident. The game would be in a different state on each computer and slowly drift even further apart.

If you’re thinking that this problem could be solved with math, you’re right (keyword swept collision detection). But even then, due to the different steps by which both PCs update the game world, you would still have different rounding errors on each PC, causing the game states to drift apart nevertheless.

The next chapter will present a solution to all this which also happens to make the implementation of the game loop easier!

Time-Stepped Game Loop

In the previous chapter, we watches a performance-scaled time loop fail to achieve a uniform outcome on two computers with different performance. Now we will look at a game loop that solves this problem and is easier to implement at the same time.

That solution is to advance the game in fixed time steps. You will still have rounding errors and inaccuracies like the one demonstrated above, but at least they will be the same on all computers, allowing your game to record replays, have a multiplayer mode and in-game cut scenes with predictable outcome.

Here’s one method to achieve this:

// We update at 100 iterations per second
public const TimeSpan StepSize = TimeSpan.FromMilliseconds(10);

public void RunGame() {
  DateTime lastIterationTime = DateTime.Now;

  // Continue running until we are asked to terminate
  while(!this.endRequested) {

    DateTime now = DateTime.Now;

    // Perform as many steps as needed to catch up with the time
    // passed since the last frame. If the time taken is not
    // evenly dividable by the step size, this code will leave
    // the remainder for the next frame
    while(lastIterationTime + StepSize <= now) {

      // Update the positions of all entities in the game
      foreach(Entity entity in this.activeEntities)
        entity.Update(timePassed);

      lastIterationTime += StepSize;
    }

    // Loop over all entities in the game and render their
    // audible and visual representations
    foreach(Entity entity in this.activeEntities)
      entity.Render();

  }

}

The next section will discuss more reasons why we want to seperate the update phase from the drawing phase and draw the connection to the Game class in Microsoft's XNA framework which provides you with a premade game loop that you can use in your games.

Advanced Game Loops

In the previous sections, we have separated the game loop's update phase and drawing phase from each other. This not only makes sense when we want to use fixed time steps, but also:

  • Once your game becomes larger, you will not want to keep drawing and update all the objects in your game all the time to keep performance at reasonable levels. The subset of objects you're going to draw is different from the set of objects you'll need to move, so it makes sense to use two different loops.
  • The graphics card works independently of the CPU. So if you seperate the update phase and the drawing phase, the graphics card can work rendering the current frame while the CPU is already moving the objects to their new locations for the next frame. This can increase the performance of your game by as much as 100%.
  • If your game makes use of a physics engine, care has to be taken that time steps don't grow too large. Current physics engines tend to explode their simulation at large time steps. So even in a single player game, separating the update phase from the drawing phase makes sense so you can move the game objects at multiple smaller steps instead of a single big one.

All this causes our updating and drawing code to becoming much, much more complicated once we go from pong and tetris clones to intermediate games. Therefore it's wise to extract all this code into seperate methods which we'll call Update() and Draw() for now:

public const TimeSpan StepSize = Seconds(0.01); // 1/100th seconds

public void RunGame() {
  DateTime lastIterationTime = DateTime.Now;

  // Continue running until we are asked to terminate
  while(!this.endRequested) {

    DateTime now = DateTime.Now;

    // Perform as many steps as needed to catch up with the time
    // passed since the last frame. If the time taken is not
    // evenly dividable by the step size, this code will leave
    // the remainder for the next frame
    while(lastIterationTime + StepSize <= now) {
      Update(StepSize);

      lastIterationTime += StepSize;
    }

    Draw();

  }

}

public void Update(TimeSpan stepSize) {

  // Update the positions of all entities in the game
  foreach(Entity entity in this.activeEntities)
    entity.Update(timePassed);

}

public void Draw() {

  // Put all entities which could potentially be seen by the
  // player into a list
  buildPotentiallyVisibleSet();

  // Render the entities in aforementioned list
  foreach(Entity entity in this.potentiallyVisibleEntities)
    entity.Render();

}

Game Loops in the XNA Framework

By chance, the XNA Framework's Game class already manages a main loop just like the one shown in this article, using the exact same names for the Draw() and Update() methods. What a coincidence! ;)

This base class is contained in the Microsoft.Xna.Framework.Game assembly and can be used like shown in the following example:

public class MyGame : Microsoft.Xna.Framework.Game {

  /// <summary>Initializes the game</summary>
  public MyGame() {
  
    // Configure the main loop to advance in fixed steps of 10 ms
    this.TargetElapsedTime = TimeSpan.FromMilliseconds(10);
    this.IsFixedTimeStep = true;

  }

  /// <summary>
  ///   This is called when the game should draw itself.
  /// </summary>
  /// <param name="gameTime">Provides a snapshot of timing values.</param>
  protected override void Draw(GameTime gameTime) {

    // Put all entities which could potentially be seen by the
    // player into a list
    buildPotentiallyVisibleSet();

    // Render the entities in aforementioned list
    foreach(Entity entity in this.potentiallyVisibleEntities)
      entity.Render();

  }

  /// <summary>
  ///   Allows the game to run logic such as updating the world,
  ///   checking for collisions, gathering input and playing audio.
  /// </summary>
  /// <param name="gameTime">Provides a snapshot of timing values.</param>
  protected override void Update(GameTime gameTime) {

    // Update the positions of all entities in the game
    foreach(Entity entity in this.activeEntities)
      entity.Update(gameTime);

  }

}

Now that you know how to set up a game loop, your next topic should be how to use the XNA framework to display 2D or 3D graphics on the screen!

Leave a Reply

Your email address will not be published. Required fields are marked *

Please copy the string JBREbZ to the field below:

This site uses Akismet to reduce spam. Learn how your comment data is processed.