Game Loop & Lifecycle

Microsoft::Xna::Framework::Game — subclassing, lifecycle methods, GameTime, GraphicsDeviceManager, GameWindow, GameComponent

Implementation status: The Game class and its full lifecycle are fully implemented. GameTime, GraphicsDeviceManager, GameWindow, GameComponent, and DrawableGameComponent are all present and functional.

Overview

Game lives in the Microsoft::Xna::Framework namespace. It is the central class of every CNA application — it owns the window, the graphics device, the content manager, and the main loop. You write a game by subclassing Game and overriding the lifecycle virtual methods. The entry point creates your subclass and calls Run(), which hands control to the framework until the window closes.

The framework guarantees the order of lifecycle calls, pumps SDL3 events, manages fixed- or variable-timestep scheduling, and forwards each frame to your overrides. You do not write your own event loop.

// entry point
int main() {
    MyGame game;
    game.Run();
    return 0;
}

Lifecycle diagram

The following sequence shows the order in which the framework calls your overrides. Methods marked once are called a single time; methods marked every frame are called repeatedly until the game exits.

  1. Constructor — create GraphicsDeviceManager, configure window title and back-buffer size (once)
  2. Initialize() — non-content game setup; call Game::Initialize() at the top (once)
  3. LoadContent() — load textures, sounds, fonts; create GPU resources (once)
  4. Update(GameTime) — game logic, input, physics (every frame)
  5. Draw(GameTime) — rendering (every frame)
  6. loop back to Update until Exit() is called
  7. UnloadContent() — release managed resources (once, on exit)
  8. Dispose(bool) — RAII cleanup of unmanaged resources (once)

Constructor

The constructor is where you create the GraphicsDeviceManager and perform any pre-initialization configuration. The graphics device is not yet created at this point, so no GPU calls are valid here. Set window dimensions and title through the manager and the Window property respectively.

class MyGame : public Game {
    GraphicsDeviceManager graphics;
public:
    MyGame()
        : graphics(*this)
    {
        graphics.PreferredBackBufferWidth  = 1280;
        graphics.PreferredBackBufferHeight = 720;
        graphics.SynchronizeWithVerticalRetrace = true;
        getWindow().Title = "My CNA Game";
    }
};

Initialize()

Initialize() is called once after the graphics device has been created but before any content is loaded. Use it for non-content setup: registering GameComponent objects, configuring services, and initializing game state that does not depend on loaded assets. Always call Game::Initialize() — it initialises all registered components.

void Initialize() override {
    // register components before calling base
    Components.Add(std::make_shared<MyComponent>(*this));

    Game::Initialize();   // calls Initialize() on all components

    // post-init game state
    playerPosition = Vector2(100.0f, 200.0f);
}

LoadContent()

LoadContent() is called once, immediately after Initialize(). The ContentManager is fully operational here and the graphics device is ready to accept resource uploads. Load all textures, sounds, sprite fonts, and effects in this override. Create GPU resources such as vertex buffers, render targets, and effect instances here as well.

void LoadContent() override {
    spriteBatch = std::make_shared<SpriteBatch>(*GraphicsDevice);
    playerTex   = Content.Load<Texture2D>("textures/player");
    music       = Content.Load<Song>("audio/theme");
    font        = Content.Load<SpriteFont>("fonts/default");
}

Update(GameTime)

Update() is called once per logical tick. All game logic belongs here: reading input, advancing physics, running AI, updating animations, and triggering audio. The GameTime argument carries timing information that lets you write frame-rate-independent code. Call Exit() from inside Update() to request a clean shutdown.

void Update(const GameTime& gameTime) override {
    auto kb = Keyboard::GetState();
    if (kb.IsKeyDown(Keys::Escape))
        Exit();

    float dt = gameTime.ElapsedGameTime.TotalSeconds();
    playerPosition.X += speed * dt;

    FrameworkDispatcher::Update();  // pump SDL3_mixer audio events
    Game::Update(gameTime);         // forwards to registered components
}

Draw(GameTime)

Draw() is called once per visual frame, immediately after Update(). With fixed timestep enabled, Draw may be skipped if the game is running behind, but it is never called more than once between two Update calls. Begin every Draw() with GraphicsDevice->Clear() to erase the previous frame. Finish with the swap that the framework handles automatically — you do not call a present or swap-buffers function yourself.

void Draw(const GameTime& gameTime) override {
    GraphicsDevice->Clear(Color::CornflowerBlue);

    spriteBatch->Begin();
    spriteBatch->Draw(playerTex, playerPosition, Color::White);
    spriteBatch->DrawString(font, "Hello CNA", Vector2(8, 8), Color::White);
    spriteBatch->End();

    Game::Draw(gameTime);   // forwards to DrawableGameComponents
}

