Tutorial 48: Fixed vs Variable Timestep

CNA — C++ XNA 4.0 reimplementation

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

ModeWhen to use
Fixed timestepPhysics simulations, deterministic replays, network synchronisation, anything sensitive to integration stability
Variable timestepPure 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 = true for any simulation that integrates state over time — it removes floating-point jitter from variable frame rates.
  • Store prevPosition before each physics step and interpolate in Draw for smooth rendering at any frame rate.
  • Check gameTime.IsRunningSlowly to skip optional work when the CPU cannot keep up.
  • gameTime.TotalGameTime counts wall-clock elapsed time; use it for time-based animations that should not stutter on slow frames.