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:
- Read the current time in milliseconds using std::chrono::system_clock
- Accumulate elapsed time into a lag value and drain it in fixed-size update steps
- Cap a single frame’s elapsed time to prevent the loop entering a spiral of death
- Measure and report how many fixed update steps ran over a timed period
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.
- Get the current time point from
std::chrono::system_clock::now(). - Convert that time point into a duration since the epoch with
time_since_epoch(). - Convert the duration into milliseconds with
std::chrono::duration_cast<std::chrono::milliseconds>and return itscount().
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.
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.
- Before the loop, record the current time in
oldTimeand setlagto0. - At the top of each iteration, read the current time into
currentTime, subtractoldTimeto getelapsedTime, then setoldTime = currentTimefor the next iteration. - Add
elapsedTimeontolag. - Add an inner
whileloop that keeps callingupdate()and subtractingms_per_updatefromlagfor as long aslagis at least onems_per_updatechunk.
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.
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.
- Choose a maximum frame time, for example
250milliseconds. - After computing
elapsedTimeand before adding it ontolag, 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.
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.
- Add a file-scope
updateCountcounter, and increment it insideupdate(). - Record a
startTimealongsideoldTimebefore the loop begins. - At the end of each outer iteration, check whether
currentTime - startTimehas reached a chosen run duration, for example5000milliseconds, and if so setgameOver = true. - After the loop ends, print
updateCountto 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.
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.