Game Loop & Lifecycle

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

ⓘ

Implementation status: Game is a real platform loop (SDL3, or the windowless headless and terminal platforms) supporting both fixed and variable timestep, with a dedicated Emscripten frame body for the browser target. GameTime, GraphicsDeviceManager, GameWindow, GameComponent and DrawableGameComponent are all present and functional. The core Framework namespace this page documents is one of CNA's strongest areas. Since this snapshot the game clock is XNA's own: with a fixed time step (the default) the first Update runs with an elapsed time of zero.

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 the selected platform's events, manages fixed- or variable-timestep scheduling, and forwards each frame to your overrides. You do not write your own event loop. Property accessors in CNA are getXProperty() / setXProperty(value), so Game's window, content manager and graphics device are reached with getWindowProperty(), getContentProperty() and getGraphicsDeviceProperty(); the lifecycle methods (Initialize, LoadContent, Update, Draw, UnloadContent) are protected virtual.

// 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. ↓
  3. Initialize() — non-content game setup; register components and anything LoadContent() depends on before calling Game::Initialize(), which initialises the components and then runs LoadContent() (once)
  4. ↓
  5. LoadContent() — load textures, sounds, fonts; create GPU resources (once)
  6. ↓
  7. Update(GameTime) — game logic, input, physics (every frame)
  8. ↓
  9. Draw(GameTime) — rendering (every frame)
  10. ↓
  11. loop back to Update until Exit() is called
  12. ↓
  13. OnExiting — the Exiting event is raised as the loop ends (once)
  14. ↓
  15. Dispose(bool) — teardown, but only when your code calls Dispose() (Run() does not, and the destructor's Dispose(false) does not reach UnloadContent()): components and the ContentManager are disposed first, then the registered graphics device service, whose device-disposing event invokes UnloadContent() (once, and only when a graphics device service such as GraphicsDeviceManager is registered)

Two consequences are easy to miss. Exiting fires before UnloadContent(), and by the time UnloadContent() runs (when it runs at all) the ContentManager has already been disposed, so it is a place to release resources you own, not to touch content. And with a fixed time step (the default), on the very first Update(), ElapsedGameTime is zero (see GameTime).

💡

On the web, Game may live on the stack. Older notes required a heap-allocated Game under Emscripten. That requirement is obsolete: Game::Run() now blocks on the caller's stack (it awaits requestAnimationFrame through Asyncify), so the MyGame game; game.Run(); entry point above is valid on every target. Application executables built by CNA enable Asyncify automatically; an external project that links CNA itself must link the CNA::EmscriptenAsyncify target.

Constructor

The constructor is where you create the GraphicsDeviceManager and perform any pre-initialization configuration. In CNA the base Game constructor has already built the GraphicsDevice (the renderer and, for a windowed renderer, the window) before your constructor body runs, but the manager's preferences (size, profile, presentation mode) are only applied later, when the game initialises just before Initialize(), so do not rely on them being in effect in the constructor. Set window dimensions and title through the manager and the Window property respectively.

class MyGame : public Game {
    GraphicsDeviceManager graphics_;
public:
    MyGame()
        : graphics_(this)                       // explicit GraphicsDeviceManager(Game*)
    {
        graphics_.setPreferredBackBufferWidthProperty(1280);
        graphics_.setPreferredBackBufferHeightProperty(720);
        graphics_.setSynchronizeWithVerticalRetraceProperty(true);
        getWindowProperty().setTitleProperty("My CNA Game");
    }
};

The default project GraphicsProfile is Reach, and the Reach limits (one render target, no occlusion queries, 16-bit indices, no float render targets) are enforced on every renderer. If your game needs HiDef features, ask for them here: graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef);. The default presentation mode is Letterbox: the Viewport is the back-buffer size whatever shape the window has.

Initialize()

Initialize() is called once after the graphics device has been created. Content loading starts when your override calls Game::Initialize(), which initialises the registered components and then calls LoadContent(); anything LoadContent() needs, and every component you add, must therefore be in place before that call (a component added after it is updated and drawn but never has its Initialize() called). 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
    getComponentsProperty().Add(std::make_shared<MyComponent>(*this));

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

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

Components.Add(std::shared_ptr<IGameComponent>) is a CNA extension that keeps the component alive for you; the XNA form takes a raw IGameComponent* and you must keep the object alive yourself. Component lists are mutex-guarded, so a loading-screen thread may add components.

