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.
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.RGBA
Image// Get render information for a rune
(r rune) RuneMetrics
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 {
.Box2D[int] //glyph fits inside this
boundingBox math.Vector2[int]
adjust mathint
advance }
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 {
int
Size int
YAdvance }
We start with the implementation of a bitmap font as it is the easiest.
type BitmapFont struct {
*image.RGBA // image containing the glyphs of this font
image .Vector2[int] // width and height of each rune
runeDimensions math, columns int // how many glyphs per row and column in the image
rows
// characters in this bitmap image, the order of this array matches
// the order in the image (left to right, top to bot)
[]rune
runes }
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
:= BitmapFont{
bitmap : rows,
rows: columns,
columns: runes,
runes}
if bitmap.image, err = imageutil.RgbaFromFile(imageFile); err != nil {
return &bitmap, err
}
:= bitmap.image.Rect.Size()
imgSize .runeDimensions = bitmapRuneDimensions(math.Vector2[int]{imgSize.X, imgSize.Y}, rows, columns)
bitmapreturn &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]{
: imageDimensions.X / columns,
X: imageDimensions.Y / rows,
Y}
}
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 {
, _ := b.GetRuneBounds(r)
bbreturn RuneMetrics{
: bb,
boundingBox: b.runeDimensions.X,
advance}
}
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) {
:= math.Box2D[int]{}
bb := 0
index 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
:= index/b.columns + 1 // +1 is to move the origin to the bottom left instead to upper right
y := index % b.columns
x := b.runeDimensions
rd = bb.New(x*rd.X, y*rd.Y, (x+1)*rd.X, (y-1)*rd.Y)
bb
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.
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.RGBA // image containing the glyphs of this font
image []rune // runes(characters,symbols) stored in this font
runes []RuneMetrics // render information for each rune (matches order of runes)
runeMetrics
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):
, err := os.ReadFile(fontFile)
fontBytes, err := truetype.Parse(fontBytes)
font:= truetype.NewFace(font, &truetype.Options{
face : fontSize,
Size: font.HintingFull,
Hinting})
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) {
:= math.Vector2[int]{1024, 1024}
imageSize := &TrueTypeFont{
ttf : image.NewRGBA(image.Rect(0, 0, imageSize.X, imageSize.Y)),
image: runes,
runes: FontMetrics{Size: int(fontSize)},
fontMetrics}
, err := os.ReadFile(fontFile)
fontBytes, err := truetype.Parse(fontBytes)
f:= truetype.NewFace(f, &truetype.Options{
face : fontSize,
Size: font.HintingFull,
Hinting})
// drawing point
:= face.Metrics().Ascent.Ceil()
yAdvance .fontMetrics.YAdvance = yAdvance
ttf:= fixed.P(0, yAdvance)
dot
for _, r := range runes {
, mask, maskp, adv, ok := face.Glyph(dot, r)
rec.Draw(ttf.image, rec, mask, maskp, draw.Src)
draw
:= math.Box2D[int]{}.FromImageRect(rec)
bbox .MakeCanonical()
bbox:= fixedPointToVec(dot)
dotvec .runeMetrics = append(ttf.runeMetrics, RuneMetrics{
ttf: bbox,
boundingBox: adv.Ceil(),
advance: bbox.P3().Sub(dotvec),
adjust})
.X += adv
dotif dot.X+adv > fixed.I(imageSize.X) {
.X, dot.Y = 0, dot.Y+fixed.I(yAdvance)
dot}
}
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:
, mask, maskp, adv, ok := face.Glyph(dot, r)
rec.Draw(ttf.image, rec, mask, maskp, draw.Src) draw
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 {
:= 0
i for i = range t.runes {
if t.runes[i] == r {
break
}
}
if i == len(t.runeMetrics) {
return RuneMetrics{}
}
return t.runeMetrics[i]
}
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) {
.CropToFitIn(math.Box2D[int]{}.FromImageRect(target.Rect))
bounds:= font.FontMetrics().YAdvance
yAdvance := math.Vector2[int]{X: 0, Y: yAdvance}
startingMargin := bounds.P1.Add(startingMargin)
start .Println(start, startingMargin)
fmt:= start
dot
for _, r := range text {
:= font.RuneMetrics(r)
rm := rm.boundingBox
runeBB := dot.Add(rm.adjust)
adjustedDot if (adjustedDot.X+runeBB.Size().X) >= bounds.P2.X || r == '\n' {
.X = start.X
dot.Y += yAdvance
dot= dot.Add(rm.adjust)
adjustedDot }
if r == '\n' {
continue
}
if adjustedDot.Y > bounds.P2.Y {
return
}
:= math.Box2D[int]{}.New(adjustedDot.X, adjustedDot.Y,
destBB .X+runeBB.Size().X, adjustedDot.Y-runeBB.Size().Y)
adjustedDot.Draw(target, destBB.ToImageRect(), font.Image(), runeBB.ToImageRect().Min, draw.Src)
draw= dot.Add(math.Vector2[int]{X: rm.advance, Y: 0})
dot }
}
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.
:= "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ `abcdefghijklmnopqrstuvwxyz{|}~�"
charList , err := NewBitmapFont("data/bitmap.png", 6, 16, []rune(charList))
bitmapFont, err := NewTrueTypeFont("data/OpenSans-Regular.ttf", 38, CharacterSetASCII())
ttfFont
:= image.NewRGBA(image.Rect(0, 0, 500, 200))
img
:= math.Box2D[int]{}.New(0, 0, 500, 100)
box (img, box, "Bitmap is cool and retro... ", bitmapFont)
Render
= math.Box2D[int]{}.New(0, 100, 500, 200)
box (img, box, "But ttf is nice and smooth!", ttfFont) Render
It produces the following:
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) {
:= font.Runes()
runes := []math.Box2D[int]{}
boundingBoxes for _, r := range runes {
:= font.RuneMetrics(r)
metrics .BoundingBox.MakeCanonical()
metrics= append(boundingBoxes, metrics.BoundingBox)
boundingBoxes }
, 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 {
.Box2D[int]
bb math[]sprite.Sprite
sprites []math.Vector2[float32]
spritePositions .Font
font textstring
text *RuneToSpriteMap
runeSpriteMap *sprite.Atlas
atlas *shaders.Shader
shader int
renderOrder }
The Font
constructor simply stores the required
dependencies passed as parameters.
func NewText(text string, font text.Font, boundingBox math.Box2D[int], runeSpriteMap *RuneToSpriteMap,
*sprite.Atlas, shader *shaders.Shader, renderOrder int) *Text {
atlas := Text{
t : boundingBox,
bb: runeSpriteMap,
runeSpriteMap: atlas,
atlas: shader,
shader: renderOrder,
renderOrder: font,
font}
.SetText(text)
treturn &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
.sprites = []sprite.Sprite{}
t.spritePositions = []math.Vector2[float32]{}
t
:= t.font.FontMetrics().YAdvance
yAdvance := t.bb.P3().Sub(math.Vector2[int]{0, yAdvance})
dot for i, r := range []rune(text) {
:= t.runeSpriteMap.Get(r)
spriteId if spriteId < 0 {
continue
}
:= t.font.RuneMetrics(r)
metrics , _ := sprite.NewSprite(spriteId, t.atlas, t.shader, t.renderOrder)
spr.SetScale(math.Vector2ConvertType[int, float32](metrics.BoundingBox.Size()))
spr.sprites = append(t.sprites, spr)
t:= math.Vector2ConvertType[int, float32](dot)
spritePos := math.Vector2ConvertType[int, float32](metrics.Adjust)
fAdjust .spritePositions = append(t.spritePositions, spritePos.Add(fAdjust))
t= dot.Add(math.Vector2[int]{X: metrics.Advance, Y: 0})
dot if i < len(text)-1 {
:= text[i+1]
nextRune := t.font.RuneMetrics(rune(nextRune)).Advance
nextAdvance if dot.X+nextAdvance >= t.bb.Size().X {
.X = 0
dot.Y -= yAdvance
dot}
}
}
}
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 {
.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())
t}
}
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 {
[]rune
Runes []int
Sprites }
func NewRuneToSpriteMap(runes []rune, sprites []int) *RuneToSpriteMap {
:= RuneToSpriteMap{
m : make([]rune, len(runes)),
Runes: make([]int, len(sprites)),
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.
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
text game.Vector3[float32]
position math.GameObjectCommon
game}
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 {
:= "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ `abcdefghijklmnopqrstuvwxyz{|}~�"
charList , err = text.NewBitmapFont("data/bitmap.png", 6, 16, []rune(charList))
font, sprites, err := text.FontToAtlas(font, Game.Atlas)
runes= game.NewRuneToSpriteMap(runes, sprites)
runeToSpriteMap = true
fontLoaded }
:= FloatText{
ft : *game.NewText(t, font, math.NewBox2D(0, 0, size.X, size.Y),
text, Game.Atlas, &Game.Shader, 3),
runeToSpriteMap: position,
position}
.SetScale(math.Vector2[float32]{1, 1})
ftreturn &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) {
:= f.GetParent().GetTranslation()
parentPos := parentPos.Add(f.position)
pos .SetTranslation(pos)
f.text.Update(dt, f)
f}
func (f *FloatText) Render(r *sprite.Renderer) {
.text.Render(r)
f}
We also provide a pass-through method for setting updating the text.
func (f *FloatText) SetText(text string) {
.text.SetText(text)
f}
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 {
:= game.Scene{}
scene .AddGameObject(NewKnight(math.Vector2[float32]{100, 100}))
scene.AddGameObject(NewCoin(math.Vector2[float32]{190, 190}))
scene// New
= NewFloatText("Score: 0", math.Vector2[int]{300, 50}, math.Vector3[float32]{20, 460, 5})
ScoreText .AddGameObject(ScoreText)
scene//
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) {
//...
:= k.bbox.CheckForCollisions()
collisions for i := range collisions {
if game.HasTag(collisions[i], TagDebuff) {
.coins--
k} else {
.coins++
k}
.SetText(fmt.Sprint("Score:", k.coins))
ScoreText[i].Destroy()
collisions}
//...
}
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 {
//...
.textBox = NewFloatText("Lancy", math.Vector2[int]{200, 20}, math.Vector3[float32]{-60, 40, 5})
knight.AddChild(knight.textBox)
knight}
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.
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.