Tutorial 48: Fixed vs Variable Timestep
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: Update is called exactly 60 times per second
setIsFixedTimeStep(true);
setTargetElapsedTime(TimeSpan::FromSeconds(1.0 / 60.0));
// Variable timestep: Update is called as fast as possible
// setIsFixedTimeStep(false);
}
};
TargetElapsedTime — default 1/60 s
// 60 Hz (default)
setTargetElapsedTime(TimeSpan::FromSeconds(1.0 / 60.0));
// 30 Hz (mobile battery saving)
setTargetElapsedTime(TimeSpan::FromSeconds(1.0 / 30.0));
// 120 Hz (high-refresh displays)
setTargetElapsedTime(TimeSpan::FromSeconds(1.0 / 120.0));
How fixed timestep works
With IsFixedTimeStep = true, the game loop accumulates real elapsed time and calls Update repeatedly with the fixed delta until the accumulator is drained. Draw is called once after. If the game cannot keep up, IsRunningSlowly is set on the GameTime.
void Update(GameTime& gameTime) override {
if (gameTime.IsRunningSlowly) {
// Skip expensive optional work (particle updates, LOD generation, etc.)
}
// gameTime.ElapsedGameTime is always exactly TargetElapsedTime here
float dt = static_cast<float>(gameTime.ElapsedGameTime.TotalSeconds()); // always 1/60
}
Variable timestep with ElapsedGameTime
// With IsFixedTimeStep = false:
void Update(GameTime& gameTime) override {
float dt = static_cast<float>(gameTime.ElapsedGameTime.TotalSeconds());
// dt varies each frame (e.g. 0.014 to 0.020 seconds on a 60Hz display)
player_.position += player_.velocity * dt; // frame-rate independent
}
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 simulation with fixed step, rendering with interpolation
The gold standard is to run physics at a fixed rate and render at the display rate with interpolation. This gives stable physics and tear-free rendering.
struct PhysicsBody {
Vector3 position;
Vector3 prevPosition; // position one physics step ago
Vector3 velocity;
};
class PhysicsDemo final : public Game {
public:
PhysicsDemo() : graphics_(this) {
// Fixed physics at 60 Hz; Draw is uncapped (variable)
setIsFixedTimeStep(true);
setTargetElapsedTime(TimeSpan::FromSeconds(1.0 / 60.0));
}
protected:
void Initialize() override {
Game::Initialize();
body_.position = Vector3(0, 10, 0);
body_.prevPosition = body_.position;
body_.velocity = Vector3(2, 0, 0);
}
void LoadContent() override {
effect_ = std::make_unique<BasicEffect>(getGraphicsDeviceProperty());
effect_->setVertexColorEnabled(true);
// ... build sphere mesh ...
}
// Called at exactly 60 Hz regardless of render speed
void Update(GameTime& gameTime) override {
const float dt = static_cast<float>(
gameTime.ElapsedGameTime.TotalSeconds()); // always 1/60
body_.prevPosition = body_.position;
// 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 as fast as the GPU allows — may be faster than 60 Hz
void Draw(const GameTime& gameTime) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::CornflowerBlue);
// Interpolation factor: where we are between the last two physics steps
// With fixed timestep this is always near 1.0 on the next Draw call,
// but matters when Draw outpaces Update (e.g. 144 Hz display, 60 Hz physics)
float alpha = static_cast<float>(
gameTime.ElapsedGameTime.TotalSeconds() /
getTargetElapsedTime().TotalSeconds());
alpha = MathHelper::Clamp(alpha, 0.0f, 1.0f);
Vector3 renderPos = Vector3::Lerp(
body_.prevPosition, body_.position, alpha);
Matrix world = Matrix::CreateTranslation(renderPos);
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_->setWorld(world);
effect_->setView(view);
effect_->setProjection(proj);
gd.setVertexBuffer(*vb_);
for (auto& pass : effect_->getCurrentTechnique().Passes) {
pass.Apply();
gd.DrawPrimitives(PrimitiveType::TriangleList, 0, primitiveCount_);
}
gd.Present();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<BasicEffect> effect_;
std::unique_ptr<VertexBuffer> vb_;
int primitiveCount_ = 0;
PhysicsBody body_;
};
Key takeaways
- Use
IsFixedTimeStep = truefor any simulation that integrates state over time — it removes floating-point jitter from variable frame rates. - Store
prevPositionbefore each physics step and interpolate inDrawfor smooth rendering at any frame rate. - Check
gameTime.IsRunningSlowlyto skip optional work when the CPU cannot keep up. gameTime.TotalGameTimecounts wall-clock elapsed time; use it for time-based animations that should not stutter on slow frames.