Tutorial 09: Drawing Text with SpriteFont
What you’ll learn
- Where a
SpriteFontcomes from (compiled XNB/CNB, or the.cnjfont descriptor) and how it is 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.
A SpriteFont can reach your game in three ways, and getContentProperty().Load<SpriteFont>("name") takes the same asset name for all of them (the ladder is name.xnb, then name.cnb, then the loose name.cnj):
- A compiled font built from an XNA
.spritefontfile. CNA’s build-time content pipeline (thecna-contenttool; the.spritefontroute needs FreeType, controlled byCNA_ENABLE_FONT_PIPELINE, defaultAUTO) rasterizes the font into an atlas and writes it as.cnb(CNA’s native format, the default) or as an XNA-compatible.xnb. - A genuine XNA
.xnbSpriteFont produced by an XNA/MonoGame/FNA content build. CNA’s built-in XNB readers include the SpriteFont reader. - A hand-made or generated
.cnjdescriptor, described in the next section. This needs no build tool at all, which makes it the easiest thing to start with.
#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
MyGame::MyGame() : graphics_(this) {
// Set the content root in the constructor: the base Game::Initialize()
// is what calls LoadContent(), so a root set after that call is too late.
getContentProperty().setRootDirectoryProperty("assets");
}
void MyGame::LoadContent() {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
// Load assets/fonts/arial16.xnb, else arial16.cnb, else arial16.cnj.
// Load<T> returns the font BY VALUE; SpriteFont has no default constructor,
// so the member is a std::optional<SpriteFont> (declare it as such).
font_ = getContentProperty().Load<SpriteFont>("fonts/arial16");
}
With std::optional<SpriteFont> font_; as the member, the examples below dereference it (*font_ where a const SpriteFont& is wanted, font_->MeasureString(...) for calls). 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. Glyph keys are 16-bit, so only characters in the Basic Multilingual Plane can be in a font, and an invalid UTF-8 sequence in your string is decoded as ? (which the font must then contain). If a character is in neither the atlas nor the defaultCharacter, DrawString throws an ArgumentException — so always set a default character.
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.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- SpriteFont and DrawString text layout — Exact text layout in CNA: SpriteFont tables and default character, per-glyph advance and the XNA first-glyph rule, MeasureString, UTF-8 decoding, whole-string flips, rotation and sub-pixel glyphs.