Tutorial 05: The Game Loop: Update and Draw

CNA Tutorial Series  ·  Beginner

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:

while (!exitRequested) {
    auto now = clock.now();
    gameTime.elapsedGameTime = now - lastTime;
    gameTime.totalGameTime   += gameTime.elapsedGameTime;
    lastTime = now;

    handleEvents();
    Update(gameTime);
    Draw(gameTime);
}

GameTime Explained

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

PropertyTypeMeaning
getElapsedGameTime()TimeSpanTime since the last Update/Draw call. Use this for movement.
getTotalGameTime()TimeSpanTotal time the game has been running. Useful for animations keyed to absolute time.
getIsRunningSlowly()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.getElapsedGameTime();

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

    double sinceStart    = gameTime.getTotalGameTime().getTotalSeconds();
    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. 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 always exactly TargetElapsedTime inside Update().

  • 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.

IsFixedTimeStep and TargetElapsedTime

Set these in the constructor:

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

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

To target 30 FPS instead:

setIsFixedTimeStep(true);
setTargetElapsedTime(TimeSpan::FromSeconds(1.0 / 30.0));

To disable fixed timestep (uncapped FPS):

setIsFixedTimeStep(false);

When IsRunningSlowly is true the loop is catching up. Drop expensive optional work:

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

    if (!gameTime.getIsRunningSlowly()) {
        // 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.getElapsedGameTime().getTotalSeconds());
    playerX_ += 300.0f * dt;  // 300 pixels/sec
}

A complete example with a moving square:

class MovingSquare final : public Game {
public:
    MovingSquare() : graphics_(this) {
        setIsFixedTimeStep(true);
        setTargetElapsedTime(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.getElapsedGameTime().getTotalSeconds());

        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();

        device.Present();
    }

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.getElapsedGameTime().getTotalSeconds());

    // 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 always be exactly TargetElapsedTime (e.g., 1/60 ≈ 0.01667 seconds) so physics stays deterministic. 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.getElapsedGameTime().getTotalSeconds());
    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.