UnloadContent()

UnloadContent() is called once when the game is shutting down, after the main loop has exited. The default implementation calls Content.Unload(), which releases every asset loaded through the ContentManager. Override it only if you have manually created GPU resources that the content manager does not own.

void UnloadContent() override {
    // release manually created resources
    customRenderTarget.reset();
    Game::UnloadContent();   // calls Content.Unload()
}

Dispose(bool)

Dispose(bool disposing) provides a deterministic RAII-style cleanup hook. When disposing is true the call originates from an explicit teardown; when false it originates from a finaliser. In C++ the distinction is less significant than in C#, but the method is present for full XNA compatibility. Release any resources not covered by UnloadContent() here, then call the base.

void Dispose(bool disposing) override {
    if (disposing) {
        // deterministic cleanup
        audioEngine.reset();
    }
    Game::Dispose(disposing);
}

GameTime

GameTime is passed to every Update() and Draw() call. It carries three pieces of timing information.

MemberTypeDescription
ElapsedGameTime TimeSpan Time since the last Update() call. Call .TotalSeconds() to get a double suitable for delta-time movement calculations.
TotalGameTime TimeSpan Accumulated time since the game started running. Useful for time-based animations and shaders.
IsRunningSlowly bool Set to true by the framework when fixed-timestep logic is behind schedule and has had to run multiple Update() calls in a single frame to catch up. Useful for disabling expensive non-essential work during a slowdown.

TimeSpan helper methods:

  • .TotalSeconds() — elapsed time as a double in seconds (most common for delta-time)
  • .TotalMilliseconds() — elapsed time in milliseconds
  • .TotalMinutes() — elapsed time in minutes

GraphicsDeviceManager

GraphicsDeviceManager is created in the Game constructor and configures the graphics device and swap chain. After construction you can adjust its properties before Run() is called. Call ApplyChanges() at runtime if you need to change resolution or toggle full-screen while the game is running.

PropertyTypeDescription
PreferredBackBufferWidth int Desired back-buffer width in pixels
PreferredBackBufferHeight int Desired back-buffer height in pixels
IsFullScreen bool Toggles full-screen exclusive mode; call ApplyChanges() after setting
PreferMultiSampling bool Enables multisample anti-aliasing (MSAA) when true
MultiSampleCount int Number of MSAA samples (2, 4, 8); ignored when PreferMultiSampling is false
SynchronizeWithVerticalRetrace bool Enables VSync; set to false for uncapped frame rate
GraphicsDevice GraphicsDevice& The underlying graphics device; valid after Initialize()
// Toggle full-screen at runtime
graphics.IsFullScreen = !graphics.IsFullScreen;
graphics.ApplyChanges();

GameWindow

GameWindow is accessed through game.getWindow(). It represents the OS window and exposes properties for the title, client area, and resize behaviour. The window is created when Run() is called; reading or writing its properties before that point has no effect.

MemberTypeDescription
Title std::string The title bar string shown by the OS
ClientBounds Rectangle The current drawable area of the window (X, Y always 0; Width and Height reflect actual size)
AllowUserResizing bool When true, the user can drag the window border to resize it
ClientSizeChanged event Fired after the client area changes size; use it to rebuild render targets or recompute the projection matrix
// In Initialize() or constructor
getWindow().AllowUserResizing = true;
getWindow().ClientSizeChanged += [this](auto& sender, auto& args) {
    auto bounds = getWindow().ClientBounds;
    RebuildProjection(bounds.Width, bounds.Height);
};

GameComponent and DrawableGameComponent

CNA supports the XNA component model. Reusable subsystems can be packaged as GameComponent or DrawableGameComponent instances and added to the Game::Components collection. The framework calls their lifecycle methods automatically in the correct order, in the order they were added (respecting UpdateOrder and DrawOrder sort keys).

GameComponent

The base component class. Override Initialize() and Update(GameTime). Useful for subsystems that do no rendering of their own — input managers, audio controllers, network handlers.

Virtual method / propertyDescription
Initialize()Called by Game::Initialize() once the graphics device is ready
Update(GameTime)Called each tick by Game::Update()
EnabledSet to false to pause Update() calls for this component
UpdateOrderInteger sort key; lower values are updated first

DrawableGameComponent

Extends GameComponent with a Draw(GameTime) override and visibility control. Add it to Components the same way; the framework calls Draw() automatically during Game::Draw().

Additional virtual method / propertyDescription
LoadContent()Called once, after Initialize(), to load component-specific assets
Draw(GameTime)Called each visual frame by Game::Draw()
VisibleSet to false to suppress Draw() calls without removing the component
DrawOrderInteger sort key; lower values are drawn first (back-to-front)

