Game Loop

From C# Gamedev Wiki

A game loop is a continuous loop that handles both running game logic and renders the game to the player.

Simple Game Loop

In the simplest example of a game loop, the game updates and renders at the same rate.

void GameLoop()
{
    while (true)
    {
        Update(); // updates game logic (handle input, physics, collision, interaction, etc.)
        Render(); // renders game to player (images, sounds, 3d models, text, etc.)
    }
}

This was a common way to design video games when they were first made, and came with the following pros and cons:

  • Pros
    • Even when the game slows down, physics and game logic are still processed in a consistent manner.
  • Cons
    • If either the Update() or the Render() take too much time, the game will slow down.
    • Game may not run at full speed on weaker hardware.
    • Different versions of the game must be made in order to run at different framerates.
      • Many old games had NTSC versions which ran at 60 fps and PAL versions which ran at 50 fps.
      • Game physics may not be consistent between versions.

Timesteps

There are two main types of timesteps:

  • Fixed Step - the game loop processes equal intervals of game time
  • Variable Step - the game loop processes varying intervals of game time based on how much real time has passed

While simple game loops originally all used fixed timesteps, some began to use variable timesteps as a way to address known issues:

// time of last iteration through game loop
DateTime lastTime;

void GameLoop()
{
    lastTime = GetRealTime();
    while (true)
    {
        // calculate elapsed real time
        var time = GetRealTime();
        var dt = time - lastTime;
        
        Update(dt); // updates game logic (handle input, physics, collision, interaction, etc.)
        Render(dt); // renders game to player (images, sounds, 3d models, text, etc.)
        
        lastTime = time;
    }
}

This solved a few important issues but introduced new ones at the same time:

  • Pros
    • Game runs at full speed on weaker hardware.
    • If either the Update() or the Render() take too much time, the game keeps running at full speed.
    • One version of the game can run on hardware with different framerates.
  • Cons
    • When the game slows down
      • Physics and game logic are not processed in a consistent manner.
      • Player inputs may be dropped.

Independent Update and Render

Ideally game logic should be fixed step for a consistent experience, but game rendering should be able to run independently at the display's refresh rate. To achieve this Update() must be able to run independently of Render(), a separation many programmers are familiar with (back end / front end, model / view, etc.).

Frame Interpolation

The easiest way to implement this separation is with Frame Interpolation. The last two frames of the game state must be stored, and then Render() simply interpolates between them based on the current time.

// fixed step processed by Update()
TimeSpan fixedStep;

// timestamp of 2nd most recently processed game logic tick 
DateTime previousTickTime;
// timestamp of most recently processed game logic tick 
DateTime currentTickTime;

// 2nd most recently processed game logic tick data
object previousTickData;
// most recently processed game logic tick data
object currentTickData;

void GameLoop()
{
    while (true)
    {
        var time = GetRealTime();
        // Update at fixed step
        while (time >= currentTickTime + fixedStep)
        {
            var newTickData = Update(fixedStep); // updates game logic (handle input, physics, collision, interaction, etc.)
            // update tick time and data
            previousTickTime = currentTickTime;
            previousTickData = currentTickData;
            currentTickTime += fixedStep;
            currentTickData = newTickData;
        }
        
        // calculate how far we need to interpolate towards next fixed timestep
        time = GetRealTime();
        var interp = (time - currentTickTime).TotalSeconds / fixedStep.TotalSeconds;
        var interpTickData = Interpolate(previousTickData, currentTickData, interp);
        Render(interpTickData); // renders game to player (images, sounds, 3d models, text, etc.)
    }
}

This is the most common approach used by commercial games today.

  • Pros
    • Easy to implement.
    • Works at framerates both greater than tick rate and less than tick rate.
      • Inputs may not be processed at tick rate when tick rate exceeds framerate.
  • Cons
    • Introduces render delay equal to the fixed timestep.
    • Interpolation may produce an incorrect state
      • For example when a player teleports from one frame to the next, the interpolated frame(s) may render the player somewhere in between the two locations, possibly inside terrain or an enemy.
      • This can be fixed by implementing the ability to mark certain fields as 'no-interpolation' on certain frames, but requires a decent amount of extra work.

Partial Step

If eliminating render delay is a high priority or interpolation is unreliable, implementing the ability to process partial steps is a better solution. This requires a new UpdatePartial() method, which is intended to handle game logic similarly to Update(), but only creating a temporary tickData object instead of a permanent one.

// fixed step processed by Update()
TimeSpan fixedStep;

// timestamp of most recently processed game logic tick 
DateTime currentTickTime;

// most recently processed game logic tick data
object currentTickData;

