Tutorial 05: The Game Loop: Update and Draw

CNA Tutorial Series  ·  Beginner

ℹ

What you’ll learn

  • What GameTime carries and how to read elapsed time from it.
  • The difference between fixed and variable timestep, and IsFixedTimeStep / TargetElapsedTime.
  • Scaling movement by delta time so behaviour is frame-rate independent.

Before you start — Tutorial 04: The Game Class Lifecycle — the loop is what drives the lifecycle callbacks.

What Is a Game Loop?

A game loop is the engine that drives a game. Every frame it does the same three things:

  1. Read input — what did the player do since the last frame?
  2. Update — move objects, apply physics, check collisions, run AI.
  3. Draw — render everything to the screen.

This repeats at 30, 60, or as many frames per second as the hardware allows. CNA's Game class provides this loop automatically. When you call game.Run(), CNA enters the loop and calls your Update() and Draw() overrides until Exit() is called.

The simplest possible loop (variable timestep, uncapped FPS) looks like this internally — a simplified sketch, not CNA’s literal source:

while (!exitRequested) {
    handleEvents();

    auto now = clock.now();
    gameTime.elapsedGameTime = now - lastTime;   // first Update: time since the loop started (0 with a fixed step)
    lastTime = now;

    Update(gameTime);
    gameTime.totalGameTime += gameTime.elapsedGameTime;           // advances AFTER Update

    Draw(gameTime);
    Present();                                                    // done by Game::EndDraw()
}

Two details of this order matter for your code: the very first Update() of a fixed-step game (the default) sees an elapsed time of zero, while a variable-step game sees the time measured since the loop started, and TotalGameTime during an Update() is the time before that step, not including it. This is XNA’s own clock behaviour, which CNA reproduces.

GameTime Explained

GameTime is passed to both Update() and Draw(). It carries two time measurements:

PropertyTypeMeaning
getElapsedGameTimeProperty()TimeSpanTime since the previous Update (zero on the first one). Use this for movement.
getTotalGameTimeProperty()TimeSpanTotal time the game has been running, as of the start of the current step. Useful for animations keyed to absolute time.
getIsRunningSlowlyProperty()boolTrue if the fixed-timestep loop is running behind. Drop non-essential work.

TimeSpan has several conversion getters:

void MyGame::Update(GameTime& gameTime) {
    auto elapsed = gameTime.getElapsedGameTimeProperty();

    double totalSeconds  = elapsed.getTotalSecondsProperty();   // e.g. 0.01667 at 60fps
    double totalMs       = elapsed.getTotalMillisecondsProperty();
    float  dt            = static_cast<float>(totalSeconds); // delta time

    double sinceStart    = gameTime.getTotalGameTimeProperty().getTotalSecondsProperty();
    std::cout << "t=" << sinceStart << " dt=" << dt << "\n";
}

Fixed vs Variable Timestep

CNA supports two update modes, controlled by IsFixedTimeStep:

Variable timestep (IsFixedTimeStep = false)

Update and Draw are called as fast as the hardware allows (exactly one Update per Draw). ElapsedGameTime varies every frame. Useful for uncapped-framerate games and benchmarks.

  • Pros: maximum FPS, lowest latency.
  • Cons: ElapsedGameTime can vary wildly on a slow machine, making physics unstable if not handled carefully.

Fixed timestep (IsFixedTimeStep = true, the default)

CNA calls Update() at a fixed rate (default 60 Hz). If a frame takes too long, CNA calls Update() extra times to catch up before calling Draw(). ElapsedGameTime is exactly TargetElapsedTime inside every Update() except the very first, which sees zero.

  • Pros: deterministic physics and game logic, easier to reason about.
  • Cons: slightly more complex loop; on a slow machine the game logic can fall behind.
ℹ

Browser builds. When CNA runs under Emscripten the frame body is driven by requestAnimationFrame and keeps its own fixed-step accumulator (with a 250 ms clamp) that steps by TargetElapsedTime. Setting IsFixedTimeStep = false is not honoured there, and the first Update gets a full step instead of zero. Write game logic so it does not depend on either detail.

IsFixedTimeStep and TargetElapsedTime

Set these in the constructor:

MyGame::MyGame() : graphics_(this) {
    // Enable fixed timestep (this is already the default)
    setIsFixedTimeStepProperty(true);

    // Target 60 updates per second (default is already 1/60 sec)
    setTargetElapsedTimeProperty(TimeSpan::FromSeconds(1.0 / 60.0));
}

To target 30 FPS instead:

setIsFixedTimeStepProperty(true);
setTargetElapsedTimeProperty(TimeSpan::FromSeconds(1.0 / 30.0));

