Tutorial 04: The Game Class Lifecycle

CNA Tutorial Series  ·  Beginner

Lifecycle Overview

When you call game.Run(), CNA drives a well-defined sequence of virtual method calls on your Game subclass. Understanding this sequence tells you exactly where to put each piece of your game code.

The full sequence looks like this:

game.Run()
  └─> Constructor          (already called before Run())
  └─> Initialize()          once, after graphics device created
  └─> LoadContent()         once, after Initialize()
  └─> [game loop begins]
      ├─> Update(gameTime)  every frame (or at fixed rate)
      └─> Draw(gameTime)    every frame
  └─> [exit signal received]
  └─> UnloadContent()       once
  └─> OnExiting()           once
  └─> [destructor]

The separation between initialization phases is important:

  • The constructor runs before the graphics device exists. Configure settings here, do not touch the GPU.
  • Initialize runs after the graphics device is created. Set up game services and non-content game components.
  • LoadContent runs after Initialize. Load all textures, sounds, and fonts here.
  • Update / Draw run in a tight loop until the game exits.
  • UnloadContent and OnExiting run once the loop ends.

Constructor

The constructor is the first thing that runs. At this point the SDL window does not exist yet and the graphics device has not been created. The constructor is the right place to:

  • Create the GraphicsDeviceManager (passing this)
  • Set the preferred back buffer width and height
  • Set the window title
  • Choose whether the game is full screen
  • Set IsFixedTimeStep and TargetElapsedTime
MyGame::MyGame()
    : graphics_(this)
{
    // Graphics device does NOT exist yet.
    // Configure display settings:
    graphics_.setPreferredBackBufferWidth(1280);
    graphics_.setPreferredBackBufferHeight(720);
    graphics_.setIsFullScreen(false);

    // Configure the game loop:
    setIsFixedTimeStep(true);
    setTargetElapsedTime(TimeSpan::FromSeconds(1.0 / 60.0)); // 60 FPS

    std::cout << "[Constructor] Game configured\n";
}

Do not call getGraphicsDeviceProperty() or create any Texture2D / SpriteBatch objects in the constructor. The graphics device does not exist at that point and you will crash or get undefined behaviour.

Initialize()

Initialize() is called once, after the graphics device and window have been created but before LoadContent(). It is the right place to:

  • Initialize game services
  • Set up GameComponent objects
  • Read configuration files
  • Print device capabilities to the console
void MyGame::Initialize() {
    // Always call the base first — it calls Initialize() on all
    // registered GameComponents.
    Game::Initialize();

    auto& vp = getGraphicsDeviceProperty().getViewport();
    std::cout << "[Initialize] Viewport: "
              << vp.getWidth() << "x" << vp.getHeight() << "\n";

    // Example: seed random number generator
    std::srand(static_cast<unsigned>(std::time(nullptr)));

    std::cout << "[Initialize] Done\n";
}

Always call Game::Initialize() first in your override. The base implementation initialises all registered GameComponent objects.

LoadContent()

LoadContent() is called once after Initialize(). Load all game assets here: textures, sprite fonts, sounds, and effects. This is also where you create SpriteBatch.

void MyGame::LoadContent() {
    std::cout << "[LoadContent] Loading assets\n";

    // Create SpriteBatch
    spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());

    // Load a texture (using ContentManager)
    playerTexture_ = Content.Load<Texture2D>("textures/player");

    // Load a sprite font
    font_ = Content.Load<SpriteFont>("fonts/arial");

    std::cout << "[LoadContent] Done\n";
}

If an asset fails to load, an exception is thrown here and the game exits cleanly before the loop starts.

Update(GameTime)

Update(GameTime& gameTime) is called every frame (or at a fixed rate if IsFixedTimeStep is true). This is where all game logic runs: moving objects, checking collisions, reading input, updating AI.

void MyGame::Update(GameTime& gameTime) {
    // Delta time: how many seconds since the last Update call
    float dt = static_cast<float>(gameTime.getElapsedGameTime().getTotalSeconds());

    // Move the player
    playerPosition_.X += velocity_.X * dt;
    playerPosition_.Y += velocity_.Y * dt;

    // Check if the player pressed Escape to quit
    auto kb = Keyboard::GetState();
    if (kb.IsKeyDown(Keys::Escape)) {
        Exit();  // signals the game loop to stop
    }

    std::cout << "[Update] dt=" << dt << " pos=("
              << playerPosition_.X << "," << playerPosition_.Y << ")\n";
}

