For the complete documentation index, see llms.txt. This page is also available as Markdown.

Font Strategies

Introduction

This page covers each font loading strategy in detail with code samples and tradeoffs. To decide which strategy fits your game, start with the decision tree on the Fonts hub page.

The strategies are:

Per-Runtime Availability

Runtime
Dynamic generation today

MonoGame

Yes — via KernSmith.

KNI

Yes — via KernSmith.

Raylib

Yes — via KernSmith (KernSmith.RaylibGum).

FNA

Not yet. If you need it, let us know on Discord or open an issue.

Sokol

Not yet. Use the Build-Time Font Cache for now.

SkiaGum

Yes — uses SkiaSharp's own glyph rasterization. See Dynamic Generation on SkiaGum.

Dynamic KernSmith Generation

KernSmith is an in-memory font generator for MonoGame, KNI, and Raylib. Install the NuGet package for your runtime and KernSmith generates font atlases at runtime so you can freely change font properties without managing files on disk.

  1. Add the KernSmith.MonoGameGum NuGet package to your project.

  2. Assign the InMemoryFontCreator after initializing Gum:

Once this is set up, font properties work automatically. Setting Font, FontSize, IsBold, IsItalic, OutlineThickness, UseFontSmoothing, and baked drop shadow (HasDropshadow plus the dropshadow fields) on a TextRuntime generates the needed font in memory without any font files on disk:

Any combination of font properties can be used and the font is created on demand.

System Fonts vs Registered Fonts

By default, KernSmith resolves the Font property by looking up fonts installed on the operating system. Setting Font = "Times New Roman" works because that font is typically installed on Windows.

System fonts are convenient for quick prototyping, but they have drawbacks for shipping a game:

  • Platform differences — a font installed on your development machine may not exist on a player's machine or on other platforms (Linux, macOS, mobile).

  • Version inconsistency — different OS versions may ship different versions of the same font, causing subtle rendering differences.

  • Licensing — system fonts may have licenses that restrict redistribution in games.

For these reasons, registering your own .ttf (or .otf) files is recommended for any font you plan to ship with your game. This guarantees every player sees the same font regardless of their operating system.

Registering Custom .ttf Fonts

Registering .ttf files is supported on MonoGame and KNI. SkiaGum uses system fonts directly and does not currently support RegisterFont.

To use a .ttf file with KernSmith:

  1. Add the .ttf file to your project's Content folder.

  2. Set its Copy to Output Directory to Copy if newer. For an easier approach that handles all content files at once, see the wildcard .csproj setup in Loading a Gum Project (.gumx).

  3. Call KernSmithFontCreator.RegisterFont before using the font:

Once registered, use the font by its family name just like a system font:

You can register multiple fonts, including different styles for the same family:

A byte[] overload is also available for fonts loaded from embedded resources, HTTP responses, or other non-file sources. For example: KernSmithFontCreator.RegisterFont("MyFont", fontBytes).

Registered fonts take priority over system fonts. If you register a font with the family name "Arial", KernSmith uses your registered .ttf instead of the system-installed Arial.

When to Use This Strategy

  • You're using Latin, Cyrillic, Greek, or another small-charset script.

  • You want to change font, size, style, outline thickness, or baked drop shadow without rebuilding atlases.

  • You don't want to check generated .fnt files into source control.

  • For CJK or other large charsets, this strategy still works — but you should pair it with Font Preloading so the per-atlas generation cost happens on a loading screen rather than during gameplay.

Dynamic Generation on SkiaGum

SkiaGum is its own thing. It does not use KernSmith — SkiaSharp rasterizes glyphs directly, so dynamic font generation works out of the box. No NuGet package to install, no InMemoryFontCreator to assign.

Assign the font properties directly on the TextRuntime:

The font must be installed on the system to be used. SkiaGum does not currently support RegisterFont-style registration of shipped .ttf files.

A dedicated SkiaGum fonts page is planned. For now, this section is the canonical reference. If something is unclear, ask on Discord or open an issue.

Custom Font File

If you have a specific .fnt file (created with the Gum tool, Angelcode Bitmap Font Generator, Hiero, or another tool), you can load it directly by setting UseCustomFont to true and assigning CustomFontFile:

For information on creating your own .fnt file with Angelcode Bitmap Font Generator, see the Use Custom Font page.

This code assumes a font file named WhitePeaberryOutline.fnt is located in the Content/WhitePeaberryOutline folder. By default all Gum content loading is performed relative to the Content folder. See the File Loading page for more information.

.fnt files reference one or more image files, so the image file must also be added to the correct folder. In this case the WhitePeaberryOutline.fnt file references a WhitePeaberryOutline.png file, so both files are in the same folder.

WhitePeaberryOutline font in the Solution Explorer

Files are loaded from-file rather than using the content pipeline. This means that extensions (such as .fnt) are included in the file path, and that both the .fnt and .png files must have their Copy to Output Directory value set to Copy if newer.

Copy if newer property set

