Tutorial 09: Drawing Text with SpriteFont
SpriteFont Overview
SpriteFont in CNA works the same way as in XNA 4.0. A SpriteFont is a pre-rasterized bitmap font — each glyph is an image in a texture atlas. You load the font asset, then draw strings with SpriteBatch::DrawString().
CNA's SpriteFont uses the same API as XNA and MonoGame's SpriteFont. You do not need a shader or a TrueType renderer at runtime — all rasterization is done at asset-build time.
#include "Microsoft/Xna/Framework/Graphics/SpriteFont.hpp"
The font.json Descriptor
CNA's content pipeline uses a JSON descriptor to define a sprite font. Place a myfont.json and the corresponding PNG texture atlas in your assets folder:
// assets/fonts/arial16.json
{
"texture": "arial16_atlas.png",
"glyphData": "arial16_glyphs.json",
"lineSpacing": 20,
"spacing": 1,
"defaultCharacter": "?"
}
The glyph data file lists the position of each character in the atlas:
// arial16_glyphs.json (abbreviated)
{
"glyphs": [
{ "char": "A", "x": 0, "y": 0, "w": 11, "h": 16, "xOffset": 0, "yOffset": 0, "advance": 12 },
{ "char": "B", "x": 11, "y": 0, "w": 10, "h": 16, "xOffset": 0, "yOffset": 0, "advance": 11 },
{ "char": " ", "x": 0, "y": 0, "w": 0, "h": 0, "xOffset": 0, "yOffset": 0, "advance": 4 }
]
}
In practice, you generate these files from a TrueType font using an offline tool such as msdf-bmfont, bmfont, or a CNA content-builder script. The exact format is documented in the CNA repository's docs/content-pipeline.md.
For quick testing, CNA ships with a built-in debug font accessible without any asset files. See SpriteFont::GetDebugFont(device) in the API reference. It is not suitable for production but lets you draw text immediately without preparing assets.
Loading Fonts
void MyGame::Initialize() {
Game::Initialize();
Content.setRootDirectory("assets");
}
void MyGame::LoadContent() {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
// Load from assets/fonts/arial16.json
font_ = Content.Load<SpriteFont>("fonts/arial16");
}
Alternatively, construct directly from file paths:
font_ = std::make_unique<SpriteFont>(
getGraphicsDeviceProperty(),
"assets/fonts/arial16.json"
);
SpriteBatch::DrawString Overloads
DrawString mirrors the XNA API. The string parameter accepts std::string and is automatically converted:
Simplest: position and color
spriteBatch_->Begin();
spriteBatch_->DrawString(*font_, "Hello, CNA!", Vector2(50.0f, 50.0f), Color::White);
spriteBatch_->End();
With rotation, origin, scale
// Full signature:
// DrawString(font, text, position, color, rotation, origin, scale, effects, depth)
spriteBatch_->DrawString(
*font_,
"Score: 9999",
Vector2(400.0f, 300.0f), // position
Color::Yellow, // color
0.0f, // rotation (radians)
Vector2::Zero, // origin
1.5f, // scale (1.5x size)
SpriteEffects::None, // flip
0.0f // depth
);
Dynamic string from game data
// Draw current score in Draw()
std::string scoreText = "Score: " + std::to_string(score_);
spriteBatch_->DrawString(*font_, scoreText, Vector2(10.0f, 10.0f), Color::White);
// Draw elapsed time
double seconds = gameTime.getTotalGameTime().getTotalSeconds();
std::string timeText = "Time: " + std::to_string(static_cast<int>(seconds));
spriteBatch_->DrawString(*font_, timeText, Vector2(10.0f, 30.0f), Color::Cyan);
MeasureString
MeasureString returns the pixel dimensions of a string drawn with a given font. Use it to position text relative to other elements:
Vector2 size = font_->MeasureString("Hello, CNA!");
// size.X = width in pixels, size.Y = height in pixels
std::cout << "Text width: " << size.X << ", height: " << size.Y << "\n";
Call MeasureString in LoadContent() for strings that do not change, not in Draw(). For dynamic strings (score, FPS counter) it is safe to call in Draw but consider caching the result if performance matters.
Text Alignment Tricks
Centre text horizontally
std::string text = "GAME OVER";
Vector2 textSize = font_->MeasureString(text);
auto& vp = getGraphicsDeviceProperty().getViewport();
float x = (vp.getWidth() - textSize.X) / 2.0f;
float y = (vp.getHeight() - textSize.Y) / 2.0f;
spriteBatch_->DrawString(*font_, text, Vector2(x, y), Color::White);
Right-align text
std::string livesText = "Lives: " + std::to_string(lives_);
Vector2 textSize = font_->MeasureString(livesText);
float screenRight = static_cast<float>(getGraphicsDeviceProperty().getViewport().getWidth());
float x = screenRight - textSize.X - 10.0f; // 10px margin from right edge
spriteBatch_->DrawString(*font_, livesText, Vector2(x, 10.0f), Color::White);
Centre using origin parameter
The origin is the "anchor point" of the text in texture space. Setting it to half the text size draws the text centred at the given position:
std::string text = "PAUSED";
Vector2 size = font_->MeasureString(text);
Vector2 origin = size / 2.0f; // centre of the text block
Vector2 centre(400.0f, 300.0f);
spriteBatch_->DrawString(*font_, text, centre, Color::White,
0.0f, origin, 1.0f, SpriteEffects::None, 0.0f);
Stacking multiple lines
float lineHeight = font_->getLineSpacing(); // pixels between baselines
float y = 50.0f;
spriteBatch_->DrawString(*font_, "Line 1", Vector2(50.0f, y), Color::White);
y += lineHeight;
spriteBatch_->DrawString(*font_, "Line 2", Vector2(50.0f, y), Color::White);
y += lineHeight;
spriteBatch_->DrawString(*font_, "Line 3", Vector2(50.0f, y), Color::White);
Unicode Considerations
CNA's SpriteFont supports Unicode through the glyph atlas approach. Characters not present in the atlas are replaced by the defaultCharacter set in the font descriptor (usually ?).
To support characters beyond ASCII, include them in the font atlas at build time. The font descriptor's glyph data must list every character you want to render.
For ASCII-only games (English with standard punctuation) a basic atlas covering codepoints 32–126 is sufficient:
// Characters 32–126 cover: space, !"#$%&'()*+,-./0-9:;<=>?@A-Z[\]^_`a-z{|}~
// This is sufficient for most English-language game text.
For multilingual games, generate a larger atlas that covers the Unicode ranges you need and specify all glyphs in the descriptor.
std::string in CNA is treated as UTF-8. Multi-byte sequences are decoded at draw time if the font atlas contains the corresponding Unicode codepoints.
With text rendering in hand, you are ready for interactive input. Tutorial 10 covers keyboard input — reading key state, detecting presses and releases, and building keyboard-driven movement.