University of Wales Trinity Saint David
ACCB5017 - Games Architecture and Mechanics

Fixed-Timestep Game Loop

Overview

This lab builds a fixed-timestep game loop, the pattern that keeps a game's simulation running at a constant rate no matter how fast or slow the machine is drawing frames. Starting from a loop that measures real elapsed time with std::chrono, you will build up an accumulator that banks that elapsed time and spends it in fixed-size update steps, guard the loop against a stalled frame queuing up more work than it can ever catch up on, and finish with a way to observe how many update steps actually ran over a timed period.

Learning Objectives

By the end of this lab you will be able to:

Concept

A loop that calls update() once per rendered frame ties the simulation's speed to how fast the machine can draw. A game that moves an object by a fixed amount every frame runs twice as fast on a machine that renders 120 frames a second as one that renders 60, and stutters unpredictably on a machine whose frame rate varies from moment to moment. The fix is to separate how often the game state updates from how often a frame is drawn.

A fixed-timestep loop does this with an accumulator. Each iteration measures the real time elapsed since the previous iteration and adds it to a running total, the lag. An inner loop then drains that lag in fixed-size chunks, calling update() once per chunk, until less than one chunk's worth of time remains. If a frame takes 40 milliseconds and the fixed step is roughly 16.67 milliseconds, update() runs twice that iteration and carries the remainder forward. If real time between frames spikes, for example while paused at a breakpoint or during a window drag, the accumulated lag can grow large enough that the inner loop can never fully drain it. Capping the elapsed time added to lag on any single iteration prevents that spiral of death.

The C++ pieces involved are std::chrono::system_clock::now() to read the current time, time_since_epoch() and duration_cast<std::chrono::milliseconds> to turn it into a millisecond count, and a plain while loop to drain the accumulator.

Tasks

Task 1 - Measuring elapsed time with std::chrono

Open GamesLoops.cpp and find the getCurrentTime function. This is the building block the rest of the loop depends on. It needs to return the current time as a single long value in milliseconds so two calls to it can be subtracted to find elapsed time.

  1. Get the current time point from std::chrono::system_clock::now().
  2. Convert that time point into a duration since the epoch with time_since_epoch().
  3. Convert the duration into milliseconds with std::chrono::duration_cast<std::chrono::milliseconds> and return its count().

Success condition: calling getCurrentTime() twice a moment apart, for example in two consecutive std::cout statements at the top of main, prints two close but increasing millisecond values.

GamesLoops.cpp
long getCurrentTime()
{
    // TODO: get the current time_point from std::chrono::system_clock

    // TODO: convert the time_point to a duration since the epoch

    // TODO: convert the duration to milliseconds and return the count
}

Task 2 - Accumulating lag and running fixed update steps

In main, set up the loop that tracks time between iterations and drains it into fixed-size update steps.

  1. Before the loop, record the current time in oldTime and set lag to 0.
  2. At the top of each iteration, read the current time into currentTime, subtract oldTime to get elapsedTime, then set oldTime = currentTime for the next iteration.
  3. Add elapsedTime onto lag.
  4. Add an inner while loop that keeps calling update() and subtracting ms_per_update from lag for as long as lag is at least one ms_per_update chunk.

Success condition: update() is called a consistent number of times per second of real time, regardless of how much other work the outer loop does per iteration.

GamesLoops.cpp
int main()
{
    const float ms_per_update = 16.67f;
    bool gameOver = false;

    long oldTime = getCurrentTime();
    long lag = 0;

    while (!gameOver)
    {
        // TODO: read the current time and work out elapsedTime since oldTime
        // TODO: update oldTime for the next iteration
        // TODO: add elapsedTime onto lag

        // TODO: drain lag in fixed ms_per_update chunks, calling update() for each one
    }
}

Task 3 - Capping elapsed time to avoid the spiral of death

If the outer loop is ever stalled for a long real-world period, for example while paused at a breakpoint or while the user is dragging the console window, elapsedTime for that one iteration can be huge. Adding all of it onto lag can leave the inner update loop with more time to drain than it can ever catch up on in real time, since draining it takes real time too and more real time keeps arriving. This is the spiral of death.

  1. Choose a maximum frame time, for example 250 milliseconds.
  2. After computing elapsedTime and before adding it onto lag, clamp it so it never exceeds that maximum.

Success condition: pausing the program in the debugger for several seconds on a breakpoint inside the loop and then resuming no longer causes a long burst of back-to-back update() calls once execution continues.

GamesLoops.cpp
const long max_frame_time = 250;

// ... inside the loop, after computing elapsedTime
// TODO: if elapsedTime is greater than max_frame_time, clamp it to max_frame_time

Task 4 - Counting updates and exiting after a timed run

Right now the loop never sets gameOver to true, so the program never stops on its own. Add a way to observe how many fixed update steps actually ran, and use it to end the program automatically.

  1. Add a file-scope updateCount counter, and increment it inside update().
  2. Record a startTime alongside oldTime before the loop begins.
  3. At the end of each outer iteration, check whether currentTime - startTime has reached a chosen run duration, for example 5000 milliseconds, and if so set gameOver = true.
  4. After the loop ends, print updateCount to the console.

Success condition: the program now exits on its own after roughly five seconds and prints an update count close to run_duration_ms / ms_per_update.

GamesLoops.cpp
int updateCount = 0;

void update()
{
    // TODO: increment updateCount
}

// ... in main, before the loop
const long run_duration_ms = 5000;
long startTime = oldTime;

// ... at the end of each outer iteration
// TODO: if currentTime - startTime >= run_duration_ms, set gameOver = true

// ... after the loop
// TODO: print updateCount to std::cout

The diagram below traces one iteration of the loop once all four tasks are wired together, from the moment elapsedTime is measured through to the point the loop either drains its lag and renders, or exits after the timed run.

Solution

Try the tasks above before revealing the solution.