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:
- Explain why rendering an entity directly at its last fixed-step position causes visible stepping when the update rate is coarser than the frame rate
- Snapshot an entity’s previous position each fixed update so it can be blended against its current position
- Compute an alpha blend factor from the leftover accumulator time after draining fixed update steps
- Render an entity at its alpha-blended position instead of its raw simulated position
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.
- At the start of
Entity::update, before changing anything else, setpreviousPosition = currentPosition. - After that snapshot, advance
currentPositionbyvelocity * dt, as it already does. - Look at the wrap-around branch below it, which resets
currentPosition.xonce the entity leaves the screen. When that branch runs, also resetpreviousPosition.xto 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.
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.
- Immediately after the
while (accumulator >= timeStep)loop, computealphaasaccumulator.count() / timeStep.count(), cast tofloat.
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.
// ... 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.
- Set
noInterpShape's position directly tonoInterp.currentPosition, with no blending. This is the entity that will visibly step. - Compute
blendedasinterp.currentPosition * alpha + interp.previousPosition * (1.0f - alpha). - Set
interpShape's position toblended.
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.
// ... 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.