To disable fixed timestep (uncapped FPS):

setIsFixedTimeStepProperty(false);

When IsRunningSlowly is true the loop is catching up (it turns on once the loop has had to run extra catch-up Update calls several times, and off again when it has caught up). Drop expensive optional work:

void MyGame::Update(GameTime& gameTime) {
    updatePhysics(gameTime);
    updateAI(gameTime);

    if (!gameTime.getIsRunningSlowlyProperty()) {
        // Only update particle effects if we have headroom
        updateParticles(gameTime);
    }
}

Delta Time in Practice

Delta time (dt) is the elapsed time in seconds since the last update. Multiplying velocities by delta time makes movement frame-rate-independent.

Without delta time:

// BAD: moves 5 pixels per frame regardless of FPS
// At 30fps: 150 pixels/sec. At 60fps: 300 pixels/sec. Inconsistent!
void MyGame::Update(GameTime& gameTime) {
    playerX_ += 5.0f;  // DO NOT do this
}

With delta time:

// GOOD: moves 300 pixels per second regardless of FPS
void MyGame::Update(GameTime& gameTime) {
    float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
    playerX_ += 300.0f * dt;  // 300 pixels/sec
}

A complete example with a moving square:

class MovingSquare final : public Game {
public:
    MovingSquare() : graphics_(this) {
        graphics_.setPreferredBackBufferWidthProperty(800);
        graphics_.setPreferredBackBufferHeightProperty(600);
        setIsFixedTimeStepProperty(true);
        setTargetElapsedTimeProperty(TimeSpan::FromSeconds(1.0 / 60.0));
    }

protected:
    void LoadContent() override {
        spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());

        // 1x1 white texture — we will use this in Tutorial 06
        pixel_ = std::make_unique<Texture2D>(getGraphicsDeviceProperty(), 1, 1);
        Color white = Color::White;
        pixel_->SetData(&white, 1);
    }

    void Update(GameTime& gameTime) override {
        float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());

        x_ += speedX_ * dt;
        y_ += speedY_ * dt;

        // Bounce off edges
        if (x_ < 0.0f || x_ + 50.0f > 800.0f) speedX_ = -speedX_;
        if (y_ < 0.0f || y_ + 50.0f > 600.0f) speedY_ = -speedY_;

        x_ = std::clamp(x_, 0.0f, 750.0f);
        y_ = std::clamp(y_, 0.0f, 550.0f);

        auto kb = Keyboard::GetState();
        if (kb.IsKeyDown(Keys::Escape)) Exit();
    }

    void Draw(const GameTime& gameTime) override {
        auto& device = getGraphicsDeviceProperty();
        device.Clear(Color::Black);

        spriteBatch_->Begin();
        spriteBatch_->Draw(*pixel_,
            Rectangle(static_cast<int>(x_), static_cast<int>(y_), 50, 50),
            Color::Yellow);
        spriteBatch_->End();
        // No device.Present(): Game::EndDraw() presents after Draw() returns.
    }

private:
    GraphicsDeviceManager graphics_;
    std::unique_ptr<SpriteBatch> spriteBatch_;
    std::unique_ptr<Texture2D> pixel_;

    float x_ = 100.0f, y_ = 100.0f;
    float speedX_ = 250.0f, speedY_ = 180.0f;  // pixels per second
};

Frame Rate Independence

The key insight: always think in "units per second", never "units per frame".

Variable typeUnitExample
Speedpixels/secondfloat speed = 200.0f;
Rotation rateradians/secondfloat rotSpeed = MathHelper::Pi;
Gravitypixels/second²float gravity = 980.0f;
Accelerationpixels/second²float accel = 500.0f;

Then in Update():

void MyGame::Update(GameTime& gameTime) {
    float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());

    // Velocity (pixels/sec)
    velocity_.Y += gravity_ * dt;  // accumulate gravity

    // Position (pixels)
    position_.X += velocity_.X * dt;
    position_.Y += velocity_.Y * dt;

    // Rotation (radians)
    rotation_ += rotationSpeed_ * dt;
}

With fixed timestep, dt will be exactly TargetElapsedTime (e.g., 1/60 ≈ 0.01667 seconds) on every update after the first, so physics stays deterministic. (The first update has dt = 0, which is harmless when you multiply by dt but a division-by-zero trap if you ever divide by it.) With variable timestep, dt varies and you must cap it to avoid "spiral of death" on very slow frames:

void MyGame::Update(GameTime& gameTime) {
    float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
    dt = std::min(dt, 0.05f);  // cap at 50ms to prevent physics tunnelling
    // ...
}

You now understand the game loop. In Tutorial 06 you will start rendering — beginning with the simplest possible shape: a colored rectangle.