LoadContent()

LoadContent() is called once, from inside Game::Initialize() (after it has initialised the components), so it runs at the point where your Initialize() override calls the base method. 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_unique<SpriteBatch>(getGraphicsDeviceProperty());
    // Load<T> returns by value (a failed load throws ContentLoadException);
    // Texture2D/SpriteFont are held in std::optional members because SpriteFont has no default constructor
    playerTex_.emplace(getContentProperty().Load<Texture2D>("textures/player"));
    font_.emplace(getContentProperty().Load<SpriteFont>("fonts/default"));
}

A stock Game already registered the built-in XNB readers in its constructor, so .xnb, .cnb and loose assets resolve with no setup.

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(GameTime& gameTime) override {
    auto kb = Keyboard::GetState();
    if (kb.IsKeyDown(Keys::Escape))
        Exit();

    float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
    position_.X += speed * dt;

    Game::Update(gameTime);         // updates registered components, then pumps FrameworkDispatcher
}

Draw(GameTime)

Draw() is called once per visual frame, immediately after Update(). With fixed timestep enabled, a frame that is running behind runs extra Update calls to catch up (the accumulated time is clamped to 500 ms) and still draws once; Draw is skipped only when SuppressDraw() or Exit() was called or BeginDraw() returns false, and it is never called more than once between two Update calls. Begin every Draw() with getGraphicsDeviceProperty().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 {
    getGraphicsDeviceProperty().Clear(Color::CornflowerBlue);

    spriteBatch_->Begin();
    spriteBatch_->Draw(*playerTex_, position_, 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, from the graphics device service's device-disposing event, when Game::Dispose() is called explicitly (or the registered graphics device service is disposed) — that is, after the main loop has exited and after OnExiting. Run() does not call Dispose(), the destructor's Dispose(false) never reaches UnloadContent(), and nothing is called when no graphics device service such as GraphicsDeviceManager is registered, so a game that only calls Run() and lets the object go out of scope never sees it; call game.Dispose() after Run() if you rely on it. The default implementation is empty: it does not unload content. The ContentManager is disposed by Game::Dispose(bool) just before the device, so every asset loaded through it is released for you and is already gone when UnloadContent() runs. Override it only to release GPU resources you created and own yourself.

void UnloadContent() override {
    // release manually created resources (the ContentManager has already been disposed)
    customRenderTarget_.reset();
    Game::UnloadContent();   // empty in the default implementation
}

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 CNA the distinction still decides which cleanup runs (see below): only the true path disposes components, the content manager and the device service. 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);
}

What the two paths do in CNA

In CNA the disposing flag decides almost everything. game.Dispose() calls Dispose(true): every IDisposable component is disposed (a DrawableGameComponent runs its UnloadContent() there), then the ContentManager, then the registered graphics device service, whose DeviceDisposing event calls the game's UnloadContent(); the Disposed event follows. The destructor calls Dispose(false), which only marks the game disposed — and since every scope exit and delete takes that path, a game that never calls Dispose() skips all of the above. An override should therefore do its explicit cleanup under if (disposing) and always call Game::Dispose(disposing); in CNA the Disposed event is raised by the public Dispose() itself, even when an override skips the base. Repeated, re-entrant and throwing disposal are covered on Exit, Exiting, Dispose and destruction.

GameTime

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

MemberTypeDescription
getElapsedGameTimeProperty() TimeSpan Time since the last Update() call. Call .getTotalSecondsProperty() to get a double suitable for delta-time movement calculations. With a fixed time step, on the first Update() it is zero (the XNA clock; measured on the real runtime), so a simulation seeded from dt does not jump on frame one; with a variable time step the first Update() receives the time measured since the loop started.
getTotalGameTimeProperty() TimeSpan Accumulated time since the game started running. It is the time before the current step, so it advances after Update() returns, in both fixed and variable modes. Useful for time-based animations and shaders.
getIsRunningSlowlyProperty() bool Set to true by the framework once the fixed-timestep loop's accumulated catch-up lag reaches five extra Update() steps: a single 50 ms hitch (three updates) never sets it, while a stall of about 100 ms or more does at once (see How IsRunningSlowly is decided). Useful for disabling expensive non-essential work during a slowdown.

