Drawing Text
Perhaps the easiest way to draw text is to use a bitmap font. Bitmap fonts are images where the characters are drawn in a regular grid.
Because every character has the same size it is easy to load the bitmap image into an atlas and use it to draw sprites for each character. Drawing text with bitmap fonts is also straightforward because the characters can just be placed next to each other using the distance they have on the bitmap image to guide placement. Below is some text rendered with a bitmap font.
Bitmap fonts go very well in retro looking games since they where heavily used in older games and they are often drawn in blocky, pixelated styles, like our example above, to emphasize this aesthetic. Nothing stops us from rendering high fidelity font characters in our bitmap image but the fact remains that characters have fixed, equidistant placement from each other so we can only render monospaced fonts with this approach.
In more modern text rendering, characters have variable spacing. This makes storing and rendering the font slightly more difficult. There are many font formats available, but perhaps the more ubiquitous is Truetype font (TTF). Truetype is a format in which fonts are stored as vectors. The specifics of the font implementation are not of much concern to us because we will be rendering the font to an atlas from which we will create sprites. What is important is the way the character sprites are placed. In TTF fonts, each character has a different bounding box and metrics that define how it is placed. For example, the letter Q in the following example sits partially under the baseline and we must account for that when rendering it.
By implementing TTF rendering our users can make use of the numerous TTF fonts available1. Below is text rendered using the Open Sans font.
Font Interface
We are supporting bitmap and TTF fonts and each will have their own
implementation. We will define a Font
interface that will
make it easy to use either font type interchangeably.
// Font is the common interface for bitmap and ttf fonts
type Font interface {
// Get the image representation of the font
Image() *image.RGBA
// Get render information for a rune
RuneMetrics(r rune) RuneMetrics
// Get font information
FontMetrics()
}
The Image
function returns the image onto which our font
is rendered. This will be used to turn the font image into an atlas. The
RuneMetrics
function returns information about a specific
rune.
type RuneMetrics struct {
boundingBox math.Box2D[int] //glyph fits inside this
adjust math.Vector2[int]
advance int
}
This information will help us when placing individual sprites for
each character and corresponds to the information shown in the TTF font
diagram above. The bounding box tells us the location of this character
in the font image. The adjust
variable holds the distance
from the origin to the bottom-left corner of the bounding box. We use it
to properly place the character when rendering so that it appears at the
correct position. The advance
variable tells us how much to
move the draw location after each character.
Finally, the FontMetrics
function returns information
about the whole font: the font size and the distance between lines:
type FontMetrics struct {
Size int
YAdvance int
}
Bitmap Font Implementation
We start with the implementation of a bitmap font as it is the easiest.
type BitmapFont struct {
image *image.RGBA // image containing the glyphs of this font
runeDimensions math.Vector2[int] // width and height of each rune
rows, columns int // how many glyphs per row and column in the image
// characters in this bitmap image, the order of this array matches
// the order in the image (left to right, top to bot)
runes []rune
}
BitmapFont
holds the bitmap image, and how many rows and
columns it has as well as information about the characters in the image.
In Go talk, characters in the data (UTF) sense are called runes and we
will be going with that as well. A glyph is the visual representation of
a rune. Notice that we don’t need information for each rune because all
of them are identical (this will not be the case for TTF fonts).
Finally, we store the runes for which we have glyphs. Go uses utf
encoding for which there are thousands of glyphs. Our bitmap image will
only hold a small subset (possibly the printable ASCII characters) so we
need to store the subset of characters that we have and we do that in
the runes
array. For convenience the characters in this
array match the order that the characters appear on the bitmap image.
For the example bitmap in the begining of the tutorial the characters
would be stored in this order:
!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ `abcdefghijklmnopqrstuvwxyz{|}~
To create a bitmap font we need the bitmap image, the number of rows and columns it has and the runes stored within. We expect the user to pass the runes with the correct order as explained above.
func NewBitmapFont(imageFile string, rows, columns int, runes []rune) (*BitmapFont, error) {
var err error
bitmap := BitmapFont{
rows: rows,
columns: columns,
runes: runes,
}
if bitmap.image, err = imageutil.RgbaFromFile(imageFile); err != nil {
return &bitmap, err
}
imgSize := bitmap.image.Rect.Size()
bitmap.runeDimensions = bitmapRuneDimensions(math.Vector2[int]{imgSize.X, imgSize.Y}, rows, columns)
return &bitmap, nil
}
Our constructor loads the image, fills in the row, column and rune information and then calculates the rune dimensions by dividing the image dimensions with the number of rows/columns.
func bitmapRuneDimensions(imageDimensions math.Vector2[int], rows, columns int) math.Vector2[int] {
return math.Vector2[int]{
X: imageDimensions.X / columns,
Y: imageDimensions.Y / rows,
}
}
This assumes that the bitmap image does not have any borders on the perimeter of the image or around the glyphs themselves.
For BitmapFont
to satisfy the Font
interface
we must provide implementations for Image
,
RuneMetrics
and FontMentrics
functions.
Image
is trivial, we just return the stored bitmap
image:
func (b *BitmapFont) Image() *image.RGBA {
return b.image
}
RuneMetrics
is also pretty simple since all runes have
the same metrics. The adjust
parameter is not initialized
so it becomes a zero vector. This means the glyphs are placed at the
origin of their bounding box which is what we want for bitmap fonts
where the placement is baked in the glyph itself.
func (b *BitmapFont) RuneMetrics(r rune) RuneMetrics {
bb, _ := b.GetRuneBounds(r)
return RuneMetrics{
boundingBox: bb,
advance: b.runeDimensions.X,
}
}
We do need to calculate the glyph bounding box which we do with the
GetRuneBounds
method. The method goes through the
runes
array and finds the index of the requested rune. We
assume the glyphs in the image are in the same order so we can use the
index to calculate the bounding box.
func (b *BitmapFont) GetRuneBounds(r rune) (math.Box2D[int], error) {
bb := math.Box2D[int]{}
index := 0
for index = range b.runes {
if b.runes[index] == r {
break
}
}
if index == len(b.runes) {
return math.Box2D[int]{}, errors.New("Rune not found in bitmap")
}
// this assumes the index matches the order in the image
y := index/b.columns + 1 // +1 is to move the origin to the bottom left instead to upper right
x := index % b.columns
rd := b.runeDimensions
bb = bb.New(x*rd.X, y*rd.Y, (x+1)*rd.X, (y-1)*rd.Y)
return bb, nil
}
Because all glyphs have the same dimensions and the number of rows and columns in the image is known, we can calculate the x and y location of the bounding box with the modulo and division operators. Take rune ‘F’ as an example. Its index in the runes array is 37 and its position in the bitmap image is row 2 and column 5 (zero indexed). The integer division operation tells us the row for a specific index and the modulo operator gives us the column. The image origin is at the top left corner so we add one to the y index to move it to the bottom right so that it matches the convention used in the rest of AGL code.
TrueType Font Implementation
Similarly to bitmap, TrueTypeFont
holds an image onto
which we have rendered glyphs. We also store the runes for which we have
glyphs in an array (runes
) and global font metrics
(fontMetrics
). Unlike bitmap, for TTF we want per-rune
information about our glyphs and we store that in
runeMetrics
.
type TrueTypeFont struct {
image *image.RGBA // image containing the glyphs of this font
runes []rune // runes(characters,symbols) stored in this font
runeMetrics []RuneMetrics // render information for each rune (matches order of runes)
fontMetrics FontMetrics
}
Building a TTF font requires that we load a font file, typically
ending in .ttf
, using the freetype
library.
This library lets us load the font and renders images of individual
runes. This is done with the following code (error checking is
omitted):
fontBytes, err := os.ReadFile(fontFile)
font, err := truetype.Parse(fontBytes)
face := truetype.NewFace(font, &truetype.Options{
Size: fontSize,
Hinting: font.HintingFull,
})
The font file is read from storage and then parsed using the truetype
library. This creates a font out of which we can create a font face. A
font face is a font rendered at a specific size. So
Open Sans 32
is face of the Open Sans
font.
Our TTF constructor builds a font face of a specific size (given by the
user) and renders the glyphs that the user requests into an image.
func NewTrueTypeFont(fontFile string, fontSize float64, runes []rune) (*TrueTypeFont, error) {
imageSize := math.Vector2[int]{1024, 1024}
ttf := &TrueTypeFont{
image: image.NewRGBA(image.Rect(0, 0, imageSize.X, imageSize.Y)),
runes: runes,
fontMetrics: FontMetrics{Size: int(fontSize)},
}
fontBytes, err := os.ReadFile(fontFile)
f, err := truetype.Parse(fontBytes)
face := truetype.NewFace(f, &truetype.Options{
Size: fontSize,
Hinting: font.HintingFull,
})
// drawing point
yAdvance := face.Metrics().Ascent.Ceil()
ttf.fontMetrics.YAdvance = yAdvance
dot := fixed.P(0, yAdvance)
for _, r := range runes {
rec, mask, maskp, adv, ok := face.Glyph(dot, r)
draw.Draw(ttf.image, rec, mask, maskp, draw.Src)
bbox := math.Box2D[int]{}.FromImageRect(rec)
bbox.MakeCanonical()
dotvec := fixedPointToVec(dot)
ttf.runeMetrics = append(ttf.runeMetrics, RuneMetrics{
boundingBox: bbox,
advance: adv.Ceil(),
adjust: bbox.P3().Sub(dotvec),
})
dot.X += adv
if dot.X+adv > fixed.I(imageSize.X) {
dot.X, dot.Y = 0, dot.Y+fixed.I(yAdvance)
}
}
return ttf, nil
}
In the above, the font struct is initialized2 and the font is loaded as described above. We then loop over the runes given by the user and render them onto the image. This snippet gets the pixel info for a rune and copies (renders) it to the image:
rec, mask, maskp, adv, ok := face.Glyph(dot, r)
draw.Draw(ttf.image, rec, mask, maskp, draw.Src)
The rune is rendered at the location given by dot
which
is initially set to the top-left of the image. After we render a rune
the dot is moved to the right by adv
pixels which is the
rune’s Advance width
metric. If the dot goes over the image
width we reset it’s X position to zero and move it one line below.
While rendering, we also store per-rune info. The runes bounding box
is conveniently provided by the face.Glyph
method (it
combines the glyph’s internal bounding box with dot
).
Advance also comes directly from face.Glyph as it is a metric used in
the font itself. For the adjust
parameter we subtract the
dot
and the corner of the bounding box. This creates the
vector in red seen below and is enough to let us properly place the
glyph when we render.
With the constructor done the rest of the implementation is
straightforward. The methods needed to implement Font
just
return the info we saved in the constructor.
func (t *TrueTypeFont) Image() *image.RGBA {
return t.image
}
func (t *TrueTypeFont) FontMetrics() FontMetrics {
return t.fontMetrics
}
func (t *TrueTypeFont) RuneMetrics(r rune) RuneMetrics {
i := 0
for i = range t.runes {
if t.runes[i] == r {
break
}
}
if i == len(t.runeMetrics) {
return RuneMetrics{}
}
return t.runeMetrics[i]
}
Rendering
We will provide a method to render text on an image. This is not particularly useful for games as we usually want our text to be sprites that we can move around but it is useful in some cases, for example when we want a big chunk of text, like a letter, to be rendered into a single sprite. It also serves as a nice way to test what we have built.
Our render function accepts an image, a Font
and a
string with the text that we want rendered. We must also provide a
bounding box to restrict the text to be rendered whithin a specific
region of the image.
func Render(target *image.RGBA, bounds math.Box2D[int], text string, font Font) {
bounds.CropToFitIn(math.Box2D[int]{}.FromImageRect(target.Rect))
yAdvance := font.FontMetrics().YAdvance
startingMargin := math.Vector2[int]{X: 0, Y: yAdvance}
start := bounds.P1.Add(startingMargin)
fmt.Println(start, startingMargin)
dot := start
for _, r := range text {
rm := font.RuneMetrics(r)
runeBB := rm.boundingBox
adjustedDot := dot.Add(rm.adjust)
if (adjustedDot.X+runeBB.Size().X) >= bounds.P2.X || r == '\n' {
dot.X = start.X
dot.Y += yAdvance
adjustedDot = dot.Add(rm.adjust)
}
if r == '\n' {
continue
}
if adjustedDot.Y > bounds.P2.Y {
return
}
destBB := math.Box2D[int]{}.New(adjustedDot.X, adjustedDot.Y,
adjustedDot.X+runeBB.Size().X, adjustedDot.Y-runeBB.Size().Y)
draw.Draw(target, destBB.ToImageRect(), font.Image(), runeBB.ToImageRect().Min, draw.Src)
dot = dot.Add(math.Vector2[int]{X: rm.advance, Y: 0})
}
}
Rendering the text is similar to how we created the TTF font. We
initialize a dot position at the top-left of our bounding box. We then
iterate rune-by-rune through the provided text. For each rune we grab
it’s bounding box using the Font.RuneMetrics
method. We
figure out the render location by adding the rune’s bounding box, the
current draw location (dot
) and the rune’s adjust vector.
We then copy the pixel values from this location to our target image
using the draw.Draw
method. If we go over the right edge of
our bounding box we reset to the left and one line below. We do the same
if a newline is encountered.
Using Render
we can now draw text using both bitmap and
TTF fonts. In this example we draw two pieces of text on the same image
using different fonts.
charList := "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ `abcdefghijklmnopqrstuvwxyz{|}~�"
bitmapFont, err := NewBitmapFont("data/bitmap.png", 6, 16, []rune(charList))
ttfFont, err := NewTrueTypeFont("data/OpenSans-Regular.ttf", 38, CharacterSetASCII())
img := image.NewRGBA(image.Rect(0, 0, 500, 200))
box := math.Box2D[int]{}.New(0, 0, 500, 100)
Render(img, box, "Bitmap is cool and retro... ", bitmapFont)
box = math.Box2D[int]{}.New(0, 100, 500, 200)
Render(img, box, "But ttf is nice and smooth!", ttfFont)
It produces the following:
Drawing Text using Sprites
To draw text in-game we will create sprites for character glyphs and
draw them like we do with any sprite. To do this we must first load our
font image into an atlas. We do this using the FontToAtlas
utility function:
func FontToAtlas(font Font, atlas *sprite.Atlas) ([]rune, []int, error) {
runes := font.Runes()
boundingBoxes := []math.Box2D[int]{}
for _, r := range runes {
metrics := font.RuneMetrics(r)
metrics.BoundingBox.MakeCanonical()
boundingBoxes = append(boundingBoxes, metrics.BoundingBox)
}
_, ids, err := atlas.AddAtlasImage(font.Image(), boundingBoxes)
if err != nil {
return nil, nil, err
}
return runes, ids, nil
}
The function goes over every rune in the font and gets its bounding
box. Then the font image and the list of bounding boxes are added to the
atlas. This creates sprite ids
for every rune. We return
the list of runes and the list of sprite ids whose indices match. So if
runes is a b c
and ids is 12 13 14
, creating a
sprite with id=12 would draw ‘a’.
We don’t want to burden our users with having to place individual
character sprites in order to draw text so we provide a character
drawing component called Text
which lives in the agl/game
package. Text
stores a text.Font
and draws a
text string by creating individual sprites for every rune in the string.
A bounding box lets us constrain the text into a specific region.
type Text struct {
bb math.Box2D[int]
sprites []sprite.Sprite
spritePositions []math.Vector2[float32]
font text.Font
text string
runeSpriteMap *RuneToSpriteMap
atlas *sprite.Atlas
shader *shaders.Shader
renderOrder int
}
The Font
constructor simply stores the required
dependencies passed as parameters.
func NewText(text string, font text.Font, boundingBox math.Box2D[int], runeSpriteMap *RuneToSpriteMap,
atlas *sprite.Atlas, shader *shaders.Shader, renderOrder int) *Text {
t := Text{
bb: boundingBox,
runeSpriteMap: runeSpriteMap,
atlas: atlas,
shader: shader,
renderOrder: renderOrder,
font: font,
}
t.SetText(text)
return &t
}
The main method of interest is SetText
which creates and
arranges the sprites used to show text.
func (t *Text) SetText(text string) {
// clear existing
t.sprites = []sprite.Sprite{}
t.spritePositions = []math.Vector2[float32]{}
yAdvance := t.font.FontMetrics().YAdvance
dot := t.bb.P3().Sub(math.Vector2[int]{0, yAdvance})
for i, r := range []rune(text) {
spriteId := t.runeSpriteMap.Get(r)
if spriteId < 0 {
continue
}
metrics := t.font.RuneMetrics(r)
spr, _ := sprite.NewSprite(spriteId, t.atlas, t.shader, t.renderOrder)
spr.SetScale(math.Vector2ConvertType[int, float32](metrics.BoundingBox.Size()))
t.sprites = append(t.sprites, spr)
spritePos := math.Vector2ConvertType[int, float32](dot)
fAdjust := math.Vector2ConvertType[int, float32](metrics.Adjust)
t.spritePositions = append(t.spritePositions, spritePos.Add(fAdjust))
dot = dot.Add(math.Vector2[int]{X: metrics.Advance, Y: 0})
if i < len(text)-1 {
nextRune := text[i+1]
nextAdvance := t.font.RuneMetrics(rune(nextRune)).Advance
if dot.X+nextAdvance >= t.bb.Size().X {
dot.X = 0
dot.Y -= yAdvance
}
}
}
}
SetText
is similar in concept with the render function
we saw earlier. It loops through the text and creates sprites for every
rune. Like render, it uses a dot
variable to keep track of
the current location in which to place a sprite. The dot is moved from
left to right by each rune’s Advance
metric. When the end
of the line is reached, we reset to the left and move one line below.
Each sprite is placed at the dot location after being adjusted by
RuneMetrics.Adjust
. The sprite’s scale is set to match the
size on the atlas.
Notice that we don’t set the sprite position. Instead, we save it in
a separate array, spritePositions
. This is because the
positions calculated here are relative to the origin of the
Text
bounding box. Text
is a component that
must be placed inside a game object. The final position of rendered text
is given by the game object position and spritePositions
.
This is done in Update
.
func (t *Text) Update(dt time.Duration, parent GameObject) {
for i := range t.sprites {
t.sprites[i].SetPosition(parent.GetTranslation().Add(t.spritePositions[i].AddZ(0)))
t.sprites[i].SetScale(parent.GetScale().Mul(t.sprites[i].GetScale()))
t.sprites[i].SetRotation(parent.GetRotation())
}
}
By keeping relative sprite positions in their own array we can move
the text without having to loop over the whole text every time. If the
parent moves the text moves with it. The costly SetText
loop only needs to be called if the text changes.
In the above code we get the sprite associated with a rune
r
using runeSpriteMap.Get(r)
. This is a
convenience type that holds the rune and sprite arrays given by
FontToAtlas
. It’s Get
method searches the
Runes
array and returns the same index in the
Sprites
array, with the assumption that two arrays are
properly matched (FontToAtlas
arrays will be).
// Holds runes and their corresponding sprite ids in two arrays whose indexes match.
type RuneToSpriteMap struct {
Runes []rune
Sprites []int
}
func NewRuneToSpriteMap(runes []rune, sprites []int) *RuneToSpriteMap {
m := RuneToSpriteMap{
Runes: make([]rune, len(runes)),
Sprites: make([]int, len(sprites)),
}
copy(m.Runes, runes)
copy(m.Sprites, sprites)
return &m
}
// Get sprite for this rune
func (rm *RuneToSpriteMap) Get(r rune) int {
for i := range rm.Runes {
if rm.Runes[i] == r {
return rm.Sprites[i]
}
}
return -1
}
The only thing of interest here is the Get
function
which can potentially be optimized to return in one operation instead of
looping if we know beforehand that our runes are arranged in a specific
way. For example, if we know that the Runes
array holds the
lowercase characters “abcdef…” we can modify Get
like
this:
func (rm *RuneToSpriteMap) SpecialGet(r rune) int {
return rm.Sprites[r-int('a')]
}
This type of optimization is not currently implemented.
Adding Text to Knight vs Trolls
In the latest iteration of Knight vs Trolls our knight must collect coins that spawn on the ground and avoid sculls that deduct from their score. So far the score was printed in the console so lets make is show up in-game.
We will first create a game object to hold our text component.
type FloatText struct {
text game.Text
position math.Vector3[float32]
game.GameObjectCommon
}
It’s constructor takes care of loading a font, if one has not been loaded already.
var fontLoaded bool
var runeToSpriteMap *game.RuneToSpriteMap
var font text.Font
func NewFloatText(t string, size math.Vector2[int], position math.Vector3[float32]) *FloatText {
if !fontLoaded {
charList := "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ `abcdefghijklmnopqrstuvwxyz{|}~�"
font, err = text.NewBitmapFont("data/bitmap.png", 6, 16, []rune(charList))
runes, sprites, err := text.FontToAtlas(font, Game.Atlas)
runeToSpriteMap = game.NewRuneToSpriteMap(runes, sprites)
fontLoaded = true
}
ft := FloatText{
text: *game.NewText(t, font, math.NewBox2D(0, 0, size.X, size.Y),
runeToSpriteMap, Game.Atlas, &Game.Shader, 3),
position: position,
}
ft.SetScale(math.Vector2[float32]{1, 1})
return &ft
}
The rest of the object is very simple. It’s Update
sets
the transform and calls Text.Update
and Render
simply calls Text.Render
.
func (f *FloatText) Update(dt time.Duration) {
parentPos := f.GetParent().GetTranslation()
pos := parentPos.Add(f.position)
f.SetTranslation(pos)
f.text.Update(dt, f)
}
func (f *FloatText) Render(r *sprite.Renderer) {
f.text.Render(r)
}
We also provide a pass-through method for setting updating the text.
func (f *FloatText) SetText(text string) {
f.text.SetText(text)
}
To render text we add a FloatText
object to our scene.
We give it a wide bounding box and set its position near the top of the
screen. We make this object a global so that the knight can access it
easily.
var ScoreText *FloatText
func NewLevel() *game.Scene {
scene := game.Scene{}
scene.AddGameObject(NewKnight(math.Vector2[float32]{100, 100}))
scene.AddGameObject(NewCoin(math.Vector2[float32]{190, 190}))
// New
ScoreText = NewFloatText("Score: 0", math.Vector2[int]{300, 50}, math.Vector3[float32]{20, 460, 5})
scene.AddGameObject(ScoreText)
//
return &scene
}
In our knight’s update function, whenever we collide with a coin or
scull we update the score. At the same time we update the
ScoreText
object.
func (k *Knight) Update(dt time.Duration) {
//...
collisions := k.bbox.CheckForCollisions()
for i := range collisions {
if game.HasTag(collisions[i], TagDebuff) {
k.coins--
} else {
k.coins++
}
ScoreText.SetText(fmt.Sprint("Score:", k.coins))
collisions[i].Destroy()
}
//...
}
FloatText
can also be added as a child of another game
object. We can, for example, add a name tag to our player:
func NewKnight(position math.Vector2[float32]) *Knight {
//...
knight.textBox = NewFloatText("Lancy", math.Vector2[int]{200, 20}, math.Vector3[float32]{-60, 40, 5})
knight.AddChild(knight.textBox)
}
A good task for the reader would be to make a +1
appear
on the knight’s head every time the grab a coin. It should move up a bit
and disappear after half a second. With sculls, it should show a
-1
going down for half a second.
Closing Remarks
In this tutorial we created a versatile method for drawing text that
can utilize bitmap and TTF fonts and showed how we can render text
sprites in our games. One limitation is that the sprite placement
method, SetText
, is very basic. It renders text
left-to-right with no support for standard text placement options such
as alignment (e.g centered text) and doesn’t deal with whitespace
(tabs/newlines). For a text heavy game or for drawing complex UIs this
would be a limitation and we may add this functionality in the
future.
See for example https://fonts.google.com/.↩︎
We initialize the font image to a fixed size for simplicity but the size can be better estimated from the number of runes passed.↩︎