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

Interpolated Game Loop

Overview

This lab builds on the fixed-timestep accumulator from the Fixed-Timestep Game Loop lab and adds render interpolation. Starting from an SFML window that already updates two entities at a slow, chunky fixed rate, you will make one of them keep track of its previous and current simulated position and render it blended smoothly between the two, while the other is left rendering directly at its raw fixed-step position. Running the finished program shows both side by side in the same window, so the difference is something you can watch, not just read about.

Learning Objectives

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

Concept

The Fixed-Timestep Game Loop lab drained a real-time accumulator in fixed-size steps so update() always advances the simulation by the same amount regardless of frame rate. That solves simulation speed, but it creates a new problem for rendering. If a frame is drawn between two fixed updates and the entity is drawn at whatever position the last update() left it in, the entity only appears to move once per fixed step. When the fixed step is slow relative to the frame rate, that shows up as visible stepping or judder, even though the simulation itself is running at a perfectly steady rate.

The fix is render interpolation. Instead of asking an entity for a single position, it keeps two: previousPosition, a snapshot taken right before the most recent update advanced it, and currentPosition, the result of that update. After the accumulator's drain loop finishes for this frame, whatever time is left over in the accumulator is less than one full step, and dividing it by the step size gives a fraction between 0 and 1 called alpha. Rendering the entity at currentPosition * alpha + previousPosition * (1 - alpha) places it somewhere between where it was and where it now is, in proportion to how far through the next update the current frame actually falls. The entity moves smoothly every frame even though the simulation itself only advances in fixed jumps.

The pieces involved are sf::Vector2f arithmetic to compute the blend, std::chrono::duration<double> for the accumulator and fixed timeStep, and accumulator.count() / timeStep.count() to turn the leftover time into alpha.

Tasks

Task 1 - Snapshotting the previous position

Open InterpolatedGameloop.cpp and find Entity::update. Right now it only has to advance currentPosition, but interpolation needs to know where the entity was a moment ago as well as where it is now.

  1. At the start of Entity::update, before changing anything else, set previousPosition = currentPosition.
  2. After that snapshot, advance currentPosition by velocity * dt, as it already does.
  3. Look at the wrap-around branch below it, which resets currentPosition.x once the entity leaves the screen. When that branch runs, also reset previousPosition.x to match, so the entity does not appear to interpolate all the way back across the screen on the frame it wraps.

Success condition: temporarily printing previousPosition and currentPosition inside update shows previousPosition always trailing one fixed step behind currentPosition, except on the update where the entity wraps, where both jump together.

InterpolatedGameloop.cpp
struct Entity {
    sf::Vector2f previousPosition;
    sf::Vector2f currentPosition;
    sf::Vector2f velocity;

    void update(float dt, float screenWidth) {
        // TODO: snapshot previousPosition = currentPosition before advancing

        currentPosition += velocity * dt;

        // wrap around so the demo loops forever
        if (currentPosition.x > screenWidth + 30.0f) {
            currentPosition.x = -30.0f;
            // TODO: reset previousPosition.x to match, so the entity does
            // not interpolate across the wrap
        }
    }
};

Task 2 - Computing the alpha blend factor

In main, find the accumulator drain loop. This is the same fixed-timestep pattern from the Fixed-Timestep Game Loop lab, expressed with std::chrono::duration<double> instead of a plain long millisecond count, so it is given below as working code rather than a task. Once it finishes for this frame, whatever is left in accumulator is less than one timeStep, and that leftover is what alpha needs to capture.

  1. Immediately after the while (accumulator >= timeStep) loop, compute alpha as accumulator.count() / timeStep.count(), cast to float.

Success condition: temporarily printing alpha once per frame shows it staying within [0, 1), resetting toward 0 immediately after any frame where the drain loop ran.

InterpolatedGameloop.cpp
// ... inside the main loop, after measuring and capping frameTime
accumulator += frameTime;

while (accumulator >= timeStep) {
    noInterp.update(static_cast<float>(timeStep.count()), static_cast<float>(windowWidth));
    interp.update(static_cast<float>(timeStep.count()), static_cast<float>(windowWidth));
    accumulator -= timeStep;
}

// TODO: compute alpha as accumulator.count() / timeStep.count(), cast to float

Task 3 - Rendering the blended position

Both circles are currently positioned the same way. Split them apart so one renders exactly at its last simulated position and the other renders blended between its previous and current position.

  1. Set noInterpShape's position directly to noInterp.currentPosition, with no blending. This is the entity that will visibly step.
  2. Compute blended as interp.currentPosition * alpha + interp.previousPosition * (1.0f - alpha).
  3. Set interpShape's position to blended.

Success condition: running the program opens a window where the red circle visibly steps forward once per fixed update while the green circle moves smoothly across the screen at the same average speed.

InterpolatedGameloop.cpp
// ... after computing alpha

// TODO: set noInterpShape's position directly to noInterp.currentPosition

// TODO: compute blended = interp.currentPosition * alpha + interp.previousPosition * (1.0f - alpha)

// TODO: set interpShape's position to blended

The diagram below shows the same blend the interpolated entity uses each frame. previousPosition and currentPosition are the two fixed points from the last fixed update, and the green marker is the rendered position, sliding between them as alpha sweeps from 0 to 1. Use Pause and the speed slider to slow it down and read the live alpha value in the control bar.

Solution

Try the tasks above before revealing the solution.