512 lines
19 KiB
Text
512 lines
19 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 what printing text actually costs. 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 `colorForeground` is a
|
|
setting for drawing text and lines, and a sprite is drawn with its own pixels.
|
|
If your carefully coloured title comes out white, this is why.
|
|
|
|
A sprite can be recoloured after it is made, but by a sprite call, not by
|
|
`colorForeground`. `spriteSetTint(image, red, green, blue)` multiplies every
|
|
pixel of the sprite by that colour each time it is drawn, so text rendered in
|
|
white can be drawn yellow today and red tomorrow without being made again.
|
|
White is the colour to render in when you mean to tint: anything multiplied
|
|
by white stays what it was, so the tint is exactly the colour you ask for.
|
|
|
|
=== What Printing Costs
|
|
|
|
The first time `fontPrint` sees a string, it does real work: it rasterizes
|
|
every letter from the font's outlines and builds an image of the whole string.
|
|
Then it keeps that image. The next time you print the same string, with the
|
|
same font, quality, and colours, it skips all of that and copies the image it
|
|
already has, which is about what drawing a sprite costs. A string you stop
|
|
printing is let go a second later, so nothing piles up.
|
|
|
|
That means the obvious way to write a title screen is also the fast one.
|
|
Printing `"ASTEROID PATROL"` sixty times a second renders it once. A score that
|
|
changes twice a second renders twice a second, on its own, with no bookkeeping
|
|
from you.
|
|
|
|
The one case that costs is text that is *different* every frame -- a timer in
|
|
milliseconds, a frame counter -- because every frame is a string `fontPrint`
|
|
has never seen. On a desktop it does not matter. On a Raspberry Pi or a
|
|
handheld, a screenful of it will. The cure is to change the text less often:
|
|
a timer in whole seconds is a new string once a second, not sixty times.
|
|
|
|
So why did this lesson use `fontToSprite` at all? Because a sprite can do
|
|
things printed text cannot:
|
|
|
|
* *It can be measured*, which is the only way to centre text or line it up
|
|
against the right hand side.
|
|
* *It can be scaled and turned* with the `sprite*` calls from lesson nine.
|
|
* *It can outlive its font*, which matters in a moment.
|
|
|
|
Use `fontPrint` for text, and `fontToSprite` when you need one of those.
|
|
|
|
The score in this lesson needs one of them: it sits against the right hand
|
|
margin, so it has to be measured, so it has to be a sprite. And a sprite of a
|
|
score is only right until the score changes. When it does, render a new sprite
|
|
and unload the old one -- forgetting that 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
|
|
----
|
|
|
|
`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 and the hint sit centred, the score
|
|
climbs against the right margin and is re-rendered only when it changes, the
|
|
hint blinks, and the label and the clock are printed with `fontPrint` every
|
|
frame -- the clock in whole seconds, so it is a new string only once 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. Then put it back, make
|
|
the hint white instead, and turn it yellow with `spriteSetTint` just before
|
|
it is drawn.
|
|
. *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, but it now renders the score sixty times a
|
|
second instead of when it changes -- exactly the work `fontPrint` saves you
|
|
when you let it. Then take out the `spriteUnload` in `renderScore` as well,
|
|
leave it running for a minute, and watch the memory your game is using.
|
|
. *Print a clock in milliseconds.* Change the clock line to print
|
|
`singeGetTicks()` without the `// 1000`. It still works; it is now a string
|
|
`fontPrint` has never seen, every frame. On a slow machine, that is the kind
|
|
of text that costs.
|
|
. *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` keeps what it renders, so printing the same text every frame is
|
|
cheap; text that is different every frame is what costs.
|
|
* `fontToSprite` renders into a sprite, which `spriteGetWidth` measures and the
|
|
`sprite*` calls can scale and turn. Use it for that, not for speed.
|
|
* Measuring is how you centre or right align text, because nothing else can
|
|
tell you how wide a string will be.
|
|
* `colorForeground` reaches text when it is made, never a sprite when it is
|
|
drawn; `spriteSetTint` is how a sprite is recoloured afterwards.
|
|
* `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.
|