Chapter 5 Your First CNA Game
This chapter develops a compact CNA program from the XNA-style skeleton in CNA’s README. It is a complete translation unit, but the surrounding CMake target and named assets remain project inputs. Later chapters supply the detailed framework contracts.
5.1 The whole program
5.2 Reading it top to bottom
The includes are the public surface. Each header follows the XNA namespace path; no CNA-specific include is required just to boot a game. This is the API-mirroring goal from Chapter 3 made visible: a developer who has written XNA or FNA code before can read this file without learning a single CNA-specific concept first.
MyGame inherits Game, and owns a GraphicsDeviceManager constructed against itself. This is the standard XNA source-level pattern, but CNA’s C++ ownership differs from FNA’s: the Game base already default-constructs its own GraphicsDevice before the derived constructor creates this manager. The manager registers the lifecycle services and, during Game::DoInitialize(), applies the preferred settings by resetting that same game-owned object; it does not construct or own a second device. Chapter 6 covers that boot sequence in full, and Chapter 12 covers what the resulting GraphicsDevice exposes.
LoadContent() is where GPU-backed resources are created, not the constructor. By the time LoadContent() runs, the graphics renderer has already been constructed by the base Game and then configured by GraphicsDeviceManager, so it is safe to construct a SpriteBatch against getGraphicsDeviceProperty() and to load a Texture2D from a file path. A derived constructor can technically already see CNA’s default renderer, but it precedes the manager’s preferred-setting reset and is the wrong lifecycle point for content. Defer resources to LoadContent() for the stable, configured device rather than relying on the old, incorrect claim that no renderer exists yet. This ordering constraint — and CNA’s raw-asset-plus-JSON-descriptor content model that makes "assets/logo.png" a literal, directly loadable file path rather than a compiled .xnb content-pipeline output — is covered fully in Chapter 33.
Update(GameTime&) and Draw(const GameTime&) are the two halves of every frame, and note both the constness difference and the base calls. Update takes a mutable GameTime& (some XNA game loops advance simulation-only clocks here); Draw takes a const GameTime&, since rendering should not mutate timing state. Calling Game::Update(gameTime) runs registered updateable components and the framework dispatcher; calling Game::Draw(gameTime) runs registered drawable components. Omitting the first call silently stops dynamic audio, media, and touch polling. Chapter 6 covers the full lifecycle and the ordering choices this pair of overrides plugs into.
The draw call is the canonical SpriteBatch triad: Begin() / Draw(...) / End(). GraphicsDevice::Clear takes a named XNA color constant (Color::CornflowerBlue — traditionally XNA’s default clear color in generated project templates, preserved here rather than swapped for something “more CNA”). Application Draw() does not call GraphicsDevice::Present() itself: after the override returns, Game::EndDraw() delegates to the manager, which presents exactly once regardless of which public renderer identity from Part IV is active. Chapter 13 covers the full SpriteBatch API, including the overloads of Draw beyond the two-float-position form used here.
main() is exactly two lines of game-specific code. Construct the game, call Run(). Everything about window creation, renderer initialization, the timing loop, and shutdown lives inside Game::Run() — covered in Chapter 6 — not in application code. This is deliberate: it is the same shape of XNA Program.cs’s Main method, preserved so that porting an existing XNA game’s entry point is close to a mechanical translation.
5.3 A second program: making it move
The first program leaves Update empty. The next fragment extends the same skeleton with arrow-key movement at a frame-rate-independent speed. It shows only the changed members and additional includes:
Reading polled input is a static call, not an object you own. Keyboard has no constructor to call and nothing to store between frames — Keyboard::GetState() is a static method returning a fresh, immutable KeyboardState snapshot every time it is called, and Update calls it once per frame. This is the same shape Chapter 44 covers for Mouse and GamePad.
Frame-rate independence comes from GameTime, not a hardcoded per-frame step. Multiplying a pixels-per-second constant by gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty() — the declared TimeSpan accessor pair, not an invented shortcut — means the logo moves at the same real-world speed whether the game is running at 30 FPS or 240 FPS. A version that instead moved by a flat constant every Update call would move four times faster on a display with a four times higher refresh rate, which is the class of bug frame-rate-independent movement exists to avoid.
Why this program uses a different Draw overload than the first one. The first program in this chapter calls spriteBatch_->Draw(*logo_, 100.0f, 80.0f) — but that specific two-float-position overload, reading modules/graphics/include/Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp directly, is tagged CNAEXT: a CNA-only convenience overload, not part of XNA 4.0 (Chapter 3 covers what that tag means). This second program instead calls Draw(*logo_, position_, Color::White), the unmarked XNA 4.0 overload that takes a Vector2 position and an explicit tint color — worth calling out directly here, since a reader who wants code that ports cleanly to XNA/FNA should reach for this overload as the default, not the convenience one the first, minimal example used to stay as short as possible.
5.4 A third program: bouncing off the edges, with sound
The second program does not constrain the sprite to the viewport. This fragment clamps it to the backbuffer bounds and plays a sound when a boundary is reached:
The bounds check clamps position, it does not just detect it. Setting position_.X back to 0.0f or maxX in the same branch that detects the violation matters: without it, holding a direction key against the edge would let position_ keep accumulating past the boundary every frame, and the sprite would need to travel back through the entire out-of-bounds distance before visibly re-entering the screen. Clamping in the same statement that detects the crossing keeps the visible sprite exactly at the edge, every frame, for as long as the key is held.
The viewport, not a hardcoded screen size, is what maxX / maxY are computed against. Viewport::Width / Height (Chapter 12) reflect whatever the back buffer’s actual current size is — reading them here, rather than hardcoding the numbers the game happened to launch with, is what keeps this bounds check correct if the window is resized or the game runs under a different PresentationMode (Chapter 6).
SoundEffect’s file-path constructor is a CNAEXT convenience, the same shape as Texture2D’s. Real XNA has no such constructor either — a real XNA game always loads a SoundEffect through the compiled content pipeline. Play() here uses the simplest zero-argument overload (default volume, pitch, and pan); a real game that wants to vary bounce loudness by impact speed would reach for the three-argument Play(volume, pitch, pan) overload instead — covered in full in this book’s audio chapter.
5.5 A fourth program: rectangle collision
The third program reacted to the screen’s own edges; this section adds the other kind of reaction a first game almost always needs next — two sprites reacting to each other. A second, static texture (a coin) sits on screen; when the moving logo’s own bounding rectangle overlaps it, the coin is collected and a counter increments. One more addition on top of the same running example:
The bounding rectangles are built fresh every frame, from the same position/size data already driving movement and drawing — not stored and kept in sync by hand. Rectangle’s constructor (Chapter 7) takes an integer x / y / width / height, so position_’s float components need an explicit static_cast<int> — the same pattern Texture2D::Width / Height already supply directly for the size half of each rectangle. Building both rectangles inline in Update(), from the exact position fields Draw() also reads, is what keeps the collision check and the visible sprites from ever silently disagreeing about where anything actually is.
Touching edges do not intersect. RectangleTests.cpp checks two rectangles placed at and — sharing an edge exactly, with zero pixels of true overlap — return false from Intersects(), as do separated rectangles. Only non-zero-area overlap counts. A game that places a coin exactly at the logo’s own width, expecting the two to register as “touching” the instant they meet, will see the check silently never fire; leaving at least a few pixels of deliberate overlap margin between two sprites meant to collide avoids this class of bug entirely.
5.6 What happens if you change the renderer
Nothing in the file above mentions a graphics renderer at all — that is the entire point of Chapter 19’s abstraction layer. Recompiling this exact, unmodified source file with -DCNA_GRAPHICS_RENDERER=VULKAN instead of OPENGLES3 produces a program that opens a Vulkan-backed window, uploads assets/logo.png through the Vulkan texture path, and issues the same SpriteBatch draw call through Vulkan command buffers instead of OpenGL calls — with the game-facing source unchanged. Part IV is dedicated to what actually differs underneath that unchanged surface, renderer by renderer.