void GameLoop()
{
    while (true)
    {
        var time = GetRealTime();
        // Update at fixed step
        while (time >= currentTickTime + fixedStep)
        {
            var newTickData = Update(fixedStep); // updates game logic (handle input, physics, collision, interaction, etc.)
            // update tick time and data
            currentTickTime += fixedStep;
            currentTickData = newTickData;
        }
        
        // Calculate partial step size
        time = GetRealTime();
        var interp = (time - currentTickTime).TotalSeconds / fixedStep.TotalSeconds;
        var partialStep = fixedStep * interp;
        // Update partial step
        var partialTickData = UpdatePartial(partialStep); // updates game logic (handle input, physics, collision, interaction, etc.)
        
        Render(partialTickData); // renders game to player (images, sounds, 3d models, text, etc.)
    }
}

This approach still runs game logic for partial steps instead of relying on interpolation, meaning that things like teleports and collisions will still occur properly.

  • Pros
    • Works at framerates both greater than tick rate and less than tick rate.
      • Inputs may not be processed at tick rate when tick rate exceeds framerate.
  • Cons
    • Harder to implement.
    • Consumes more processing power since an additional partial step is calculated for each rendered frame.
      • Not a good fit for CPU heavy games or games with a large game state.
      • May be able to process partial steps for a small section of the game state and extrapolate the rest.
        • For example process collisions for objects near the player, but ignore collisions for faraway enemies during partial steps only.

Multi Threaded

Another important improvement comes from a multi threaded game loop, in which Update() and Render() are run on separate threads. This prevents slowdowns on one thread from affecting the other, creating smoother and more consistent gameplay.

Be Warned! Multi threaded code is difficult to debug and can give rise to many difficult issues. Beginners are recommended to stick with a single threaded game loop, as the problems of multi threading will often outweigh its benefits.

Be careful when using async / await! They can lead to many types of bugs and errors when working on a game, and it is generally better to define all the threads/tasks you will need at startup and utilize them as needed.

Frame Interpolation

// fixed step processed by Update()
TimeSpan fixedStep;

// timestamp of 2nd most recently processed game logic tick 
DateTime previousTickTime;
// timestamp of most recently processed game logic tick 
DateTime currentTickTime;

// 2nd most recently processed game logic tick data
object previousTickData;
// most recently processed game logic tick data
object currentTickData;

// Lock object
object tickDataLock = new object();

void GameLoopThread1()
{
    while (true)
    {
        var time = GetRealTime();
        // Update at fixed step
        while (time >= currentTickTime + fixedStep)
        {
            var newTickData = Update(fixedStep); // updates game logic (handle input, physics, collision, interaction, etc.)
            // update tick time and data
            lock (tickDataLock)
            {
                previousTickTime = currentTickTime;
                previousTickData = currentTickData;
                currentTickTime += fixedStep;
                currentTickData = newTickData;
            }
        }
    }
}

void GameLoopThread2()
{
    while (true)
    {
        var time = GetRealTime();
        // calculate how far we need to interpolate towards next fixed timestep
        var interp = 0.0;
        object tickData0;
        object tickData1;
        lock (tickDataLock)
        {
            interp = (time - currentTickTime).TotalSeconds / fixedStep.TotalSeconds;
            tickData0 = previousTickData;
            tickData1 = currentTickData;
        }
        var interpTickData = Interpolate(tickData0, tickData1, interp);
        Render(interpTickData); // renders game to player (images, sounds, 3d models, text, etc.)
    }
}

Partial Step

// fixed step processed by Update()
TimeSpan fixedStep;

// timestamp of most recently processed game logic tick 
DateTime currentTickTime;

// most recently processed game logic tick data
object currentTickData;

// Lock object
object tickDataLock = new object();

void GameLoopThread1()
{
    while (true)
    {
        var time = GetRealTime();
        // Update at fixed step
        while (time >= currentTickTime + fixedStep)
        {
            var newTickData = Update(fixedStep); // updates game logic (handle input, physics, collision, interaction, etc.)
            // update tick time and data
            lock (tickDataLock)
            {
                currentTickTime += fixedStep;
                currentTickData = newTickData;
            }
        }
    }
}

void GameLoopThread2()
{
    while (true)
    {
        var time = GetRealTime();
        // Calculate partial step size
        var interp = 0.0;
        TimeSpan partialStep;
        object fixedTickData;
        lock (tickDataLock)
        {
            interp = (time - currentTickTime).TotalSeconds / fixedStep.TotalSeconds;
            partialStep = fixedStep * interp;
            fixedTickData = currentTickData;
        }
        // Update partial step
        var partialTickData = UpdatePartial(fixedTickData, partialStep); // updates game logic (handle input, physics, collision, interaction, etc.)
        
        Render(partialTickData); // renders game to player (images, sounds, 3d models, text, etc.)
    }
}

Note that the partial update actually occurs on the render thread, not the update thread.

Notes

In practice game loops may have a lot more code. They may wait a certain amount of time between passes, do less processing on certain passes than others, contain exit logic, etc.