Tutorial 05: The Game Loop: Update and Draw
What Is a Game Loop?
A game loop is the engine that drives a game. Every frame it does the same three things:
- Read input — what did the player do since the last frame?
- Update — move objects, apply physics, check collisions, run AI.
- 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:
| Property | Type | Meaning |
|---|---|---|
getElapsedGameTime() | TimeSpan | Time since the last Update/Draw call. Use this for movement. |
getTotalGameTime() | TimeSpan | Total time the game has been running. Useful for animations keyed to absolute time. |
getIsRunningSlowly() | bool | True 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:
ElapsedGameTimecan 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 type | Unit | Example |
|---|---|---|
| Speed | pixels/second | float speed = 200.0f; |
| Rotation rate | radians/second | float rotSpeed = MathHelper::Pi; |
| Gravity | pixels/second² | float gravity = 980.0f; |
| Acceleration | pixels/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.