Tutorial 09: Drawing Text with SpriteFont

CNA Tutorial Series  ·  Beginner

What you’ll learn

  • The .cnj font descriptor and how a SpriteFont is loaded.
  • The DrawString overloads and what each parameter controls.
  • Using MeasureString to centre and align text.
  • What to watch for with non-ASCII characters.

Before you startTutorial 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:

  • texturerequired. The glyph atlas, given as a content asset name, not a file path. It is loaded back through ContentManager, so it is resolved and cached like any other texture. Omitting it raises a ContentLoadException.
  • 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. char is the numeric code point; source is the glyph's [x, y, w, h] rectangle inside the atlas; crop is the [x, y, w, h] offset and size used when drawing; kerning is 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.