Introduction
Hello! In our previous lesson, we established a crucial mechanism for making our game logic frame-rate independent by implementing a variable time-step using deltaTime. This ensures that an object moves at the same speed whether the game is running at 60 FPS or 500 FPS.
However, as we briefly touched upon, this approach has a significant flaw when it comes to physics and other deterministic simulations. Today, we will address this by implementing a professional-grade game loop. Our learning outcome is to separate a fixed-timestep update loop for physics from a variable-timestep render loop.
This is the standard architecture used in most commercial game engines. It provides the best of both worlds: the stability and predictability of a fixed update rate for simulation, and the fluid, high-frame-rate visuals of a variable update rate for rendering.
The core concept should be familiar from your chemical engineering background. When solving differential equations numerically to model a process, you use a fixed time step, , to advance the simulation. Using a variable or excessively large can lead to numerical instability and inaccurate results. We face the exact same challenge in game physics, and the solution is conceptually identical.
1. The Problem with Variable Timesteps for Physics
In the last lesson, we celebrated how deltaTime makes movement consistent across different hardware. So, why is it suddenly a problem? While it's great for simple visual updates, it's detrimental to tasks that require precision and consistency, like physics.
The following video provides an excellent summary of the issues.
Watch this video from Vittorio Romeo to understand the specific drawbacks of relying solely on a variable delta time for all game logic.
Watch from the beginning to 03:59. The first two minutes will be a quick recap of what we learned last lesson. The crucial part is from 02:36 onward, where he explains why this seemingly perfect solution can fail for collision detection and physics, and how it makes the simulation non-deterministic.
To summarize the key problems:
- Instability: A sudden lag spike can cause
deltaTimeto become very large. If you're updating position withposition += velocity * deltaTime, a hugedeltaTimecan cause an object to teleport a large distance in a single frame. This can make it "tunnel" through walls or other objects, breaking your collision detection. - Non-Determinism: The simulation's outcome can vary depending on the frame rate. Two players running the same game with the same inputs could see different results if their computers have different performance. This makes networked multiplayer, replays, and reliable bug reproduction nearly impossible.
2. The Solution: Decoupling Update from Render
The solution is to decouple our simulation logic from our rendering logic.
- Simulation (Physics): Will run at a constant, fixed rate (e.g., 60 times per second). We'll call this the "tick rate". Every update will use the same fixed
dt. - Rendering: Will run as fast as the hardware allows, using whatever time is left over.
This means we might run multiple physics updates in one frame if rendering is slow, or run zero physics updates if rendering is very fast.