Keep Update() free of rendering calls. Never call SpriteBatch::Begin() or device.Clear() inside Update().

Draw(GameTime)

Draw(const GameTime& gameTime) is called every frame after Update(). All rendering happens here. The pattern is always: clear, draw, present.

void MyGame::Draw(const GameTime& gameTime) {
    auto& device = getGraphicsDeviceProperty();

    // 1. Clear the back buffer
    device.Clear(Color::CornflowerBlue);

    // 2. Draw sprites
    spriteBatch_->Begin();
    spriteBatch_->Draw(*playerTexture_, playerPosition_, Color::White);
    spriteBatch_->End();

    // 3. Present the frame
    device.Present();
}

Note that Draw takes a const GameTime& (the const version) while Update takes a non-const GameTime&. This matches the XNA signatures.

UnloadContent()

UnloadContent() is called once when the game loop exits, before the destructor runs. It gives you an explicit hook to release GPU resources in the right order.

void MyGame::UnloadContent() {
    std::cout << "[UnloadContent] Releasing assets\n";

    // Smart pointers auto-release, but you can reset them explicitly
    // to ensure GPU resources are freed before the device shuts down.
    spriteBatch_.reset();
    playerTexture_.reset();
    font_.reset();

    std::cout << "[UnloadContent] Done\n";
}

With std::unique_ptr, resources are freed automatically when they go out of scope. UnloadContent() is mainly useful when you need to control release order, for example freeing textures before shutting down the graphics device.

OnExiting()

OnExiting() is called after UnloadContent() when the game is about to quit. Use it to save game state, write config files, or show a farewell message.

void MyGame::OnExiting() {
    std::cout << "[OnExiting] Saving game state\n";
    saveSettings();  // write your own save function
    Game::OnExiting();  // call base
}

Full Lifecycle Demo

Here is a complete game that logs every lifecycle event to standard output so you can see exactly when each method is called:

#pragma once

#include <iostream>
#include <memory>

#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GameTime.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;

class LifecycleDemo final : public Game {
public:
    LifecycleDemo() : graphics_(this) {
        std::cout << "[1] Constructor\n";
        graphics_.setPreferredBackBufferWidth(800);
        graphics_.setPreferredBackBufferHeight(600);
    }

    ~LifecycleDemo() {
        std::cout << "[8] Destructor\n";
    }

protected:
    void Initialize() override {
        std::cout << "[2] Initialize\n";
        Game::Initialize();
    }

    void LoadContent() override {
        std::cout << "[3] LoadContent\n";
        spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
    }

    void Update(GameTime& gameTime) override {
        static int frame = 0;
        if (frame == 0) std::cout << "[4] First Update\n";
        if (++frame == 180) {
            std::cout << "[4] Update - requesting exit after 180 frames\n";
            Exit();
        }
    }

    void Draw(const GameTime& gameTime) override {
        static int frame = 0;
        if (frame++ == 0) std::cout << "[5] First Draw\n";
        getGraphicsDeviceProperty().Clear(Color::CornflowerBlue);
        getGraphicsDeviceProperty().Present();
    }

    void UnloadContent() override {
        std::cout << "[6] UnloadContent\n";
        spriteBatch_.reset();
    }

    void OnExiting() override {
        std::cout << "[7] OnExiting\n";
        Game::OnExiting();
    }

private:
    GraphicsDeviceManager graphics_;
    std::unique_ptr<SpriteBatch> spriteBatch_;
};

int main() {
    LifecycleDemo game;
    game.Run();
    return 0;
}

Running this program prints:

[1] Constructor
[2] Initialize
[3] LoadContent
[4] First Update
[5] First Draw
... (Update and Draw called ~60 times per second) ...
[4] Update - requesting exit after 180 frames
[6] UnloadContent
[7] OnExiting
[8] Destructor

Now that you understand the lifecycle, Tutorial 05 digs into the game loop itself — how fixed and variable timestep work, and how to use delta time for frame-rate-independent movement.