Tutorial 09: Drawing Text with SpriteFont

CNA Tutorial Series  ·  Beginner

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.