The easiest way to mark all content as "Copy to Output Directory" is to use wildcard items in your .csproj. For instructions (including Android), see Loading a Gum Project (.gumx).

When to Use This Strategy

  • You have a single hand-authored bitmap font you want to ship as-is.

  • You want full control over which .fnt file backs a given TextRuntime.

  • You don't need to vary size/style/outline at runtime — the file you assign is the file you get.

Direct BitmapFont Assignment

You can construct a BitmapFont yourself and assign it directly, bypassing the font property system entirely. Two sources for the BitmapFont are common:

From a .fnt File on Disk

Load a BitmapFont from a .fnt file (and its companion .png page textures) and assign it:

From KernSmith with Custom Options

TextRuntime exposes a subset of KernSmith's options (Font, FontSize, IsBold, IsItalic, OutlineThickness, UseFontSmoothing, and baked drop shadow via HasDropshadow and the dropshadow fields). To use outline color, gradient fills, SDF, color fonts, custom glyph subsets, or a non-default rasterizer backend, build a BitmapFont yourself by calling KernSmith directly, then assign it.

For the full catalog of effects available through this path, see Advanced Font Effects.

This path requires the KernSmith package for your runtime (KernSmith.MonoGameGum, KernSmith.KniGum, or KernSmith.RaylibGum). The bridge type GumFontGenerator lives in KernSmith.GumCommon, which is a transitive dependency.

Flow 1: Start from a BmfcSave, mutate options, then generate

This is the most common flow. BmfcSave carries the same font descriptor TextRuntime uses internally — start there so size, style, charset, and smoothing match your normal text, then layer in the extras KernSmith supports:

The helper that wraps a BmFontResult into a BitmapFont is the same pattern KernSmithFontCreator.TryCreateFont uses internally — one Texture2D per atlas page, then the BitmapFont(Texture2D[], string) constructor:

Flow 2: Build FontGeneratorOptions from scratch

If you don't have a BmfcSave to start from — for example you're constructing a one-off display font for a title screen — build the options directly:

The Channels setting matters. Gum's text renderer expects a specific channel layout: alpha = glyph (RGB = 1) when there is no outline, and alpha = outline (RGB = glyph) when there is one. GumFontGenerator.BuildOptions sets this for you — if you build options from scratch you must set it yourself or text will render incorrectly.

Sharing One BitmapFont Across Many TextRuntimes

A single BitmapFont instance can be assigned to any number of TextRuntimes. Generate once, assign many times — there is no per-runtime regeneration cost and the underlying atlas textures are shared:

When to Use This Strategy

  • You want a single BitmapFont instance shared across many TextRuntimes without each one re-loading from disk.

  • You're loading fonts from a non-standard source (embedded resource, network, custom pipeline) and want to keep the load step out of the property-driven path.

  • You need effects that TextRuntime does not expose — outline color, gradient fills, SDF, color fonts, or custom glyph subsets. Baked drop shadow is on the property path. See Advanced Font Effects.

  • You need a custom character set that doesn't match any BmfcSave.Ranges value you'd want to ship.

  • You need to override the rasterizer backend — for example forcing RasterizerBackend.StbTrueType on Blazor WASM where the native FreeType library isn't available.

Build-Time Font Cache

This approach is primarily useful when your project already has pre-generated font files from the Gum tool, or when dynamic font generation is not yet available for your runtime (Sokol and FNA today). For MonoGame, KNI, and Raylib, Dynamic KernSmith Generation is the recommended approach; for SkiaGum see Dynamic Generation on SkiaGum.

If UseCustomFont is false (the default) and no InMemoryFontCreator is registered, a TextRuntime's font is determined by its font component values. These values combine to produce a file name, and the corresponding .fnt file must already exist in a FontCache folder.

For the naming convention, generation rules, and full details, see Font Cache.

When to Use This Strategy

  • Pixel-perfect determinism: the atlas in source control is the atlas the player sees, every time.

  • Dynamic generation isn't available for your runtime (Sokol and FNA today).

  • You want zero runtime CPU cost for font generation (atlases are loaded from disk, not generated).

Missing Font Exceptions

By default TextRuntime instances do not throw exceptions for missing font files even if GraphicalUiElement.ThrowExceptionsForMissingFiles is set to CustomSetPropertyOnRenderable.ThrowExceptionsForMissingFiles. This is because a TextRuntime's font is decided by a combination of multiple properties.

If UseCustomFont is false, the font is determined by the combination of font values (size, style, etc.). If UseCustomFont is true, the font is determined by CustomFontFile.

Ultimately the variables which are used for fonts can be assigned in any order and from multiple spots (direct assignments, states, Gum projects). The TextRuntime doesn't know when variable assignment is finished. We can address this in a few ways:

The first is to explicitly load the desired BitmapFont as discussed above. Calling the BitmapFont constructor causes missing files to throw immediately.

Another option is to use the GraphicalUiElement.ThrowExceptionsForMissingFiles method to verify that a font is valid after a TextRuntime has been fully configured:

Last updated

Was this helpful?