singe/docs/it/lessons/19-text.adoc

547 lines
21 KiB
Text

[#_lesson_19_text_that_looks_good]
== Lezione 19: Testo che fa bella figura
image::learn/19-text.png[La lezione finita, 480]
Ogni parola che hai messo sullo schermo dalla prima lezione è uscita da
`overlayPrint`, e `overlayPrint` ha esattamente un font: un blocco di pixel a
larghezza fissa incorporato nel motore, sempre della stessa dimensione, in
qualunque colore capiti. È perfetto per un contatore di fotogrammi. È sbagliato
per un titolo, un punteggio, un menu, o qualsiasi cosa che un giocatore deve
guardare invece di scorrere con gli occhi.
Questa lezione carica un vero font dal disco, ci stampa, lo colora, lo misura
così puoi centrare le cose, e ti mostra quanto costa davvero stampare del testo.
Alla fine avrai una schermata del titolo che sembra voluta invece che capitata
per caso.
[#_set_the_overlay_to_a_size_you_can_think_in]
=== Dai all'overlay una dimensione con cui ragionare
Crea un nuovo file chiamato `text.singe` con due righe:
[source,lua]
----
dofile("Singe/Framework.singe")
overlaySetResolution(discGetWidth(), discGetHeight())
----
La prima riga l'hai scritta molte volte; carica il framework, che definisce
`DIR`, i nomi `SWITCH_*` e i nomi `FONT_QUALITY_*` che stai per usare.
La seconda conta più di quanto sembri. L'overlay -- la superficie su cui finisce
tutto quello che disegni -- parte a *metà* della dimensione del mondo. Un gioco
senza video ha un canvas di 720 per 480, quindi l'overlay parte da 360 per 240,
e un font da 36 punti su un overlay largo 360 pixel è enorme.
`overlaySetResolution` sostituisce l'overlay con uno della dimensione che
chiedi, e da quel momento ogni coordinata che dai, e tutto ciò che ti dicono
`overlayGetWidth` e `overlayGetHeight`, sta in quello spazio.
`discGetWidth()` e `discGetHeight()` rispondono con la dimensione del video
quando c'è, e con quella del canvas quando non c'è, così quell'unica riga ti dà
un overlay che corrisponde al mondo, qualunque tipo di gioco sia questo.
Chiamala una volta, all'inizio, prima di disegnare qualsiasi cosa.
[#_load_a_font]
=== Carica un font
Sotto, aggiungi:
[source,lua]
----
fontQuality(FONT_QUALITY_BLENDED)
local titleFont = fontLoad("Singe/FreeSansBold.ttf", 48)
----
`fontLoad` apre un file di font TrueType e ti restituisce un numero -- un
*handle* -- che usi ogni volta che vuoi quel font. Il secondo argomento è la
dimensione in punti, ed è fissa per tutta la vita dell'handle. Non c'è modo di
cambiare la dimensione di un font già caricato. Se vuoi lo stesso carattere in
tre dimensioni, chiami `fontLoad` tre volte e tieni tre handle.
`Singe/FreeSansBold.ttf` è il font che il motore ha estratto nella cartella
`Singe` dentro la tua cartella di lavoro la prima volta che hai eseguito
qualcosa. Ce l'ha ogni copia di Singe, quindi puoi contarci. Qualsiasi altro
file `.ttf` funziona allo stesso modo; mettilo accanto al tuo script e caricalo
come `DIR .. "myfont.ttf"`.
Ora stampaci:
[source,lua]
----
function onOverlayUpdate()
overlayClear()
fontPrint(24, 60, "ASTEROID PATROL")
return OVERLAY_UPDATED
end
----
Eseguilo. Grandi lettere bianche vicino all'angolo in alto a sinistra.
Nota che cosa *non* hai fatto: non hai mai detto a `fontPrint` quale font
usare. `fontLoad` seleziona il font che ha appena caricato, e
`fontPrint` disegna sempre con quello selezionato. Con più di un font caricato
scegli tra loro con `fontSelect`, ed è quello che farai tra un momento.
Nota anche che `24` e `60` sono pixel, non celle di carattere. Questa è la
differenza tra `fontPrint` e `overlayPrint` che fa inciampare la gente:
`overlayPrint(2, 2, ...)` vuol dire due lettere verso destra e due righe verso
il basso, mentre `fontPrint(2, 2, ...)` vuol dire due pixel verso destra e due
pixel verso il basso, cioè schiacciato nell'angolo. La `y` che dai è la *cima*
del testo, non la linea di base su cui poggia.
[#_color]
=== Colore
`fontPrint` prende tre argomenti e nessuno di questi è un colore. Il colore
arriva da un'altra parte:
[source,lua]
----
colorForeground(255, 211, 90)
fontPrint(24, 60, "ASTEROID PATROL")
----
`colorForeground` imposta rosso, verde e blu da `0` a `255`, più un quarto
numero facoltativo per l'opacità. Non è un argomento di niente; è
un'impostazione che il motore si ricorda. Tutto ciò che disegni dopo esce in
quel colore finché non lo cambi di nuovo, esattamente come funzionano già
`overlayBox` e `overlayLine`. Quindi lo schema è sempre lo stesso: imposta il
colore, poi disegna.
Questa è la prima cosa che la gente sbaglia con i font, e la seconda arriva tra
qualche pagina.
[#_quality]
=== Qualità
`fontQuality` decide come vengono rasterizzate le lettere -- come le curve di
una lettera vengono trasformate in pixel -- e ci sono tre scelte. Provale una
per una mettendola sopra la riga di `fontLoad` e guardando bene il risultato.
`FONT_QUALITY_SOLID` è la più veloce. Ogni pixel o è la lettera o non lo è,
quindi le diagonali e le curve escono con dei gradini ben visibili. A 48 punti
sembra da quattro soldi. A 12 punti su un overlay piccolo può sembrare *più
nitida* delle alternative, ed è a volte quello che vuoi.
`FONT_QUALITY_SHADED` ammorbidisce i bordi, ma lo fa sfumandoli nel colore di
sfondo attuale, e disegna quello sfondo come un rettangolo pieno dietro tutta la
stringa. Ottieni un'etichetta con un riquadro intorno, che tu lo volessi o no.
È la risposta giusta per un indicatore di debug sopra un'immagine piena di
roba, e quella sbagliata per quasi tutto il resto.
`FONT_QUALITY_BLENDED` ammorbidisce i bordi e li lascia trasparenti, così le
lettere stanno sopra qualunque cosa ci sia dietro. È quella che costa di più ed
è quella che vuoi quasi sempre. Usala, a meno che tu non abbia un motivo per
non farlo.
L'impostazione è globale, non per font, e vale dal momento in cui la chiami in
poi. Gli sprite che hai già creato mantengono la qualità con cui sono stati
creati. La voce del Riferimento su `fontQuality` elenca i tre nomi e i loro valori.
[#_measuring_so_you_can_center_things]
=== Misurare, per poter centrare le cose
Prima o poi -- di solito dopo una decina di minuti -- vuoi il titolo in mezzo
allo schermo. Sai che lo schermo è largo `overlayGetWidth()`. Non sai quanto è
largo il testo, e non c'è nessuna chiamata `fontMeasure` a cui chiederlo.
Quello che c'è invece è `fontToSprite`. Disegna una stringa con il font
selezionato, la qualità attuale e il colore attuale, e invece di metterla sullo
schermo ti restituisce uno *sprite*: lo stesso tipo di handle che ti dà
`spriteLoad`, il che vuol dire che ogni chiamata `sprite*` che hai imparato
nella nona lezione funziona anche su questo. Comprese le due che lo misurano.
[source,lua]
----
fontSelect(titleFont)
colorForeground(255, 211, 90)
local titleImage = fontToSprite("ASTEROID PATROL")
----
E poi, in `onOverlayUpdate`:
[source,lua]
----
local x = (overlayGetWidth() - spriteGetWidth(titleImage)) / 2
spriteDraw(titleImage, x, 60)
----
Questo è centrare, ed è tutto il trucco. Prendi la larghezza dello spazio,
togli la larghezza della cosa, e metà di quello che resta è lo spazio vuoto da
ogni lato.
Allineare a destra è la stessa idea con meno aritmetica. Per mettere il testo
contro il margine destro:
[source,lua]
----
spriteDraw(scoreImage, overlayGetWidth() - MARGIN - spriteGetWidth(scoreImage), y)
----
`spriteGetHeight` misura l'altra direzione, per impilare righe o centrare in
verticale.
C'è un tranello in quel primo blocco di codice, ed è la seconda cosa che
sbagliano tutti. *Il colore viene fissato quando lo sprite viene creato, non
quando viene disegnato.* `colorForeground` sopra `fontToSprite` cambia il
testo. Metterlo invece sopra `spriteDraw` non fa proprio niente, perché
`colorForeground` è un'impostazione per disegnare testo e linee, e uno sprite
viene disegnato con i suoi pixel. Se il tuo titolo colorato con tanta cura esce
bianco, il motivo è questo.
Uno sprite si può ricolorare dopo averlo creato, ma con una chiamata degli
sprite, non con `colorForeground`. `spriteSetTint(image, red, green, blue)`
moltiplica ogni pixel dello sprite per quel colore ogni volta che viene
disegnato, così un testo creato in bianco si può disegnare giallo oggi e rosso
domani senza rifarlo. Il bianco è il colore in cui crearlo quando hai intenzione
di tingerlo: qualsiasi cosa moltiplicata per il bianco resta com'era, quindi la
tinta è esattamente il colore che chiedi.
[#_what_printing_costs]
=== Quanto costa stampare
La prima volta che `fontPrint` vede una stringa, fa un lavoro vero: rasterizza
ogni lettera dai contorni del font e costruisce un'immagine di tutta la
stringa. Poi quell'immagine se la tiene. La volta dopo che stampi la stessa
stringa, con lo stesso font, la stessa qualità e gli stessi colori, salta tutto
questo e copia l'immagine che ha già, che costa più o meno quanto disegnare uno
sprite. Una stringa che smetti di stampare viene lasciata andare un secondo
dopo, quindi non si accumula niente.
Questo vuol dire che il modo ovvio di scrivere una schermata del titolo è anche
quello veloce. Stampare `"ASTEROID PATROL"` sessanta volte al secondo lo
disegna una volta sola. Un punteggio che cambia due volte al secondo viene
disegnato due volte al secondo, da solo, senza che tu debba tenere nessun conto.
L'unico caso che costa è il testo che è *diverso* a ogni fotogramma -- un timer
in millisecondi, un contatore di fotogrammi -- perché ogni fotogramma è una
stringa che `fontPrint` non ha mai visto. Su un computer desktop non importa. Su
un Raspberry Pi o una console portatile, uno schermo pieno di testo così
importa. Il rimedio è cambiare il testo meno spesso: un timer in secondi interi
è una stringa nuova una volta al secondo, non sessanta.
Allora perché questa lezione ha usato `fontToSprite`? Perché uno sprite può fare
cose che il testo stampato non può fare:
* *Si può misurare*, ed è l'unico modo per centrare il testo o allinearlo contro
il lato destro.
* *Si può ridimensionare e ruotare* con le chiamate `sprite*` della nona lezione.
* *Può sopravvivere al suo font*, e tra un momento questo conterà.
Usa `fontPrint` per il testo, e `fontToSprite` quando ti serve una di queste
cose.
Il punteggio di questa lezione ne ha bisogno: sta contro il margine destro,
quindi va misurato, quindi deve essere uno sprite. E uno sprite di un punteggio
è giusto solo finché il punteggio non cambia. Quando cambia, crea un nuovo
sprite e scarica quello vecchio -- dimenticarsene lascia per strada uno sprite a
ogni cambio:
[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` si ricorda quale punteggio dice quello sprite, così il codice che
disegna può capire se è ancora giusto:
[source,lua]
----
if score ~= scoreShown then
renderScore()
end
----
`string.format("%06d", score)` è Lua puro, non Singe. Trasforma un numero in una
stringa, e `%06d` vuol dire «come numero intero, di almeno sei cifre,
riempito di zeri», ed è per questo che i punteggi da sala giochi hanno gli zeri
davanti. Cambialo in `%d` e gli zeri spariscono.
[#_putting_fonts_away]
=== Mettere via i font
`fontUnload` chiude un font e rende il suo handle non valido. Gli sprite che hai
creato con `fontToSprite` non ne risentono -- una volta disegnato, uno sprite non
ha più niente a che fare con il font -- quindi puoi scaricare un font e
continuare a disegnare il testo creato con lui.
Un'insidia: se scarichi il font che era selezionato, dopo non è
selezionato *nessun* font, e il `fontPrint` successivo chiude il tuo gioco con
un errore. Se hai intenzione di continuare a stampare, selezionane prima un
altro.
Il posto ordinato per tutto questo è `onShutdown`, una callback che il motore
chiama una volta quando il gioco si sta chiudendo:
[source,lua]
----
function onShutdown()
spriteUnload(titleImage)
fontUnload(titleFont)
end
----
Singe fa pulizia al posto tuo quando il gioco finisce, quindi non prende fuoco
niente se salti questa parte. Scrivila lo stesso. Un gioco che rilascia quello
che ha caricato è un gioco che puoi caricare due volte.
[#_the_whole_script]
=== Lo script completo
[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 centered(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, centered(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, centered(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
----
Eseguilo e tieni premuto il pulsante di fuoco. Il titolo e il suggerimento
stanno al centro, il punteggio sale contro il margine destro e viene ridisegnato
solo quando cambia, il suggerimento lampeggia, e l'etichetta e l'orologio
vengono stampati con `fontPrint` a ogni fotogramma -- l'orologio in secondi
interi, così è una stringa nuova solo una volta al secondo.
[#_what_just_happened_15]
=== Cos'è appena successo
[source,lua]
----
titleFont = fontLoad("Singe/FreeSansBold.ttf", 48)
scoreFont = fontLoad("Singe/FreeSansBold.ttf", 36)
smallFont = fontLoad("Singe/FreeSansBold.ttf", 18)
----
Un file, tre dimensioni, tre handle. Ognuno costa memoria per il suo insieme di
glifi disegnati, quindi carica le dimensioni che usi e non di più. Tre è
normale. Una dozzina comincia a puzzare.
Dopo queste tre righe il font selezionato è `smallFont`, perché caricare
seleziona. Ecco perché il codice chiama `fontSelect(titleFont)` prima di
disegnare il titolo: l'ultima cosa caricata non è quella che vuoi per prima.
[source,lua]
----
local function centered(image)
return (overlayGetWidth() - spriteGetWidth(image)) / 2
end
----
Scritta una volta come funzione perché viene usata due volte, e perché la
prossima volta che vorrai centrare qualcosa la vorrai di nuovo. Dai un nome
all'aritmetica e non dovrai mai più rileggerla.
[source,lua]
----
if (singeGetTicks() // 500) % 2 == 0 then
spriteDraw(hintImage, centered(hintImage), HINT_Y)
end
----
`singeGetTicks()` è il numero di millisecondi dall'avvio del motore. `//`
è la divisione che butta via il resto, quindi `singeGetTicks() // 500` conta i
mezzi secondi, e `% 2` è il resto della divisione per due, che fa `0`,
`1`, `0`, `1`... Quindi il suggerimento viene disegnato per mezzo secondo e
saltato per mezzo secondo. Non disegnare niente è il modo per far lampeggiare
qualcosa.
[source,lua]
----
fontSelect(smallFont)
colorForeground(160, 160, 160)
fontPrint(MARGIN, LABEL_Y, "SCORE")
----
Qui servono tutte e due le impostazioni, e vale la pena capire bene perché.
Disegnare uno sprite non cambia il font selezionato né il colore, quindi le due
chiamate `spriteDraw` qui sopra li lasciano esattamente come li hanno trovati.
Ma il codice di preparazione ha selezionato tre font diversi uno dopo l'altro, e
`renderScore` ne seleziona un altro ogni volta che il punteggio si muove, quindi
quando questa riga viene eseguita è selezionato quello che è capitato di
scegliere per ultimo. Non cercare di tenerne traccia. Seleziona il font e
imposta il colore subito prima di stampare, ogni volta. Sono due righe, e
tolgono di mezzo un'intera categoria di serate passate a grattarsi la testa.
[#_try_it_19]
=== Prova tu
. *Cambia la qualità.* Metti `FONT_QUALITY_SOLID` nella riga di `fontQuality`
e guarda la curva della `S` in `ASTEROID`. Poi prova `FONT_QUALITY_SHADED` e
guarda che cosa succede dietro ogni stringa.
. *Fai diventare giallo il suggerimento.* Sposta la riga
`colorForeground(140, 200, 255)` da sopra `fontToSprite` a subito sopra lo
`spriteDraw` che disegna `hintImage`, e cambia i numeri. Capisci perché non
succede niente. Poi rimettila a posto, fai invece il suggerimento bianco, e
fallo diventare giallo con `spriteSetTint` subito prima che venga disegnato.
. *Allinea l'orologio a destra.* L'orologio è disegnato con `fontPrint`, che
non si può misurare. Mettilo contro il margine destro lo stesso. C'è un solo
modo, e trovarlo è il punto.
. *Togli il controllo su `scoreShown`*, così `renderScore()` viene eseguita a
ogni fotogramma. Sembrerà identico, ma adesso disegna il punteggio sessanta
volte al secondo invece di quando cambia -- esattamente il lavoro che
`fontPrint` ti risparmia quando lo lasci fare. Poi togli anche lo
`spriteUnload` in `renderScore`, lascialo girare per un minuto, e guarda la
memoria che usa il tuo gioco.
. *Stampa un orologio in millisecondi.* Cambia la riga dell'orologio perché
stampi `singeGetTicks()` senza il `// 1000`. Funziona ancora; adesso è una
stringa che `fontPrint` non ha mai visto, a ogni fotogramma. Su una macchina
lenta, è questo il tipo di testo che costa.
. *Usa un font tuo.* Trova un `.ttf` sul tuo computer, copialo accanto al tuo
script e caricalo con `DIR .. "thatfont.ttf"`. Certi font sono orribili a 18
punti e vanno benissimo a 48. È colpa del font, non tua.
[#_break_it_on_purpose_18]
=== Rompilo apposta
Aggiungi una riga con scritto `fontPrint(24, 60, "SCORE")` subito sotto
`overlaySetResolution(discGetWidth(), discGetHeight())`, così viene eseguita
prima di qualsiasi `fontLoad`. Singe parte e muore subito:
----
4:fontPrint: No font selected.
----
Leggilo come qualsiasi altro errore. `4` è la riga. `fontPrint` è la chiamata
del motore che si è lamentata -- un messaggio di questa forma arriva da dentro
il motore invece che da Lua, quindi nomina la funzione invece del file. Il resto
è la lamentela, e qui è precisa: gli hai chiesto di stampare e non c'era nessun
font con cui farlo.
Questo errore lo incontrerai sul serio in un modo meno ovvio. Scarica il font
selezionato mentre il gioco gira e il `fontPrint` successivo dice esattamente
la stessa cosa, a qualche centinaio di righe di distanza dal `fontUnload` che
l'ha causato.
[#_what_you_learned_19]
=== Cosa hai imparato
* `fontLoad` apre un file TrueType a una dimensione in punti fissa e
restituisce un handle. Una dimensione, un handle.
* Caricare un font lo seleziona; `fontSelect` sceglie tra i font caricati.
* `fontPrint` conta in pixel e `overlayPrint` conta in celle di carattere.
* Il colore non è un argomento. `colorForeground` è un'impostazione: impostala,
poi disegna.
* `fontQuality` sceglie come vengono rasterizzate le lettere, e
`FONT_QUALITY_BLENDED` è la risposta normale.
* `fontPrint` si tiene quello che disegna, quindi stampare lo stesso testo a
ogni fotogramma costa poco; quello che costa è il testo diverso a ogni
fotogramma.
* `fontToSprite` disegna dentro uno sprite, che `spriteGetWidth` misura e le
chiamate `sprite*` possono ridimensionare e ruotare. Usalo per questo, non per la
velocità.
* Misurare è il modo per centrare il testo o allinearlo a destra, perché
nient'altro può dirti quanto sarà larga una stringa.
* `colorForeground` arriva al testo quando viene creato, mai a uno sprite
quando viene disegnato; `spriteSetTint` è il modo per ricolorare uno sprite
dopo.
* `fontUnload` libera un font; gli sprite che ha creato gli sopravvivono.
[#_next_time_19]
=== La prossima volta
Un testo che fa bella figura è ben lontano da un menu che funziona. Una pagina
con un titolo, un elenco in cui sposti un cursore, uno slider e un pulsante è
un centinaio di righe di disegno e di aritmetica dei cursori, se la costruisci
con le chiamate di questa lezione. La ventesima lezione non la
costruisce affatto con quelle chiamate.