Tutorial 09: Drawing Text with SpriteFont
What you’ll learn
- The
.cnjfont descriptor and how aSpriteFontis loaded. - The
DrawStringoverloads and what each parameter controls. - Using
MeasureStringto centre and align text. - What to watch for with non-ASCII characters.
Before you start — Tutorial 08: Loading and Drawing Textures — fonts load through the same ContentManager path as textures.
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 .cnj Font Descriptor
A sprite font is described by a single .cnj document — CNA's one JSON content format, the same one used for models and effects. It carries a cnjVersion and a type, and the type must be "SpriteFont" or loading raises a ContentLoadException. Everything, including the glyph table, lives in that one file; there is no separate glyph-data file.
// assets/fonts/arial16.cnj
{
"cnjVersion": 1,
"type": "SpriteFont",
"texture": "fonts/arial16_atlas",
"lineSpacing": 20,
"spacing": 1.0,
"defaultCharacter": "?",
"glyphs": [
{ "char": 65, "source": [0, 0, 11, 16], "crop": [0, 0, 11, 16], "kerning": [0.0, 12.0, 0.0] },
{ "char": 66, "source": [11, 0, 10, 16], "crop": [0, 0, 10, 16], "kerning": [0.0, 11.0, 0.0] },
{ "char": 32, "source": [0, 0, 0, 0], "crop": [0, 0, 0, 0], "kerning": [0.0, 4.0, 0.0] }
]
}
The fields the reader actually understands are:
texture— required. The glyph atlas, given as a content asset name, not a file path. It is loaded back throughContentManager, so it is resolved and cached like any other texture. Omitting it raises aContentLoadException.lineSpacing— vertical distance between baselines, in pixels.spacing— additional horizontal spacing between characters.defaultCharacter— the substitute drawn for characters missing from the font. Only its first character is used.glyphs— one entry per character.charis the numeric code point;sourceis the glyph's[x, y, w, h]rectangle inside the atlas;cropis the[x, y, w, h]offset and size used when drawing;kerningis XNA's three floats — left bearing, advance width, right bearing.
Note that char is a number, and source, crop and kerning are arrays rather than named x/y/advance fields. In practice you generate this file with an offline tool rather than writing the glyph table by hand.
Loading Fonts
void MyGame::Initialize() {
Game::Initialize();
Content.setRootDirectoryProperty("assets");
}
void MyGame::LoadContent() {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
// Load from assets/fonts/arial16.cnj
font_ = getContentProperty().Load<SpriteFont>("fonts/arial16");
}
ContentManager is the practical way to obtain a SpriteFont. There is no constructor that takes a file path: the only public constructor takes an already-loaded atlas Texture2D plus the fully built glyph table (bounds, cropping, characters, line spacing, spacing, kerning, and the default character). That is the constructor the .cnj reader calls for you after parsing the descriptor.
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.getTotalGameTimeProperty().getTotalSecondsProperty();
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().getViewportProperty();
float x = (vp.getWidthProperty() - textSize.X) / 2.0f;
float y = (vp.getHeightProperty() - 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().getViewportProperty().getWidthProperty());
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_->getLineSpacingProperty(); // 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.