Fixed vs variable timestep

CNA replicates the XNA timestep modes exactly. The default is fixed timestep.

PropertyDefaultEffect
IsFixedTimeStep true When true, Update() is called at the rate defined by TargetElapsedTime. The framework accumulates wall-clock time and calls Update() multiple times per render frame if the GPU fell behind, setting IsRunningSlowly = true in that case.
TargetElapsedTime 1/60 s TimeSpan that sets the desired tick interval under fixed timestep. Override to target 30 Hz, 120 Hz, or any other rate.
IsFixedTimeStep = false Variable timestep: Update() is called exactly once per render frame; ElapsedGameTime reflects the true wall-clock delta and varies frame to frame. Divide all movement and physics by ElapsedGameTime.TotalSeconds().

Under variable timestep, never hard-code per-frame distances or velocities. Always multiply by ElapsedGameTime.TotalSeconds() to keep motion consistent regardless of frame rate.

FrameworkDispatcher::Update()

FrameworkDispatcher::Update() must be called once per frame from your Update() override. It pumps the SDL3_mixer audio event queue, ensuring audio callbacks are processed on the main thread. Without this call, audio may stutter or fail to start on some platforms.

void Update(const GameTime& gameTime) override {
    FrameworkDispatcher::Update();
    // ... rest of Update
}

Code examples

1. Minimal Game subclass

#include <Microsoft/Xna/Framework/Game.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 MyGame : public Game {
    GraphicsDeviceManager graphics;
    std::shared_ptr<SpriteBatch> spriteBatch;
    std::shared_ptr<Texture2D>   playerTex;

public:
    MyGame() : graphics(*this) {
        graphics.PreferredBackBufferWidth  = 1280;
        graphics.PreferredBackBufferHeight = 720;
        graphics.SynchronizeWithVerticalRetrace = true;
        getWindow().Title = "My CNA Game";
    }

protected:
    void Initialize() override {
        Game::Initialize();
    }

    void LoadContent() override {
        spriteBatch = std::make_shared<SpriteBatch>(*GraphicsDevice);
        playerTex   = Content.Load<Texture2D>("textures/player");
    }

    void Update(const GameTime& gameTime) override {
        FrameworkDispatcher::Update();
        if (Keyboard::GetState().IsKeyDown(Keys::Escape))
            Exit();
        Game::Update(gameTime);
    }

    void Draw(const GameTime& gameTime) override {
        GraphicsDevice->Clear(Color::CornflowerBlue);
        spriteBatch->Begin();
        spriteBatch->Draw(playerTex, Vector2(100, 200), Color::White);
        spriteBatch->End();
        Game::Draw(gameTime);
    }
};

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

2. Frame-rate-independent movement with ElapsedGameTime

// Member variables
Vector2 position { 0.0f, 300.0f };
const float speed = 200.0f;  // pixels per second

void Update(const GameTime& gameTime) override {
    FrameworkDispatcher::Update();

    // TotalSeconds() converts the TimeSpan to a double
    float dt = static_cast<float>(gameTime.ElapsedGameTime.TotalSeconds());

    auto kb = Keyboard::GetState();
    if (kb.IsKeyDown(Keys::Right)) position.X += speed * dt;
    if (kb.IsKeyDown(Keys::Left))  position.X -= speed * dt;
    if (kb.IsKeyDown(Keys::Down))  position.Y += speed * dt;
    if (kb.IsKeyDown(Keys::Up))    position.Y -= speed * dt;

    Game::Update(gameTime);
}

3. Adding a DrawableGameComponent

// HudComponent.hpp
class HudComponent : public DrawableGameComponent {
    std::shared_ptr<SpriteFont>  font;
    std::shared_ptr<SpriteBatch> batch;
    int& score;
public:
    HudComponent(Game& game, int& score)
        : DrawableGameComponent(game), score(score) {}

    void LoadContent() override {
        font  = Game.Content.Load<SpriteFont>("fonts/hud");
        batch = std::make_shared<SpriteBatch>(*GraphicsDevice);
    }

    void Draw(const GameTime&) override {
        batch->Begin();
        batch->DrawString(font,
            "Score: " + std::to_string(score),
            Vector2(8, 8), Color::White);
        batch->End();
    }
};

// In Game constructor or Initialize():
Components.Add(std::make_shared<HudComponent>(*this, score));

4. Variable timestep setup

MyGame() : graphics(*this) {
    graphics.PreferredBackBufferWidth  = 1920;
    graphics.PreferredBackBufferHeight = 1080;

    // Disable fixed timestep — Update() fires once per render frame
    IsFixedTimeStep = false;

    // Uncap the frame rate by disabling VSync too (optional)
    graphics.SynchronizeWithVerticalRetrace = false;
}