Skip to content

Text and fonts

Drawing and measuring text, bitmap and MSDF fonts, and localization.

Files: text_primitives.odin, fonts.odin, font_compile.odin (#+build !js), localization.odin.

Fonts besides TrueType: bitmap fonts (BMFont) and MSDF fonts. Every text the engine draws goes through DrawText / MeasureText with a font’s path, so Labels, Menus, Buttons and RichText use them with no change.

  • Bitmap fonts: a .fnt file (AngelCode BMFont, text or XML, the format Hiero, BMFont, Littera and Snow’s tools write) and its page images beside it. Each glyph is a picture: right for pixel-art lettering, drawn at whole multiples of its size (size scales it; the font’s own size draws it as made).
  • MSDF fonts: a TrueType font whose asset says msdf="true" (asset_import.odin) gets an MSDF atlas made by the editor (msdf-atlas-gen: <font>.msdf.bmp and <font>.msdf.json beside it). On the GPU renderer, text in it is drawn from the atlas through an MSDF shader (shaders/text/msdf.frag): sharp at any size and zoom, and turned with its node. Elsewhere (no GPU renderer, the web) the font draws as a TrueType font, the same letters.

Both are laid out by the engine (fontLayout): lines broken at newlines and wrapped at spaces, kerning pairs, the line height and ascent the font gives. A glyph the font lacks draws as nothing (an MSDF font’s atlas has the characters it was made with: printable ASCII and Latin-1 by default).

Making MSDF fonts’ atlases (fonts.odin) with msdf-atlas-gen, one of the shader tools (tools/build_shader_tools.sh builds it into shader_tools/<os>_<arch>/): a TrueType or OpenType font becomes <font>.msdf.bmp (three distance channels) and <font>.msdf.json (where each glyph is, its metrics, the kerning) beside it. Editors call it when a font asset’s MSDF setting is on, and again when the font file changes; exports ship both files.

Localization: a game’s text in several languages, from CSV tables like Godot’s.

A table’s first row names the languages (the first column, the keys, is named anything); each row after it is a key and its text in each language. Rows with an empty key or one starting with # are comments. Quotes allow commas and line breaks (standard CSV):

key,en,fr,de
menu.play,Play,Jouer,Spielen
menu.quit,Quit,Quitter,Beenden
# The HUD
hud.coins,"Coins: %d","Pièces : %d","Münzen: %d"

A project’s tables are its assets of kind “Translation” (&lt;asset path="i18n/strings.csv" kind="Translation"/>, format 13), loaded in their order, and it may pick the language (locale="fr"); without it the player’s system language is used when a table has it, else the first column’s. Tr(key) gives a key’s text in the current language (falling back to the first column’s, then the key itself), and Labels and Menu items whose text starts with @ are keys (“@menu.play”), translated as they draw, so switching the language (Locale_Set, from an options menu) changes them at once. “@@” starts text with a literal “@”.

Example

// Given: coinCount: int, item: ^e.MenuItem
coins := fmt.tprintf(e.Tr("hud.coins"), coinCount)
_ = coins
// An options menu's language select
e.Locale_Set(game, e.Locale_List(game)[item.selectedOption])
GlyphFont :: struct {
msdf: bool,
// Pixels per em the glyphs were made at (sizes scale from it)
size: f32,
// At that size: from one line to the next, and from a line's top to its baseline
lineHeight, ascent: f32,
glyphs: map[rune]FontGlyph,
kerning: map[[2]rune]f32,
// The pictures, by page
pages: [dynamic]^sdl.Texture,
// MSDF: the atlas's distance range in its pixels, and the shader drawing it
pxRange: f32,
material: ^ShaderMaterial,
}

A font drawn from pictures of its glyphs: a bitmap font, or an MSDF font’s atlas

struct. Source: fonts.odin:41

FontGlyph :: struct {
page: int,
// Its picture in the page, in pixels
src: sdl.FRect,
// Where the picture goes from the pen (x) and the line's top (y), and its size
offset, size: [2]f32,
// How far the pen moves after it
advance: f32,
}

One glyph of a GlyphFont, at the font’s size

struct. Source: fonts.odin:57

BMFontData :: struct {
// `info size` (pixels per em; positive), `common lineHeight`, `base`
size, lineHeight, base: f32,
// The page images, relative to the .fnt file
pages: [dynamic]string,
// The `char` lines
chars: [dynamic]BMFontChar,
// The `kerning` lines
kernings: [dynamic]BMFontKerning,
}

A BMFont file’s contents (ParseBMFont)

struct. Source: fonts.odin:68

BMFontChar :: struct {
// The character
id: rune,
// The picture's place and size on its page; where it's drawn from the pen (x) and the
// line's top (y); how far the pen moves after it (pixels at the font's size)
x, y, width, height, xoffset, yoffset, xadvance: f32,
// The page image it's on (an index into `BMFontData.pages`)
page: int,
}

A BMFont file’s char line: one glyph and where its picture is

struct. Source: fonts.odin:80

BMFontKerning :: struct {
// The character on the left, and the one after it
first, second: rune,
// What's added to the pen's move between them (pixels; negative is closer)
amount: f32,
}

A BMFont file’s kerning line: a pair of characters drawn closer or further apart

struct. Source: fonts.odin:91

Localization :: struct {
// Languages, in the order the tables named them (the first is the fallback)
locales: [dynamic]string,
// Each key's text, one per language ("" where a table has none); shorter when a later table
// added languages
texts: map[string][]string,
// The language in use: an index into `locales`
current: int,
}

A game’s translation tables, merged (Game.localization; in the game’s arena)

struct. Source: localization.odin:42

DrawText :: proc(rm: ^ResourceManager, pos: sdl.FPoint, text: string, font: string, size: f32, color: sdl.Color)

Draws a line of text (newlines break it), its top-left corner at pos.

Inputs

  • rm: The resource manager the font is loaded through (&game.resourceManager)
  • pos: The text’s top-left corner, in local coordinates
  • text: The text (UTF-8)
  • font: A font file path, or DEFAULT_FONT
  • size: The point size (scaled by the draw transform, rounded to whole points)
  • color: The color (multiplied by the node’s tint)

Example

e.DrawText(&game.resourceManager, {16, 16}, "Score: 100", e.DEFAULT_FONT, 24, {255, 255, 255, 255})

procedure. Source: text_primitives.odin:29

DrawTextWrapped :: proc(rm: ^ResourceManager, pos: sdl.FPoint, text: string, font: string, size: f32, color: sdl.Color, wrapWidth: f32)

Draws text wrapped to fit a width, its top-left corner at pos.

Inputs

  • rm: The resource manager the font is loaded through (&game.resourceManager)
  • pos: The text’s top-left corner, in local coordinates
  • text: The text (UTF-8)
  • font: A font file path, or DEFAULT_FONT
  • size: The point size (scaled by the draw transform, rounded to whole points)
  • color: The color (multiplied by the node’s tint)
  • wrapWidth: The width to wrap lines at, in local units. 0 only breaks on newlines

Example

e.DrawTextWrapped(&game.resourceManager, {16, 16}, "A long line of dialogue...", e.DEFAULT_FONT, 18, {255, 255, 255, 255}, 300)

procedure. Source: text_primitives.odin:48

DrawTextCentered :: proc(rm: ^ResourceManager, pos: sdl.FPoint, text: string, font: string, size: f32, color: sdl.Color)

Draws a line of text centered on pos, for titles and buttons.

Inputs

  • rm: The resource manager the font is loaded through (&game.resourceManager)
  • pos: The text’s center, in local coordinates
  • text: The text (UTF-8)
  • font: A font file path, or DEFAULT_FONT
  • size: The point size
  • color: The color (multiplied by the node’s tint)

Example

e.DrawTextCentered(&game.resourceManager, sdl.FPoint(game.size * 0.5), "PAUSED", e.DEFAULT_FONT, 48, {255, 255, 255, 255})

procedure. Source: text_primitives.odin:99

MeasureText :: proc(rm: ^ResourceManager, text: string, font: string, size: f32, wrapWidth: f32 = 0) -> sdl.FPoint

Measures the text without drawing it.

Inputs

  • rm: The resource manager the font is loaded through (&game.resourceManager)
  • text: The text (UTF-8)
  • font: A font file path, or DEFAULT_FONT
  • size: The point size
  • wrapWidth: The width to wrap lines at, in pixels. 0 only breaks on newlines

Returns

  • The width and height, in pixels at that size (no draw transform applied); {0, 0} for empty text or a font that can’t be loaded

procedure. Source: text_primitives.odin:118

Font_IsBitmap :: proc(path: string) -> bool

Whether a font path is a bitmap font

procedure. Source: fonts.odin:99

ParseBMFont :: proc(text: string, allocator := context.allocator) -> (data: BMFontData, ok: bool)

Reads a BMFont file, text or XML (binary .fnt files aren’t read: export text or XML).

Inputs

  • text: The file’s contents
  • allocator: For the lists and page names

Returns

  • The font’s data (free with BMFontData_Destroy), and false when it isn’t a BMFont file

procedure. Source: fonts.odin:113

BMFontData_Destroy :: proc(data: ^BMFontData, allocator := context.allocator)

Frees what ParseBMFont made

procedure. Source: fonts.odin:188

ResourceManager_SetFontMSDF :: proc(rm: ^ResourceManager, path: string, on: bool)

Turns a font’s MSDF atlas on or off (asset_import.odin: Game_LoadProject does it for the project’s font assets with msdf="true"). The atlas files are read the first time text in the font draws on the GPU renderer.

Inputs

  • rm: The resource manager
  • path: The font, as text is drawn with it (the project folder joined)
  • on: Whether it has an atlas

procedure. Source: fonts.odin:206

ResourceManager_ForgetGlyphFont :: proc(rm: ^ResourceManager, path: string)

Forgets a font’s glyph pictures, so the next text reads its files again (an editor made a new MSDF atlas, or saved a bitmap font).

procedure. Source: fonts.odin:225

Font_MakeMSDF :: proc(tools: ShaderTools, font, scratch: string, charset := MSDF_DEFAULT_CHARSET, size := MSDF_DEFAULT_SIZE, force := false) -> (messages: string, ok: bool, made: bool)

Makes a font’s MSDF atlas beside it, when the font is newer than its atlas (or force).

Inputs

  • tools: The shader tools (ShaderTools_Find), with msdf-atlas-gen
  • font: The .ttf / .otf file
  • scratch: A folder for the charset file (made when missing)
  • charset: msdf-atlas-gen’s charset syntax: ranges [32, 126], characters 'A', strings "abc"
  • size: Pixels per em in the atlas
  • force: Make it even when it’s up to date

Returns

  • The tool’s messages when it failed, whether it worked, and whether it made the atlas (false: up to date)

procedure, only in builds with #+build !js. Source: font_compile.odin:38

Locale_LoadCSV :: proc(game: ^Game, text: string, source := "translations") -> bool

Reads a translation table (CSV, as above) into the game’s, adding its keys and languages to those already loaded (a key loaded again takes the new text).

Inputs

  • game: The game
  • text: The CSV
  • source: Its name, for messages

Returns

  • false (logged) when the table can’t be parsed

Example

e.Locale_LoadCSV(game, "key,en,fr\nmenu.play,Play,Jouer\n", "builtin")

procedure. Source: localization.odin:67

Locale_LoadFile :: proc(game: ^Game, path: string) -> bool

Reads a translation table from a file (the data pack first: platform layer), as Locale_LoadCSV.

Inputs

  • game: The game
  • path: The file

Returns

  • false (logged) when it can’t be read or parsed

Example

e.Locale_LoadFile(game, "i18n/extra.csv")

procedure. Source: localization.odin:135

Locale_Set :: proc(game: ^Game, locale: string) -> bool

Switches the language: an exact name (“pt_BR”), else one of the same language (“fr” for “fr_CA”, or “fr_FR” for “fr”).

Inputs

  • game: The game (nil: the one playing)
  • locale: The language’s name, as the tables’ first row has it

Returns

  • false when no table has it (the language stays)

Example

if !e.Locale_Set(game, "fr") do log.warn("no French text")

procedure. Source: localization.odin:159

Locale_Get :: proc(game: ^Game = nil) -> string

The language in use.

Inputs

  • game: The game (nil: the one playing)

Returns

  • Its name as the tables’ first row has it (“” when no table is loaded); the game’s memory

procedure. Source: localization.odin:184

Locale_List :: proc(game: ^Game = nil) -> []string

The languages the tables have, in their order (the first is the fallback).

Inputs

  • game: The game (nil: the one playing)

Returns

  • The names (the game’s: don’t keep the slice past it, or past loading another table); nil without a game

procedure. Source: localization.odin:199

Locale_System :: proc() -> string

The player’s preferred language, as SDL reports it (“fr_CA”, or “fr”), temp allocated; “” when unknown.

procedure. Source: localization.odin:209

Tr :: proc(key: string, game: ^Game = nil) -> string

A key’s text in the current language; the first language’s when it has none there, and the key itself when no table has it (so missing text shows up as its key).

Inputs

  • key: The key, as the tables’ first column has it
  • game: The game (nil: the one playing)

Returns

  • The text (the game’s memory: valid while it runs)

Example

e.CreateLabel(scene, e.Tr("title.press_start"), {640, 500}, 24, {255, 255, 255, 255})

procedure. Source: localization.odin:233

Tr_Text :: proc(text: string, game: ^Game = nil) -> string

Text as Labels and Menu items show it: “@key” is Tr(key), “@@…” is the text from its second character (a literal “@”), anything else is itself.

Inputs

  • text: The text
  • game: The game (nil: the one playing)

Returns

  • The text to show (a translation, or part of text: nothing is allocated)

procedure. Source: localization.odin:255

BMFONT_EXTENSION :: ".fnt"

A bitmap font file’s extension

constant. Source: fonts.odin:34

MSDF_ATLAS_SUFFIX :: ".msdf.json"

What an MSDF font’s atlas files end with, after the font’s own name (“Font.ttf.msdf.json”)

constant. Source: fonts.odin:36

MSDF_IMAGE_SUFFIX :: ".msdf.bmp"

What an MSDF font’s atlas picture ends with, after the font’s own name (“Font.ttf.msdf.bmp”)

constant. Source: fonts.odin:38

MSDF_DEFAULT_CHARSET :: "[32, 126], [160, 255]"

The characters an MSDF atlas holds by default: printable ASCII and Latin-1

constant, only in builds with #+build !js. Source: font_compile.odin:17

MSDF_DEFAULT_SIZE :: 48

Pixels per em the atlas’s glyphs are made at (bigger: finer corners, a bigger picture)

constant, only in builds with #+build !js. Source: font_compile.odin:19

MSDF_DEFAULT_RANGE :: 4

The distance range in the atlas’s pixels (the shader’s pxRange)

constant, only in builds with #+build !js. Source: font_compile.odin:21

TRANSLATION_ASSET_KIND :: "Translation"

The asset kind of translation tables in project.xml

constant. Source: localization.odin:39