TimeSpan helper methods:

  • .getTotalSecondsProperty() — elapsed time as a double in seconds (most common for delta-time)
  • .getTotalMillisecondsProperty() — elapsed time in milliseconds
  • .getTotalMinutesProperty() — elapsed time in minutes

There is no GameTime::ElapsedSeconds() helper; go through getElapsedGameTimeProperty().getTotalSecondsProperty().

How IsRunningSlowly is decided

getIsRunningSlowlyProperty() is a smoothed signal, not a per-frame one. After each fixed-step tick, CNA adds the tick's extra update steps (steps minus one) to a lag counter; the flag turns on when the counter reaches five and turns off only once it is back at zero, and every tick with a single step lowers the counter by one. So one 50 ms stutter (three updates) never sets it, five consecutive two-update ticks do, and a single stall of about 100 ms or more (six or more updates in one tick) sets it at once and keeps it set for about as many normal ticks afterwards. The flag is written after the tick's updates, so the updates that caused the lag do not see it; that tick's Draw does. Variable timestep never changes it, and the browser build always reports false. The worked traces and the comparison with XNA 4.0's different rule are on GameTime and the timestep.

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
get/setPreferredBackBufferWidthProperty int Desired back-buffer width in pixels
get/setPreferredBackBufferHeightProperty int Desired back-buffer height in pixels
get/setIsFullScreenProperty bool Toggles full-screen mode; call ApplyChanges() after setting (or call ToggleFullScreen())
get/setPreferMultiSamplingProperty bool Enables multisample anti-aliasing (MSAA) when true, on renderers that support it
get/setSynchronizeWithVerticalRetraceProperty bool Enables VSync; set to false for uncapped frame rate
get/setGraphicsProfileProperty GraphicsProfile Reach (default) or HiDef; the profile's limits are enforced on every renderer
get/setPreferredBackBufferFormatProperty, get/setPreferredDepthStencilFormatProperty SurfaceFormat, DepthFormat Back-buffer and depth/stencil format preferences
get/setPreferredPresentationModeProperty CNAEXT PresentationMode Letterbox (default), Overscan, Stretch or native size
getGraphicsDeviceProperty() GraphicsDevice* The underlying graphics device (from IGraphicsDeviceService); valid after Initialize(). In a Game subclass prefer getGraphicsDeviceProperty() on the game, which returns a reference.
// Toggle full-screen at runtime
graphics_.setIsFullScreenProperty(!graphics_.getIsFullScreenProperty());
graphics_.ApplyChanges();

GameWindow

GameWindow is accessed through game.getWindowProperty(). It represents the OS window and exposes properties for the title, client area, and resize behaviour. Setting the title and resize flag in the constructor is the normal XNA pattern and works; for any renderer that needs a window, the native window is already created during Game construction, so getClientBoundsProperty() can be queried from a derived constructor, although the manager's preferred size is only applied when the game initialises, just before Initialize() runs.

