[#_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.