Tutorial 48: Fixed vs Variable Timestep
What you’ll learn
- What
IsFixedTimeStepandTargetElapsedTimeactually do. - How a fixed step catches up after a slow frame.
- Reading
ElapsedGameTimefor a variable step, and which model suits which game. - Running physics on a fixed step, and how to interpolate the render when you need smoother motion than the update rate.
Before you start — Tutorial 05: The Game Loop — this is the detailed treatment of the timing model introduced there.
The GameTime passed to Update and Draw tells you how much real time has elapsed. Whether that duration is fixed or variable depends on the IsFixedTimeStep flag.
IsFixedTimeStep flag
class MyGame final : public Game {
public:
MyGame() : graphics_(this) {
// Fixed timestep (the default): Update runs once per 1/60 s step
setIsFixedTimeStepProperty(true);
setTargetElapsedTimeProperty(TimeSpan::FromSeconds(1.0 / 60.0));
// Variable timestep: Update runs once per frame, with the measured elapsed time
// setIsFixedTimeStepProperty(false);
}
private:
GraphicsDeviceManager graphics_;
};
TargetElapsedTime — default 1/60 s
// 60 Hz (default)
setTargetElapsedTimeProperty(TimeSpan::FromSeconds(1.0 / 60.0));
// 30 Hz (mobile battery saving)
setTargetElapsedTimeProperty(TimeSpan::FromSeconds(1.0 / 30.0));
// 120 Hz (high-refresh displays)
setTargetElapsedTimeProperty(TimeSpan::FromSeconds(1.0 / 120.0));
How fixed timestep works
With IsFixedTimeStep = true each pass of the game loop (a “tick”) waits until at least one TargetElapsedTime of real time has accumulated, then calls Update once per whole step in the accumulator, and finally calls Draw once. If a frame ran long, the next tick calls Update several times in a row to catch up (the accumulated time is capped at Game::MaxElapsedTime, 500 ms, so a long stall cannot trigger an endless catch-up). Because Draw runs once per tick, Draw can never run more often than Update in fixed mode: on a 144 Hz display the game still updates and draws 60 times per second unless you lower TargetElapsedTime.
When the game is falling behind — the accumulated update lag reaches five steps — GameTime::getIsRunningSlowlyProperty() becomes true, and it clears again once the lag is gone.
void Update(GameTime& gameTime) override {
Game::Update(gameTime); // updates registered components (see Tutorial 47)
if (gameTime.getIsRunningSlowlyProperty()) {
// Skip expensive optional work (particle updates, LOD generation, etc.)
}
// ElapsedGameTime is exactly TargetElapsedTime in every fixed-step Update, except the
// very first one of the game, which sees TimeSpan::Zero (XNA's game clock).
float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty()); // 1/60, or 0 on the first Update
}
Two details of that clock follow XNA 4.0 (they changed after alpha.1, when CNA still copied FNA’s): the first Update runs with ElapsedGameTime == 0, and TotalGameTime is the game time before the current step, so it advances only after Update returns. Inside Draw, ElapsedGameTime is TargetElapsedTime multiplied by the number of steps that ran in this tick (1 on a normal frame, more after a catch-up).
Web builds use their own frame body. Under Emscripten the browser drives the loop from requestAnimationFrame, and CNA’s frame callback keeps a separate fixed-step accumulator (frame time clamped to 250 ms). It ignores IsFixedTimeStep = false, always passes exactly the target step, never sets IsRunningSlowly, advances TotalGameTime before calling Update, and gives the first update a full step instead of zero. Code that must behave identically on the web should not depend on the first-update zero or on a variable step arriving; the accumulator pattern later in this tutorial works with both.
Variable timestep with ElapsedGameTime
// With IsFixedTimeStep = false:
void Update(GameTime& gameTime) override {
float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
// dt varies each frame (e.g. 0.014 to 0.020 seconds on a 60Hz display)
player_.position = player_.position + player_.velocity * dt; // frame-rate independent
}
In variable mode Update and Draw each run once per loop pass, as fast as the platform allows. dt is the measured time since the previous pass; after a long pause, such as loading, call Game::ResetElapsedTime() so the next Update does not see one enormous dt. (pos += vel * dt also compiles, but compound assignment on vectors is a CNA extension, not XNA.)
When to use each
| Mode | When to use |
|---|---|
| Fixed timestep | Physics simulations, deterministic replays, network synchronisation, anything sensitive to integration stability |
| Variable timestep | Pure rendering games, input-driven UIs, tools, anything that does not integrate state over time |
Physics on the fixed step
The built-in fixed step already gives you what a physics simulation needs: every Update integrates the same dt, so the result does not depend on the frame rate. For most games that is all you need, and it is the simplest thing to get right.
struct PhysicsBody {
Vector3 position;
Vector3 prevPosition; // used by the interpolation variant below
Vector3 velocity;
};
class PhysicsDemo final : public Game {
public:
PhysicsDemo() : graphics_(this) {
// Fixed physics at 60 Hz (this is also the default)
setIsFixedTimeStepProperty(true);
setTargetElapsedTimeProperty(TimeSpan::FromSeconds(1.0 / 60.0));
}
protected:
void Initialize() override {
Game::Initialize();
body_.position = Vector3(0, 10, 0);
body_.velocity = Vector3(2, 0, 0);
}
void LoadContent() override {
effect_ = std::make_unique<BasicEffect>(getGraphicsDeviceProperty());
effect_->VertexColorEnabled = true;
// ... build sphere mesh ...
}
// One call per 1/60 s step (after a slow frame, several calls in a row)
void Update(GameTime& gameTime) override {
Game::Update(gameTime);
// dt is 1/60 s, except on the game's very first Update, where it is 0
const float dt = static_cast<float>(
gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
// Gravity
body_.velocity.Y -= 9.81f * dt;
body_.position = body_.position + body_.velocity * dt;
// Floor bounce
if (body_.position.Y < 0.0f) {
body_.position.Y = 0.0f;
body_.velocity.Y = -body_.velocity.Y * 0.7f; // damping
}
}
// Called once per tick, after the Update call(s) of that tick
void Draw(const GameTime& gameTime) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::CornflowerBlue);
Matrix world = Matrix::CreateTranslation(body_.position);
Matrix view = Matrix::CreateLookAt(
Vector3(0, 5, 15), Vector3(0, 3, 0), Vector3::Up);
Matrix proj = Matrix::CreatePerspectiveFieldOfView(
MathHelper::PiOver4, 800.0f / 600.0f, 0.1f, 100.0f);
effect_->setWorldProperty(world);
effect_->setViewProperty(view);
effect_->setProjectionProperty(proj);
gd.SetVertexBuffer(vb_.get());
for (auto& pass : effect_->getCurrentTechniqueProperty()->getPassesProperty()) {
pass.Apply();
gd.DrawPrimitives(PrimitiveType::TriangleList, 0, primitiveCount_);
}
// No Present(): Game presents the frame after Draw() returns.
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<BasicEffect> effect_;
std::unique_ptr<VertexBuffer> vb_;
int primitiveCount_ = 0;
PhysicsBody body_;
};
Interpolating the render (your own accumulator)
Interpolation smooths motion when you render more often than you simulate. The built-in fixed step cannot do this for you: inside Draw, ElapsedGameTime is a whole multiple of TargetElapsedTime (at least one step), so “elapsed divided by target” is always ≥ 1 and clamps to 1; an interpolation built on it would silently do nothing. To render between two physics states, switch to a variable step and run your own fixed-step accumulator, then use the leftover fraction as the interpolation factor:
class InterpolatedDemo final : public Game {
public:
InterpolatedDemo() : graphics_(this) {
setIsFixedTimeStepProperty(false); // CNA hands us the real elapsed time
}
protected:
void Initialize() override {
Game::Initialize();
body_.position = Vector3(0, 10, 0);
body_.prevPosition = body_.position;
body_.velocity = Vector3(2, 0, 0);
}
void Update(GameTime& gameTime) override {
Game::Update(gameTime);
// Clamp the frame time so one long stall (a window drag, a debugger stop)
// cannot make us run hundreds of catch-up steps
const double frameTime = std::min(
gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty(), 0.25);
accumulator_ += frameTime;
while (accumulator_ >= kStep) {
body_.prevPosition = body_.position; // state one step ago
Step(static_cast<float>(kStep));
accumulator_ -= kStep;
}
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::CornflowerBlue);
// Where we are between the last two physics states, in [0, 1)
const float alpha = static_cast<float>(accumulator_ / kStep);
Vector3 renderPos = Vector3::Lerp(body_.prevPosition, body_.position, alpha);
Matrix world = Matrix::CreateTranslation(renderPos);
// ... set world/view/projection on the effect and draw, as in the previous example ...
}
private:
void Step(float dt) {
body_.velocity.Y -= 9.81f * dt;
body_.position = body_.position + body_.velocity * dt;
if (body_.position.Y < 0.0f) {
body_.position.Y = 0.0f;
body_.velocity.Y = -body_.velocity.Y * 0.7f;
}
}
static constexpr double kStep = 1.0 / 60.0;
GraphicsDeviceManager graphics_;
PhysicsBody body_;
double accumulator_ = 0.0;
};
This pattern also behaves the same on the web frame body described above, because it does not care whether Update receives a measured or a fixed dt. If you want a fixed update rate but want to skip a frame’s drawing, call Game::SuppressDraw() from Update.
Key takeaways
- Use
IsFixedTimeStep = true(the default) for any simulation that integrates state over time — everyUpdatesees the samedt, which removes the jitter of variable frame rates. - The first
UpdateseesElapsedGameTime == 0, so a state that divides bydtmust guard against zero. - Fixed mode calls
Drawonce per tick, so it cannot render faster than it updates. For interpolated rendering, use a variable step with your own accumulator, storing the previous state before each physics step. - Check
gameTime.getIsRunningSlowlyProperty()to skip optional work when the CPU cannot keep up. gameTime.getTotalGameTimeProperty()is game time, not wall-clock time: it is the sum of theElapsedGameTimevalues of the completed updates, so in fixed mode it moves in whole steps. Use it for time-based animation that should follow the simulation.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- GameTime and the timestep: exact clock semantics — What CNA's game clock reports: GameTime protection, fixed-step catch-up with worked numbers, IsRunningSlowly hysteresis versus XNA 4.0, ResetElapsedTime, vsync versus timestep, and the browser clock.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-GAP-063: A fixed-step TargetElapsedTime above 500 ms never produces an Update from Game::Tick, essentially the same ceiling XNA 4.0 has — With IsFixedTimeStep true, Game::Tick clamps the accumulator to MaxElapsedTime (500 ms) before its update loop, so a TargetElapsedTime above 500 ms is never reached: Update does not run while Draw keeps running, at about