A first CNA game, read line by line
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Both programs were syntax-checked with g++ -fsyntax-only against the TARGET headers; nothing was built or run, and the renderer paragraph is not backed by a run on each renderer.
This page reads a small CNA game line by line and then grows it into a moving, bouncing, collecting sprite, explaining at each step which framework contract the code relies on and where CNA's conveniences differ from portable XNA 4.0 code. It is for a reader who has the build running and wants to know why the skeleton looks the way it does before writing a real game; the hands-on sequence is Tutorial 03 and Tutorial 04, and the contracts are explained in depth on The Game class.
The whole program
A complete translation unit. The CMake target and the assets/logo.png file are project inputs; the file path is resolved against the process's working directory.
#include <memory>
#include "Microsoft/Xna/Framework/Color.hpp"
#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"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
class MyGame final : public Game {
public:
MyGame() : graphics_(this) {}
protected:
void LoadContent() override
{
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
logo_ = std::make_unique<Texture2D>("assets/logo.png", getGraphicsDeviceProperty());
}
void Update(GameTime& gameTime) override
{
// game state goes here
Game::Update(gameTime); // components, then FrameworkDispatcher::Update()
}
void Draw(const GameTime& gameTime) override
{
auto& device = getGraphicsDeviceProperty();
device.Clear(Color::CornflowerBlue);
spriteBatch_->Begin();
spriteBatch_->Draw(*logo_, 100.0f, 80.0f); // CNAEXT overload
spriteBatch_->End();
Game::Draw(gameTime); // drawable components; no Present() here
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
std::unique_ptr<Texture2D> logo_;
};
int main()
{
MyGame game;
game.Run();
return 0;
}
Reading it top to bottom
The includes are the public surface
Every header path follows the XNA namespace path (Microsoft/Xna/Framework/…), and nothing CNA-specific is needed to boot a game. That is the API-mirroring goal made visible: a developer who has written XNA or FNA code can read the file without learning a CNA concept first. Property access is the one systematic spelling change — GraphicsDevice becomes getGraphicsDeviceProperty() (language conventions).
The game owns a manager, and the manager does not own the device
MyGame derives from Game and owns a GraphicsDeviceManager constructed against itself — the standard XNA source pattern. The C++ ownership underneath differs from FNA's: the Game base constructor has already built its own GraphicsDevice (renderer and, for a windowed renderer, the window) before the manager member is constructed. The manager registers itself as the game's device-manager and device services and, during DoInitialize, applies its preferences by resetting that same game-owned device; it never constructs or owns a second one. GameWindow and GraphicsDeviceManager gives the exact sequence.
LoadContent() is where GPU resources are created
By the time LoadContent() runs, the renderer exists and the manager has applied its preferences, so creating a SpriteBatch against getGraphicsDeviceProperty() and loading a texture are safe. The derived constructor could technically already reach the device — it is live — but that is the pre-preference device, before the reset that applies back-buffer size, profile and presentation mode, so the constructor is the wrong point for content. The reason to defer is that reset, not an absence of the device.
Update(GameTime&) and Draw(const GameTime&)
The two overrides are the halves of every tick, and their signatures differ on purpose: Update receives a mutable reference, Draw a const one, because rendering must not change timing state (and a game should not assign to the Update parameter either — why). The base calls are framework work, not ceremony: Game::Update(gameTime) updates the registered components and then pumps FrameworkDispatcher::Update(), so omitting it silently stops dynamic sound streams, microphone buffers, media-player transitions and touch input; Game::Draw(gameTime) draws the registered drawable components.
Clear, the SpriteBatch triad, and no Present()
GraphicsDevice::Clear takes a named colour; Color::CornflowerBlue is the clear colour of XNA's generated project templates, kept rather than replaced by something "more CNA". Drawing is the Begin() / Draw(…) / End() triad (SpriteBatch guide). Draw() does not call GraphicsDevice::Present(): after the override returns, Game::EndDraw() delegates to the manager, which presents exactly once whichever renderer is active. A Present() inside Draw() presents every frame twice.
main() is two lines
Construct the game, call Run(). Window creation, renderer initialization, the timing loop and the exit sequence live inside Game, which keeps the shape of an XNA Program.Main so that porting an entry point is close to mechanical. Run() returning does not dispose anything; add game.Dispose(); after it when UnloadContent() must run (the three endings).
Two CNA conveniences in the minimal program
Two calls above are CNAEXT extensions, not XNA 4.0 API:
| Minimal program uses | What it is | Portable XNA 4.0 form |
|---|---|---|
Texture2D("assets/logo.png", device) | Decodes a loose image file directly (straight alpha, see Tutorial 08) | getContentProperty().Load<Texture2D>("logo"), which returns the texture by value and resolves .xnb, then .cnb, then a loose file under the content root |
spriteBatch_->Draw(*logo_, 100.0f, 80.0f) | A two-float position overload | Draw(texture, Vector2 position, Color color) |
Both are marked in Texture2D.hpp and SpriteBatch.hpp; with CNA_STRICT_XNA_API defined they produce deprecation warnings. Code meant to port back to XNA or FNA should prefer the right-hand column; the rest of this page uses the XNA Draw overload.
Movement, edges, sound and a collision
The same skeleton, extended in three steps: arrow-key movement at a frame-rate-independent speed, clamping to the visible area with a sound on arrival, and a coin that is collected on overlap. This is the finished translation unit.
#include <memory>
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Rectangle.hpp"
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include "Microsoft/Xna/Framework/Audio/SoundEffect.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Audio;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Input;
class MyGame final : public Game {
public:
MyGame() : graphics_(this) {}
protected:
void LoadContent() override
{
auto& device = getGraphicsDeviceProperty();
spriteBatch_ = std::make_unique<SpriteBatch>(device);
logo_ = std::make_unique<Texture2D>("assets/logo.png", device); // CNAEXT
coin_ = std::make_unique<Texture2D>("assets/coin.png", device); // CNAEXT
bounce_ = std::make_unique<SoundEffect>("assets/bounce.wav"); // CNAEXT
}
void Update(GameTime& gameTime) override
{
const KeyboardState keys = Keyboard::GetState();
const float seconds =
static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
const float speed = 200.0f; // pixels per second
if (keys.IsKeyDown(Keys::Left)) position_.X -= speed * seconds;
if (keys.IsKeyDown(Keys::Right)) position_.X += speed * seconds;
if (keys.IsKeyDown(Keys::Up)) position_.Y -= speed * seconds;
if (keys.IsKeyDown(Keys::Down)) position_.Y += speed * seconds;
// Clamp against the logical draw space, not a hard-coded window size.
const Viewport& viewport = getGraphicsDeviceProperty().getViewportProperty();
const float maxX = static_cast<float>(viewport.getWidthProperty() - logo_->getWidthProperty());
const float maxY = static_cast<float>(viewport.getHeightProperty() - logo_->getHeightProperty());
bool atEdge = false;
if (position_.X < 0.0f) { position_.X = 0.0f; atEdge = true; }
if (position_.X > maxX) { position_.X = maxX; atEdge = true; }
if (position_.Y < 0.0f) { position_.Y = 0.0f; atEdge = true; }
if (position_.Y > maxY) { position_.Y = maxY; atEdge = true; }
if (atEdge && !wasAtEdge_)
bounce_->Play(); // once per arrival, not once per frame
wasAtEdge_ = atEdge;
if (!collected_) {
const Rectangle logoBounds(static_cast<int>(position_.X), static_cast<int>(position_.Y),
logo_->getWidthProperty(), logo_->getHeightProperty());
const Rectangle coinBounds(static_cast<int>(coinPosition_.X), static_cast<int>(coinPosition_.Y),
coin_->getWidthProperty(), coin_->getHeightProperty());
if (logoBounds.Intersects(coinBounds)) {
collected_ = true;
++score_;
bounce_->Play(0.5f, 0.5f, 0.0f); // volume, pitch, pan
}
}
Game::Update(gameTime);
}
void Draw(const GameTime& gameTime) override
{
getGraphicsDeviceProperty().Clear(Color::CornflowerBlue);
spriteBatch_->Begin();
spriteBatch_->Draw(*logo_, position_, Color::White); // XNA 4.0 overload
if (!collected_)
spriteBatch_->Draw(*coin_, coinPosition_, Color::White);
spriteBatch_->End();
Game::Draw(gameTime);
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
std::unique_ptr<Texture2D> logo_;
std::unique_ptr<Texture2D> coin_;
std::unique_ptr<SoundEffect> bounce_;
Vector2 position_{100.0f, 80.0f};
Vector2 coinPosition_{300.0f, 200.0f};
bool wasAtEdge_ = false;
bool collected_ = false;
int score_ = 0;
};
int main()
{
MyGame game;
game.Run();
return 0;
}
Movement: polled input and elapsed time
Keyboard::GetState() is a static call that returns a fresh, immutable KeyboardState snapshot; there is no keyboard object to own or keep between frames (the same shape as Mouse and GamePad, see Input). The snapshot is published once per tick, before the updates, so every catch-up update of one tick sees the same keys.
Speed is a pixels-per-second constant multiplied by gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty() — the declared TimeSpan accessor pair, not a shortcut. Two clock facts qualify the usual frame-rate-independence story. Under the default fixed step every update after the first sees exactly one TargetElapsedTime and updates run at the target rate whatever the display does, so even a flat per-update step would not speed up on a faster monitor; multiplying by elapsed time is what keeps the speed right when the game changes TargetElapsedTime or switches to variable step, where one update runs per drawn frame and a flat step would move four times faster on a display with four times the refresh rate. And the game's first update sees a zero elapsed time, so nothing moves on frame one (clock rules).
Edges: clamp in the same branch that detects
Setting the coordinate back to 0 or the maximum in the same branch that detects the crossing matters: without it, holding a key against the edge keeps accumulating position outside the screen, and the sprite has to travel back through the whole overshoot before it reappears. The bounds come from the device's Viewport, the logical draw space, not from numbers the game happened to start with. With the default Letterbox presentation mode the viewport is the back-buffer size whatever shape the window has, so the clamp stays correct when the player resizes the window; under modes whose logical size follows the window, reading the viewport each update is what keeps it correct (presentation modes).
The edge flag stays true on every update while the key is held — including every catch-up update of a slow tick — so the sound is triggered on the transition into the edge, not on every frame at the edge. SoundEffect's file-path constructor is a CNAEXT convenience, the same shape as Texture2D's; a real XNA game loads a SoundEffect through the content pipeline. Play() uses default volume, pitch and pan; Play(volume, pitch, pan) is the XNA overload for varying them. Which audio implementation plays the sound is a separate configure-time choice (Audio).
Collision: rectangles built from the drawn state
The two bounding rectangles are rebuilt on every update from the same position fields Draw reads and the texture sizes, so the collision test and the visible sprites cannot disagree. Rectangle's constructor takes integer x, y, width and height, hence the explicit static_cast<int> of the float positions (truncation toward zero; the clamp keeps them non-negative here). Rectangle::Intersects is strict: two 100 × 100 rectangles at (0, 0) and (100, 0) share an edge with zero overlap and do not intersect, which RectangleTests.cpp pins as IntersectsAdjacentRectanglesReturnsFalse. A coin placed exactly at the logo's width never registers as touched; only a non-zero overlap counts.
What changes when the renderer changes
Nothing in the file names a renderer. Configuring the same unmodified source with -DCNA_GRAPHICS_RENDERER=VULKAN instead of the Linux default OPENGLES3 gives a program whose device, texture upload and sprite batching go through Vulkan instead of OpenGL ES, with the game-facing code unchanged. The program uses only the renderer-independent 2D layer (Clear, SpriteBatch, Texture2D), so it needs no 3D capability and is also in scope for the seven 2D-only renderers. What differs underneath — presentation, formats, capability limits, the default Reach profile's enforcement — is the subject of Renderers and runtime renderer selection.
Evidence and limits
Both programs were syntax-checked as complete translation units with g++ -std=c++23 -fsyntax-only -Wall -Wextra against the TARGET headers (with the SDL_RENDERER and SDL3 configuration definitions and Sharp Runtime next @ 41b918c9), without warnings; they were not built, linked or run, and the asset files are placeholders. The API facts were read from the public headers linked above, Game.cpp and Rectangle.cpp at this snapshot. The renderer paragraph states what the renderer contract promises, not a run on each renderer.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Getting started · Tutorial 03: your first CNA window · Tutorial 12: moving sprites · Tutorial 16: collision detection
- Architecture
- Runtime lifecycle
- Internals
- Startup source trace
- Deep dives
- The Game class · GameTime and the timestep · Language conventions