To manage this, we use an accumulator. Think of it as a time bank. In each main loop iteration, we measure the real time that has passed (frameTime) and add it to our accumulator. Then, we check if we have enough "time" in the bank to perform one or more fixed-sized physics steps.
The following article is a classic in game development literature and explains this concept beautifully.
The article 'Fix Your Timestep!' by Glenn Fiedler is the definitive guide on this topic. We'll focus on the section that introduces the final, robust game loop structure.
Read the section titled 'Free the physics'. Pay close attention to the mental model of the renderer 'producing' time and the simulation 'consuming' it. Study the code block that introduces the accumulator variable.
The core logic, which we will soon implement in C, looks like this:
// A constant representing our fixed time step for physics
const double FIXED_DT = 1.0 / 60.0;
// The time accumulator
double accumulator = 0.0;
// In the main loop...
frameTime = calculate_how_long_the_last_frame_took();
accumulator += frameTime;
// Consume time from the accumulator in fixed-size chunks
while (accumulator >= FIXED_DT) {
update_physics(FIXED_DT);
accumulator -= FIXED_DT;
}
render_game();
3. Implementing the Fixed Timestep Loop in C
Let's translate this theory into code. We'll modify the main loop we built in the previous lesson.
First, define the constants for our fixed update loop outside the main loop. A tick rate of 60 Hz is a common starting point.
// main.c
// Add these new constants
#define PHYSICS_TICK_RATE 60
const double FIXED_DT = 1.0 / PHYSICS_TICK_RATE;
int main(void) {
// ... window creation and other setup ...
double lastFrameTime = 0.0;
double accumulator = 0.0;
while (!glfwWindowShouldClose(window)) {
// --- Time calculation ---
double currentFrameTime = glfwGetTime();
double deltaTime = currentFrameTime - lastFrameTime;
lastFrameTime = currentFrameTime;
// Add the real frame time to the accumulator
accumulator += deltaTime;
// --- Input Processing (happens every frame) ---
process_input(window);
// --- Fixed Physics Update ---
while (accumulator >= FIXED_DT) {
// Update physics, game logic, etc. using a fixed time step
update_game_state(FIXED_DT);
accumulator -= FIXED_DT;
}
// --- Rendering (happens every frame) ---
render_game(); // We will need to modify this soon
glfwSwapBuffers(window);
glfwPollEvents();
}
// ... cleanup ...
return 0;
}
Notice a few key changes:
- We have a new function
update_game_state(). This is where all deterministic logic, like physics, will go. It always receivesFIXED_DTas its time step. - General
process_input()can still happen once per frame. This is for checking button presses. The effect of that input (e.g., applying a force to an object) would happen insideupdate_game_state(). - The variable
deltaTimeis now only used to feed theaccumulator. It is no longer passed to our game logic functions.
4. The Final Touch: Smoothing with Interpolation
We have solved the physics stability problem, but we've created a new, visual one. Our physics state now only changes at discrete 1/60th-of-a-second intervals. However, our render_game() function is called at a different, variable rate.
This mismatch means the rendered object will appear to be "stuck" at its old position for a few frames, then instantly "jump" to the new one. This causes a noticeable visual stutter or jitter, even at high frame rates.
The solution is to interpolate the object's rendered position. When we render, we figure out how far we are, time-wise, between the last physics update and the next one. This value, often called alpha, will be between 0.0 and 1.0. We can then draw our object at a position blended between its previous state and its current state.
alpha = accumulator / FIXED_DT;
render_position = current_position * alpha + previous_position * (1.0 - alpha);
This is a classic linear interpolation, or "lerp". The resources below explain this final, crucial step.
Let's return to 'Fix Your Timestep!' to see the theoretical solution for visual stutter.
Read the final section, 'The final touch'. It explains exactly why the stuttering happens and how to use the remainder in the accumulator to calculate a blending factor for interpolation.
This next video provides a practical demonstration of both the problem and the solution.
13. C++ Fixed Delta Time Update Loop Celeste Clone
This video from Cakez shows the fixed timestep loop in action. You'll see the visual jitter he mentions, and then see how he fixes it with interpolation.
Watch from 08:17 to 14:34. First, he separates the logic into a simulate function (our update_game_state). Then, from 11:32, he explains the need for interpolation and implements a 'lerp' function to smooth out the movement. Notice how the cube is 'jiggly' before interpolation and smooth after. The code is in C++, but the logic and math are identical.
5. Putting It All Together
To implement interpolation, we need to store the state of our objects from the previous physics tick as well as the current one.
Let's imagine a simple GameState struct and see the final main loop.
// A simple struct to hold our object's state
typedef struct {
vec3 position;
} GameObjectState;
// ...
#define PHYSICS_TICK_RATE 60
const double FIXED_DT = 1.0 / PHYSICS_TICK_RATE;
int main(void) {
// ... setup ...
double lastFrameTime = 0.0;
double accumulator = 0.0;
// We need two states to interpolate between
GameObjectState previousState = { .position = {0.0f, 0.0f, 0.0f} };
GameObjectState currentState = { .position = {0.0f, 0.0f, 0.0f} };
while (!glfwWindowShouldClose(window)) {
double currentFrameTime = glfwGetTime();
double deltaTime = currentFrameTime - lastFrameTime;
lastFrameTime = currentFrameTime;
accumulator += deltaTime;
process_input(window);
while (accumulator >= FIXED_DT) {
// Before updating, the 'current' state becomes the 'previous' one
previousState = currentState;
// Update the 'current' state using fixed delta time
update_game_state(¤tState, FIXED_DT);
accumulator -= FIXED_DT;
}
// --- Interpolation for Rendering ---
const double alpha = accumulator / FIXED_DT;
GameObjectState renderState;
// renderState.position = currentState.position * alpha + previousState.position * (1.0 - alpha);
// In C with glm, this is:
vec3 scaled_current, scaled_previous;
glm_vec3_scale(currentState.position, (float)alpha, scaled_current);
glm_vec3_scale(previousState.position, (float)(1.0 - alpha), scaled_previous);
glm_vec3_add(scaled_current, scaled_previous, renderState.position);
render_game(renderState); // Pass the interpolated state to the render function
glfwSwapBuffers(window);
glfwPollEvents();
}
// ...
}
This loop structure is robust, efficient, and is the foundation upon which you'll build the rest of your game engine.
Conclusion
Congratulations! You have now implemented one of the most important architectural patterns in game development. By separating your fixed-rate simulation from your variable-rate rendering, you've created a system that is both physically stable and visually smooth.
Key Takeaways:
- Using a variable timestep for physics leads to instability and non-deterministic behavior.
- A fixed-timestep loop runs simulation logic at a constant rate (e.g., 60 Hz) for consistency.
- An accumulator is used to bank the real time that has passed, which is then consumed in fixed-size chunks by the physics simulation.
- Interpolation between the previous and current physics states is essential to smooth out rendering and prevent visual stuttering.
Preview of the Next Lesson:
We've established the core pulse of our engine. Now, we need something to simulate. What is an object in our game world? In the next lesson, we will begin answering that by tackling the learning outcome: Design a basic Entity struct to represent any object in the game world. This will be the fundamental building block for players, enemies, bullets, and everything else that exists in your game.