Tutorial 04: The Game Class Lifecycle
What you’ll learn
- The order CNA calls
Initialize,LoadContent,Update,Draw,OnExitingandUnloadContent— and whyLoadContentruns insideGame::Initialize(). - Which work belongs in the constructor versus
Initialize(). - Why content loading has its own callback rather than living in the constructor.
Before you start — Tutorial 03: Your First CNA Window — this walks through the callbacks of the window you just got running.
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(); the base Game constructor
has already created the window and a default device)
└─> [GraphicsDeviceManager preferences applied to the device]
└─> Initialize() once
└─> Game::Initialize() initialises the components, then calls…
└─> LoadContent() once
└─> [game loop begins]
├─> Update(gameTime) every frame (or at a fixed rate)
└─> Draw(gameTime) every frame, then CNA presents the frame
└─> [Exit() called or window closed]
└─> OnExiting() once, right after the loop ends
└─> [Run() returns]
game.Dispose()
└─> UnloadContent() once (device is being disposed)
└─> [destructor]
The separation between initialization phases is important:
- The constructor runs after the base
Gameconstructor has already created a graphics device with default settings; yourGraphicsDeviceManagerpreferences are applied just beforeInitialize(). 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 is called by the base
Game::Initialize()(exactly as in XNA), so it runs at the point where you call the base method. Load all textures, sounds, and fonts here. - Update / Draw run in a tight loop until the game exits.
- OnExiting runs once the loop ends. UnloadContent runs when the game is disposed, which happens when you call
game.Dispose()afterRun()returns.
The first Update sees dt = 0. In this snapshot CNA uses XNA’s own game clock: with the default fixed time step the first call to Update() reports an ElapsedGameTime of zero (with IsFixedTimeStep = false it receives the time measured since the loop started), and TotalGameTime advances only after Update() returns. Code that divides by dt must guard against zero on frame one. (On the browser build the frame loop keeps its own fixed-step clock, so the first Update there gets a full step.)
Constructor
The constructor is the first thing that runs. At this point CNA's base Game constructor has already created the graphics device (and, for a windowed renderer, the window), but only with default settings: your GraphicsDeviceManager preferences are applied just before Initialize(). The constructor is the right place to:
- Create the
GraphicsDeviceManager(passingthis) - Set the preferred back buffer width and height
- Set the window title
- Choose whether the game is full screen
- Set
IsFixedTimeStepandTargetElapsedTime - Set the content root directory (
getContentProperty().setRootDirectoryProperty(...)) — it must be in place beforeLoadContent()runs
MyGame::MyGame()
: graphics_(this)
{
// The device exists, but these preferences are applied only before Initialize().
// Configure display settings:
graphics_.setPreferredBackBufferWidthProperty(1280);
graphics_.setPreferredBackBufferHeightProperty(720);
graphics_.setIsFullScreenProperty(false);
// Configure the game loop:
setIsFixedTimeStepProperty(true);
setTargetElapsedTimeProperty(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. In CNA the device already exists at that point, but only with default settings that your preferences replace just before Initialize(), and XNA creates its device only then, so resources created here are not portable.
Initialize()
Initialize() is called once, after the graphics device and window have been created. It is the right place to:
- Initialize game services
- Set up
GameComponentobjects - Read configuration files
- Print device capabilities to the console
void MyGame::Initialize() {
// Anything LoadContent() depends on goes BEFORE the base call,
// because Game::Initialize() is what calls LoadContent().
std::srand(static_cast<unsigned>(std::time(nullptr)));
// The base call initialises all registered GameComponents,
// then runs LoadContent().
Game::Initialize();
// LoadContent() has already finished here.
auto& vp = getGraphicsDeviceProperty().getViewportProperty();
std::cout << "[Initialize] Viewport: "
<< vp.getWidthProperty() << "x" << vp.getHeightProperty() << "\n";
std::cout << "[Initialize] Done\n";
}
Always call Game::Initialize() in your override — if you forget it, your components are never initialised and LoadContent() is never called. Code you place before the call runs before LoadContent(); code you place after it runs after LoadContent() has finished.
LoadContent()
LoadContent() is called once, from inside Game::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). Load<T> returns the asset BY VALUE.
playerTexture_ = getContentProperty().Load<Texture2D>("textures/player");
// Load a sprite font. SpriteFont has no default constructor, so the
// member is a std::optional<SpriteFont>.
font_ = getContentProperty().Load<SpriteFont>("fonts/arial");
std::cout << "[LoadContent] Done\n";
}
The matching members are Texture2D playerTexture_; and std::optional<SpriteFont> font_;. getContentProperty().Load<T>("name") looks for name.xnb, then name.cnb, then a loose file such as name.png, under the content root (a Content folder, looked for first under the game’s title folder, TitleLocation.Path, and otherwise relative to the process’s working directory, unless you change it with setRootDirectoryProperty). If an asset cannot be loaded, a ContentLoadException is thrown (asking for the wrong type from a compiled asset, such as Load<Texture2D> on an .xnb that holds a SpriteFont, is also a ContentLoadException, naming the asset and the type it produced); nothing catches it for you, so it propagates out of Run() (CNA logs it first). Wrap game.Run() in try/catch in main() if you want a friendly error message.
Update(GameTime)
Update(GameTime& gameTime) is called every frame (or at a fixed rate if IsFixedTimeStep is true; on a slow frame CNA may call it several times before the next Draw). 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 previous Update call.
// It is 0 on the very first call.
float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
// 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: clear, draw — and CNA presents the finished frame for you when Draw returns.
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. Nothing else to do: Game::EndDraw() presents the frame
// after Draw() returns. Do not call device.Present() yourself.
}
Note that Draw takes a const GameTime& (the const version) while Update takes a non-const GameTime&. This matches the XNA signatures.
Do not call Present() in Draw(). After your Draw returns, the Game calls EndDraw(), which presents the back buffer. An extra device.Present() inside Draw presents every frame twice. Call it yourself only if you drive frames manually, without Game::Run().
OnExiting()
OnExiting() is called once, right after the game loop ends — when you call Exit() or the player closes the window — and before UnloadContent(). Use it to save game state, write config files, or show a farewell message. (It raises the Exiting event; call the base to keep that working.)
void MyGame::OnExiting(System::Object* sender, const System::EventArgs& args) {
std::cout << "[OnExiting] Saving game state\n";
saveSettings(); // write your own save function
Game::OnExiting(sender, args); // call base
}
UnloadContent()
UnloadContent() is the hook for releasing your own GPU resources in the right order. CNA calls it once, when the game’s graphics device is disposed — and that happens inside game.Dispose(), the C++ counterpart of the using block in XNA. The base implementation is empty, and the ContentManager is already disposed by the time your override runs, so use it for resources you created.
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();
font_.reset();
std::cout << "[UnloadContent] Done\n";
}
int main() {
MyGame game;
game.Run();
game.Dispose(); // raises UnloadContent(); see the note below
return 0;
}
With std::unique_ptr and value members, 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.
The destructor alone does not call UnloadContent(): ~Game() only performs the finaliser-style Dispose(false), which skips the device-disposal step. If your game relies on UnloadContent() running (for example to flush a file), call game.Dispose() after Run() returns, as the demo below does. The same is true in XNA, where UnloadContent() runs from Dispose(), not from the finaliser.
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:
#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/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_.setPreferredBackBufferWidthProperty(800);
graphics_.setPreferredBackBufferHeightProperty(600);
}
~LifecycleDemo() {
std::cout << "[10] Destructor\n";
}
protected:
void Initialize() override {
std::cout << "[2] Initialize - before the base call\n";
Game::Initialize(); // initialises components, then calls LoadContent()
std::cout << "[4] Initialize - after the base call\n";
}
void LoadContent() override {
std::cout << "[3] LoadContent\n";
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
}
void Update(GameTime& gameTime) override {
if (updates_ == 0) {
float dt = static_cast<float>(
gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
std::cout << "[5] First Update, dt = " << dt << " s\n";
}
if (++updates_ == 180) {
std::cout << "[7] Update - requesting exit after 180 frames\n";
Exit();
}
}
void Draw(const GameTime& gameTime) override {
if (draws_++ == 0) std::cout << "[6] First Draw\n";
getGraphicsDeviceProperty().Clear(Color::CornflowerBlue);
// No Present() here: Game::EndDraw() presents after Draw returns.
}
void OnExiting(System::Object* sender, const System::EventArgs& args) override {
std::cout << "[8] OnExiting\n";
Game::OnExiting(sender, args);
}
void UnloadContent() override {
std::cout << "[9] UnloadContent\n";
spriteBatch_.reset();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
int updates_ = 0;
int draws_ = 0;
};
int main() {
LifecycleDemo game;
game.Run(); // returns after Exit()
game.Dispose(); // this is what triggers UnloadContent()
return 0;
}
Running this program prints:
[1] Constructor
[2] Initialize - before the base call
[3] LoadContent
[4] Initialize - after the base call
[5] First Update, dt = 0 s
[6] First Draw
... (Update and Draw called ~60 times per second) ...
[7] Update - requesting exit after 180 frames
[8] OnExiting
[9] UnloadContent
[10] Destructor
Note the two things this output shows that surprise people coming from other engines: LoadContent appears between the two halves of Initialize, and the first Update reports a zero elapsed time.
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.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Exit, Exiting, Dispose and destruction — What each way of ending a CNA game runs: Exit versus Exiting, explicit Dispose versus destruction, the XNA 4.0 disposal order, repeated, re-entrant and throwing disposal, and component lifetimes at shutdown.
- The Game class: contract, run modes and extension points — Exact semantics of CNA's Game base class: event types, property guards, override points, Run versus RunOneFrame versus Tick, the browser loop, reserved debug keys and the exception boundary.