MemberTypeDescription
get/setTitleProperty std::string The title bar string shown by the OS
getClientBoundsProperty() Rectangle The current drawable area of the window. X and Y are the client area’s desktop position where the platform reports one (0 on Wayland); Width and Height are its actual size, and in fullscreen the back buffer’s size, as after XNA’s mode switch
get/setAllowUserResizingProperty 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
getCurrentOrientationProperty() DisplayOrientation Follows the surface CNA actually draws to rather than the OS window; OrientationChanged is the matching event
FileDropEXT, TextDropEXT CNAEXT events Drag-and-drop of files and text onto the window
// In the constructor
getWindowProperty().setAllowUserResizingProperty(true);
getWindowProperty().ClientSizeChanged += [this](System::Object*, const System::EventArgs&) {
    const Rectangle bounds = getWindowProperty().getClientBoundsProperty();
    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::getComponentsProperty() 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 (a component's constructor takes the Game&; getGameProperty() returns it)
Update(GameTime)Called each tick by Game::Update()
get/setEnabledPropertySet to false to pause Update() calls for this component
get/setUpdateOrderPropertyInteger 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()
get/setVisiblePropertySet to false to suppress Draw() calls without removing the component
get/setDrawOrderPropertyInteger sort key; lower values are drawn first (back-to-front)

Fixed vs variable timestep

CNA offers XNA's two timestep modes and, on the native loop, XNA's own clock rules (with the fixed step a zero-length first update; in both modes TotalGameTime advancing only after Update returns). It does not replicate XNA exactly: IsRunningSlowly follows a different rule, ResetElapsedTime() does nothing under the fixed step, InactiveSleepTime is stored but never applied, and the browser loop keeps its own clock (see the Emscripten caveat below and GameTime and the timestep). The default is fixed timestep.

PropertyDefaultEffect
get/setIsFixedTimeStepProperty 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 frame ran long. getIsRunningSlowlyProperty() turns true only once the accumulated catch-up lag reaches five extra steps (how it is decided). The very first Update() still has an elapsed time of zero.
get/setTargetElapsedTimeProperty 1/60 s TimeSpan that sets the desired tick interval under fixed timestep. Override to target 30 Hz, 120 Hz, or any other rate.
setIsFixedTimeStepProperty(false) — Variable timestep: Update() is called exactly once per render frame; getElapsedGameTimeProperty() reflects the true wall-clock delta and varies frame to frame. Divide all movement and physics by getElapsedGameTimeProperty().getTotalSecondsProperty().
⚠

Emscripten caveat. The browser frame body (Game::EmscriptenMainLoopCallback) keeps its own fixed-step accumulator: it clamps a step at 250 ms, does not honour IsFixedTimeStep = false, advances TotalGameTime before Update(), and gives the first update a full step. The XNA clock rule above applies to the native Tick() path only.

⚠

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

FrameworkDispatcher::Update()

FrameworkDispatcher::Update() pumps the audio and media event queues (for example the SDL3_mixer or ALSA mixer callbacks) on the main thread, so that sound effects, streaming songs and the media player advance. The default Game::Update(GameTime&) already calls it after updating the registered components, so a normal game does nothing: just end your Update() override with Game::Update(gameTime). Call it yourself only if you override Update() without calling the base class, or in a program that has no Game at all (a tool or a test that plays audio).

void Update(GameTime& gameTime) override {
    // ... your logic ...
    Game::Update(gameTime);      // components + FrameworkDispatcher::Update()
}

Code examples

1. Minimal Game subclass

#include <Microsoft/Xna/Framework/Game.hpp>
#include <Microsoft/Xna/Framework/GraphicsDeviceManager.hpp>
#include <Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp>
#include <Microsoft/Xna/Framework/Graphics/Texture2D.hpp>
#include <Microsoft/Xna/Framework/Input/Keyboard.hpp>
#include <memory>
#include <optional>

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

class MyGame : public Game {
    GraphicsDeviceManager graphics_;
    std::unique_ptr<SpriteBatch> spriteBatch_;
    std::optional<Texture2D>     playerTex_;

public:
    MyGame() : graphics_(this) {
        graphics_.setPreferredBackBufferWidthProperty(1280);
        graphics_.setPreferredBackBufferHeightProperty(720);
        graphics_.setSynchronizeWithVerticalRetraceProperty(true);
        getWindowProperty().setTitleProperty("My CNA Game");
    }

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

    void LoadContent() override {
        spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
        playerTex_.emplace(getContentProperty().Load<Texture2D>("textures/player"));
    }

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

    void Draw(const GameTime& gameTime) override {
        getGraphicsDeviceProperty().Clear(Color::CornflowerBlue);
        spriteBatch_->Begin();
        spriteBatch_->Draw(*playerTex_, Vector2(100, 200), Color::White);
        spriteBatch_->End();
        Game::Draw(gameTime);
    }
};

int main() {
    MyGame game;      // a stack-allocated Game is valid on every target, including the web
    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(GameTime& gameTime) override {
    // getTotalSecondsProperty() converts the TimeSpan to a double
    float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());

    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::optional<SpriteFont>    font;
    std::unique_ptr<SpriteBatch> batch;
    const int& score;
public:
    HudComponent(Game& game, const int& score)
        : DrawableGameComponent(game), score(score) {}

protected:
    void LoadContent() override {
        font.emplace(getGameProperty().getContentProperty().Load<SpriteFont>("fonts/hud"));
        batch = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
    }

public:
    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():
getComponentsProperty().Add(std::make_shared<HudComponent>(*this, score_));

4. Variable timestep setup

MyGame() : graphics_(this) {
    graphics_.setPreferredBackBufferWidthProperty(1920);
    graphics_.setPreferredBackBufferHeightProperty(1080);

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

    // Uncap the frame rate by disabling VSync too (optional)
    graphics_.setSynchronizeWithVerticalRetraceProperty(false);
}