491 lines
18 KiB
Text
491 lines
18 KiB
Text
== Lesson 19: Text That Looks Good
|
|
|
|
image::learn/19-text.png[The finished lesson, 480]
|
|
|
|
Every word you have put on screen since lesson one came out of `overlayPrint`,
|
|
and `overlayPrint` has exactly one font: a fixed-width block of pixels built
|
|
into the engine, the same size forever, in whatever colour it happens to be.
|
|
It is perfect for a frame counter. It is wrong for a title, a score, a menu, or
|
|
anything a player is meant to look at rather than read past.
|
|
|
|
This lesson loads a real font off disk, prints with it, colours it, measures it
|
|
so you can centre things, and shows you the one performance trap that catches
|
|
everybody. At the end you will have a title screen that looks deliberate
|
|
instead of accidental.
|
|
|
|
=== Set the Overlay to a Size You Can Think In
|
|
|
|
Start a new file called `text.singe` with two lines:
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
----
|
|
|
|
The first line you have written many times; it loads the framework, which
|
|
defines `DIR`, the `SWITCH_*` names, and the `FONT_QUALITY_*` names you are
|
|
about to use.
|
|
|
|
The second matters more than it looks. The overlay -- the surface everything
|
|
you draw lands on -- starts at *half* the size of the world. A game with no
|
|
video has a 720 by 480 canvas, so the overlay starts at 360 by 240, and a
|
|
36 point font on a 360 pixel wide overlay is enormous. `overlaySetResolution`
|
|
replaces the overlay with one of the size you ask for, and from then on every
|
|
coordinate you give, and everything `overlayGetWidth` and `overlayGetHeight`
|
|
tell you, is in that space.
|
|
|
|
`discGetWidth()` and `discGetHeight()` answer the size of the video when there
|
|
is one and the size of the canvas when there is not, so that one line gives you
|
|
an overlay that matches the world whichever kind of game this is. Call it once,
|
|
at the start, before you draw anything.
|
|
|
|
=== Load a Font
|
|
|
|
Under that, add:
|
|
|
|
[source,lua]
|
|
----
|
|
fontQuality(FONT_QUALITY_BLENDED)
|
|
|
|
local titleFont = fontLoad("Singe/FreeSansBold.ttf", 48)
|
|
----
|
|
|
|
`fontLoad` opens a TrueType font file and hands you back a number -- a *handle*
|
|
-- that you use whenever you want that font. The second argument is the point
|
|
size, and it is fixed for the life of the handle. There is no way to change the
|
|
size of a loaded font afterwards. If you want the same face at three sizes, you
|
|
call `fontLoad` three times and keep three handles.
|
|
|
|
`Singe/FreeSansBold.ttf` is the font the engine unpacked into the `Singe`
|
|
folder in your work folder the first time you ran anything. Every copy of Singe
|
|
has it, so you can rely on it. Any other `.ttf` file works the same way; put it
|
|
beside your script and load it as `DIR .. "myfont.ttf"`.
|
|
|
|
Now print with it:
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
fontPrint(24, 60, "ASTEROID PATROL")
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Run it. Big white letters near the top left.
|
|
|
|
Notice what you did *not* do: you never told `fontPrint` which font to use.
|
|
`fontLoad` makes the font it just loaded the selected one, and `fontPrint`
|
|
always draws with whatever is selected. With more than one font loaded you
|
|
choose between them with `fontSelect`, which you will do in a moment.
|
|
|
|
Notice also that `24` and `60` are pixels, not character cells. This is the
|
|
difference between `fontPrint` and `overlayPrint` that trips people up:
|
|
`overlayPrint(2, 2, ...)` means two letters in and two lines down, while
|
|
`fontPrint(2, 2, ...)` means two pixels in and two pixels down, which is
|
|
jammed into the corner. The `y` you give is the *top* of the text, not the
|
|
baseline it sits on.
|
|
|
|
=== Colour
|
|
|
|
`fontPrint` takes three arguments and none of them is a colour. The colour
|
|
comes from somewhere else:
|
|
|
|
[source,lua]
|
|
----
|
|
colorForeground(255, 211, 90)
|
|
fontPrint(24, 60, "ASTEROID PATROL")
|
|
----
|
|
|
|
`colorForeground` sets red, green, and blue from `0` to `255`, and an optional
|
|
fourth number for opacity. It is not an argument to anything; it is a setting
|
|
that the engine remembers. Everything drawn after it comes out in that colour
|
|
until you change it again, exactly the way `overlayBox` and `overlayLine`
|
|
already work. So the pattern is always the same: set the colour, then draw.
|
|
|
|
This is the first thing people get wrong with fonts, and the second thing is
|
|
coming up in a few pages.
|
|
|
|
=== Quality
|
|
|
|
`fontQuality` decides how the letters are rasterized -- how the curves of a
|
|
letter are turned into pixels -- and there are three choices. Try each one by
|
|
putting it above the `fontLoad` line and looking at the result closely.
|
|
|
|
`FONT_QUALITY_SOLID` is the fastest. Every pixel is either the letter or it is
|
|
not, so the diagonals and curves come out with visible stair steps. At 48 point
|
|
it looks cheap. At 12 point on a small overlay it can look *sharper* than the
|
|
alternatives, which is sometimes what you want.
|
|
|
|
`FONT_QUALITY_SHADED` smooths the edges, but it does it by blending them into
|
|
the current background colour, and it draws that background as a solid
|
|
rectangle behind the whole string. You get a label with a box around it whether
|
|
you wanted one or not. It is the right answer for a debug readout over a busy
|
|
picture and the wrong answer for almost everything else.
|
|
|
|
`FONT_QUALITY_BLENDED` smooths the edges and keeps them see-through, so the
|
|
letters sit on whatever is behind them. It costs the most and it is what you
|
|
want nearly all the time. Use it unless you have a reason not to.
|
|
|
|
The setting is global, not per font, and it applies from the moment you call it
|
|
onwards. Sprites you have already made keep the quality they were made with.
|
|
The manual's entry for `fontQuality` lists the three names and their values.
|
|
|
|
=== Measuring, so You Can Centre Things
|
|
|
|
Sooner or later -- usually about ten minutes in -- you want the title in the
|
|
middle of the screen. You know the screen is `overlayGetWidth()` wide. You do
|
|
not know how wide the text is, and there is no `fontMeasure` call to ask.
|
|
|
|
What there is instead is `fontToSprite`. It renders a string with the selected
|
|
font, the current quality, and the current colour, and instead of putting it on
|
|
screen it hands you back a *sprite*: the same kind of handle `spriteLoad` gives
|
|
you, which means every `sprite*` call you learned in lesson nine works on it.
|
|
Including the two that measure it.
|
|
|
|
[source,lua]
|
|
----
|
|
fontSelect(titleFont)
|
|
colorForeground(255, 211, 90)
|
|
local titleImage = fontToSprite("ASTEROID PATROL")
|
|
----
|
|
|
|
And then, in `onOverlayUpdate`:
|
|
|
|
[source,lua]
|
|
----
|
|
local x = (overlayGetWidth() - spriteGetWidth(titleImage)) / 2
|
|
spriteDraw(titleImage, x, 60)
|
|
----
|
|
|
|
That is centring, and it is the whole trick. Take the width of the space, take
|
|
away the width of the thing, and half of what is left is the gap at each side.
|
|
|
|
Right alignment is the same idea with less arithmetic. To put text against the
|
|
right hand margin:
|
|
|
|
[source,lua]
|
|
----
|
|
spriteDraw(scoreImage, overlayGetWidth() - MARGIN - spriteGetWidth(scoreImage), y)
|
|
----
|
|
|
|
`spriteGetHeight` measures the other direction, for stacking lines or centring
|
|
vertically.
|
|
|
|
There is a catch in that first code block, and it is the second thing everybody
|
|
gets wrong. *The colour is baked in when the sprite is made, not when it is
|
|
drawn.* `colorForeground` above `fontToSprite` changes the text. Putting it
|
|
above `spriteDraw` instead does nothing at all, because a sprite is drawn with
|
|
its own pixels and is never tinted. If your carefully coloured title comes out
|
|
white, this is why.
|
|
|
|
=== The Trap: fontPrint Is Not Free
|
|
|
|
Here is the thing the manual says plainly and everyone reads past.
|
|
|
|
`fontPrint` does not draw a picture it made earlier. Every single call
|
|
rasterizes the whole string from the font outlines, builds an image of it,
|
|
copies that image onto the overlay, and throws it away. Then, one sixtieth of a
|
|
second later, it does all of that again. For a title that has not changed since
|
|
the game started, you are re-drawing the same letters from scratch sixty times
|
|
a second, forever.
|
|
|
|
On a desktop with three lines of text you will never notice. With a screen full
|
|
of text, or on a Raspberry Pi, or in a cabinet, you will notice a great deal.
|
|
|
|
The fix is the call you just met. `fontToSprite` does that work *once*.
|
|
`spriteDraw` afterwards is a plain copy, which is what the hardware is for. So
|
|
the rule is short:
|
|
|
|
* Text that never changes -- titles, labels, instructions, menu entries --
|
|
`fontToSprite` once at startup, `spriteDraw` every frame.
|
|
* Text that changes every frame and nobody is staring at -- a debug readout, a
|
|
frame number -- `fontPrint` is fine.
|
|
* Text that changes sometimes -- a score, a name, a timer in whole seconds --
|
|
render a new sprite when it changes, and unload the old one.
|
|
|
|
That last case is worth writing out, because "unload the old one" is easy to
|
|
forget and forgetting it leaks a sprite per change:
|
|
|
|
[source,lua]
|
|
----
|
|
local function renderScore()
|
|
if scoreImage ~= nil then
|
|
spriteUnload(scoreImage)
|
|
end
|
|
fontSelect(scoreFont)
|
|
colorForeground(255, 255, 255)
|
|
scoreImage = fontToSprite(string.format("%06d", score))
|
|
scoreShown = score
|
|
end
|
|
----
|
|
|
|
`scoreShown` remembers which score that sprite says, so the drawing code can
|
|
tell whether it is still right:
|
|
|
|
[source,lua]
|
|
----
|
|
if score ~= scoreShown then
|
|
renderScore()
|
|
end
|
|
----
|
|
|
|
A score changes perhaps twice a second. This renders twice a second instead of
|
|
sixty times, and the code to do it is nine lines.
|
|
|
|
`string.format("%06d", score)` is plain Lua, not Singe. It turns a number into
|
|
a string, and `%06d` means "as a whole number, at least six digits, padded with
|
|
zeros", which is why arcade scores have leading zeros. Change it to `%d` and
|
|
the zeros go away.
|
|
|
|
=== Putting Fonts Away
|
|
|
|
`fontUnload` closes a font and makes its handle invalid. Sprites you made with
|
|
`fontToSprite` are not affected -- once rendered, a sprite has nothing more to
|
|
do with the font -- so you can unload a font and go on drawing text made from
|
|
it.
|
|
|
|
One sharp edge: if you unload the font that was selected, *no* font is selected
|
|
afterwards, and the next `fontPrint` ends your game with an error. Select
|
|
another one first if you are going to keep printing.
|
|
|
|
The tidy place for all this is `onShutdown`, a callback the engine calls once
|
|
when the game is closing:
|
|
|
|
[source,lua]
|
|
----
|
|
function onShutdown()
|
|
spriteUnload(titleImage)
|
|
fontUnload(titleFont)
|
|
end
|
|
----
|
|
|
|
Singe cleans up after you when the game ends, so nothing catches fire if you
|
|
skip this. Write it anyway. A game that releases what it loaded is a game you
|
|
can load twice.
|
|
|
|
=== The Whole Script
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
|
|
local MARGIN = 24
|
|
local TITLE_Y = 60
|
|
local SCORE_Y = 170
|
|
local LABEL_Y = 186
|
|
local HINT_Y = 300
|
|
local CLOCK_Y = 430
|
|
|
|
local score = 0
|
|
local titleFont = nil
|
|
local scoreFont = nil
|
|
local smallFont = nil
|
|
local titleImage = nil
|
|
local hintImage = nil
|
|
local scoreImage = nil
|
|
local scoreShown = nil
|
|
|
|
|
|
local function centred(image)
|
|
return (overlayGetWidth() - spriteGetWidth(image)) / 2
|
|
end
|
|
|
|
|
|
local function renderScore()
|
|
if scoreImage ~= nil then
|
|
spriteUnload(scoreImage)
|
|
end
|
|
fontSelect(scoreFont)
|
|
colorForeground(255, 255, 255)
|
|
scoreImage = fontToSprite(string.format("%06d", score))
|
|
scoreShown = score
|
|
end
|
|
|
|
|
|
fontQuality(FONT_QUALITY_BLENDED)
|
|
|
|
titleFont = fontLoad("Singe/FreeSansBold.ttf", 48)
|
|
scoreFont = fontLoad("Singe/FreeSansBold.ttf", 36)
|
|
smallFont = fontLoad("Singe/FreeSansBold.ttf", 18)
|
|
|
|
fontSelect(titleFont)
|
|
colorForeground(255, 211, 90)
|
|
titleImage = fontToSprite("ASTEROID PATROL")
|
|
|
|
fontSelect(smallFont)
|
|
colorForeground(140, 200, 255)
|
|
hintImage = fontToSprite("Press the fire button to score")
|
|
|
|
renderScore()
|
|
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 then
|
|
score = score + 125
|
|
end
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
|
|
spriteDraw(titleImage, centred(titleImage), TITLE_Y)
|
|
|
|
if score ~= scoreShown then
|
|
renderScore()
|
|
end
|
|
spriteDraw(scoreImage, overlayGetWidth() - MARGIN - spriteGetWidth(scoreImage), SCORE_Y)
|
|
|
|
fontSelect(smallFont)
|
|
colorForeground(160, 160, 160)
|
|
fontPrint(MARGIN, LABEL_Y, "SCORE")
|
|
fontPrint(MARGIN, CLOCK_Y, "Running for " .. (singeGetTicks() // 1000) .. " seconds")
|
|
|
|
if (singeGetTicks() // 500) % 2 == 0 then
|
|
spriteDraw(hintImage, centred(hintImage), HINT_Y)
|
|
end
|
|
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
function onShutdown()
|
|
spriteUnload(titleImage)
|
|
spriteUnload(hintImage)
|
|
if scoreImage ~= nil then
|
|
spriteUnload(scoreImage)
|
|
end
|
|
fontUnload(titleFont)
|
|
fontUnload(scoreFont)
|
|
fontUnload(smallFont)
|
|
end
|
|
----
|
|
|
|
Run it and hold the fire button. The title sits centred and never re-renders,
|
|
the score climbs and re-renders only when it changes, the hint blinks, and the
|
|
clock at the bottom is the one thing honestly drawn with `fontPrint` sixty
|
|
times a second.
|
|
|
|
=== What Just Happened
|
|
|
|
[source,lua]
|
|
----
|
|
titleFont = fontLoad("Singe/FreeSansBold.ttf", 48)
|
|
scoreFont = fontLoad("Singe/FreeSansBold.ttf", 36)
|
|
smallFont = fontLoad("Singe/FreeSansBold.ttf", 18)
|
|
----
|
|
|
|
One file, three sizes, three handles. Each one costs memory for its own set of
|
|
rendered glyphs, so load the sizes you use and no more. Three is normal. A
|
|
dozen is a smell.
|
|
|
|
After these three lines the selected font is `smallFont`, because loading
|
|
selects. That is why the code calls `fontSelect(titleFont)` before rendering
|
|
the title: the last thing loaded is not the thing you want first.
|
|
|
|
[source,lua]
|
|
----
|
|
local function centred(image)
|
|
return (overlayGetWidth() - spriteGetWidth(image)) / 2
|
|
end
|
|
----
|
|
|
|
Written once as a function because it is used twice, and because the next time
|
|
you want something centred you will want it again. Give the arithmetic a name
|
|
and you never have to read it again.
|
|
|
|
[source,lua]
|
|
----
|
|
if (singeGetTicks() // 500) % 2 == 0 then
|
|
spriteDraw(hintImage, centred(hintImage), HINT_Y)
|
|
end
|
|
----
|
|
|
|
`singeGetTicks()` is the number of milliseconds since the engine started. `//`
|
|
is division that throws away the remainder, so `singeGetTicks() // 500` counts
|
|
half-seconds, and `% 2` is the remainder after dividing by two, which is `0`,
|
|
`1`, `0`, `1`... So the hint is drawn for half a second and skipped for half a
|
|
second. Drawing nothing is how you make something blink.
|
|
|
|
[source,lua]
|
|
----
|
|
fontSelect(smallFont)
|
|
colorForeground(160, 160, 160)
|
|
fontPrint(MARGIN, LABEL_Y, "SCORE")
|
|
----
|
|
|
|
Both settings are needed here, and it is worth being clear about why. Drawing
|
|
a sprite does not change the selected font or the colour, so the two
|
|
`spriteDraw` calls above leave both exactly as they found them. But the setup
|
|
code selected three different fonts in turn, and `renderScore` selects another
|
|
one whenever the score moves, so the selection when this line runs is whichever
|
|
one happened to be chosen last. Do not try to keep track of it. Select the font
|
|
and set the colour immediately before you print, every time. It is two lines
|
|
and it removes a whole category of puzzled evening.
|
|
|
|
=== Try It
|
|
|
|
. *Change the quality.* Put `FONT_QUALITY_SOLID` in the `fontQuality` line and
|
|
look at the curve of the `S` in `ASTEROID`. Then try `FONT_QUALITY_SHADED`
|
|
and watch what happens behind every string.
|
|
. *Make the hint yellow.* Move the `colorForeground(140, 200, 255)` line from
|
|
above `fontToSprite` to just above the `spriteDraw` that draws `hintImage`,
|
|
and change the numbers. Work out why nothing happens.
|
|
. *Right align the clock.* The clock is drawn with `fontPrint`, which cannot be
|
|
measured. Get it against the right margin anyway. There is only one way, and
|
|
finding it is the point.
|
|
. *Take out the `scoreShown` check*, so `renderScore()` runs every frame
|
|
instead. It will look identical. Leave it running for a minute and watch the
|
|
memory your game is using.
|
|
. *Use your own font.* Find a `.ttf` on your machine, copy it beside your
|
|
script, and load it with `DIR .. "thatfont.ttf"`. Some fonts look terrible at
|
|
18 point and fine at 48. That is the font's fault, not yours.
|
|
|
|
=== Break It on Purpose
|
|
|
|
Add a line reading `fontPrint(24, 60, "SCORE")` directly under
|
|
`overlaySetResolution(discGetWidth(), discGetHeight())`, so that it runs before
|
|
any `fontLoad` has. Singe starts and dies at once:
|
|
|
|
----
|
|
5:fontPrint: No font selected.
|
|
----
|
|
|
|
Read it the same way as any other error. `5` is the line. `fontPrint` is which
|
|
engine call complained -- this shape of message comes from inside the engine
|
|
rather than from Lua, so it names the function instead of the file. The rest is
|
|
the complaint, and here it is exact: you asked it to print and there was no
|
|
font to print with.
|
|
|
|
You will meet this one for real in a less obvious way. Unload the selected font
|
|
while the game is running and the next `fontPrint` says precisely the same
|
|
thing, several hundred lines away from the `fontUnload` that caused it.
|
|
|
|
=== What You Learned
|
|
|
|
* `fontLoad` opens a TrueType file at one fixed point size and returns a
|
|
handle. One size, one handle.
|
|
* Loading a font selects it; `fontSelect` chooses between loaded fonts.
|
|
* `fontPrint` counts in pixels and `overlayPrint` counts in character cells.
|
|
* Colour is not an argument. `colorForeground` is a setting; set it, then draw.
|
|
* `fontQuality` picks how letters are rasterized, and `FONT_QUALITY_BLENDED`
|
|
is the usual answer.
|
|
* `fontPrint` re-renders the string on every single call.
|
|
* `fontToSprite` renders once into a sprite, which `spriteDraw` then copies
|
|
cheaply and `spriteGetWidth` measures.
|
|
* Measuring is how you centre or right align text, because nothing else can
|
|
tell you how wide a string will be.
|
|
* A sprite's colour is fixed when it is made, not when it is drawn.
|
|
* `fontUnload` frees a font; the sprites it made survive it.
|
|
|
|
=== Next Time
|
|
|
|
Text that looks good is a long way from a menu that works. A page with a
|
|
title, a list you move a cursor through, a slider, and a button is a hundred
|
|
lines of drawing and cursor arithmetic if you build it out of the calls in this
|
|
lesson. Lesson twenty does not build it out of those calls at all.
|