singe/docs/it/lessons/20-gui.adoc

628 lines
23 KiB
Text

[#_lesson_20_menus_and_screens]
== Lezione 20: Menu e schermate
image::learn/20-gui.png[La lezione finita, 480]
Puoi costruire un menu con quello che sai già. Un riquadro, un po' di testo, un
cursore che si sposta quando si sposta la levetta, un `if` per ogni scelta, e un
po' di aritmetica per tenere tutto allineato quando aggiungi una quarta voce.
Funziona. Sono anche un centinaio di righe per una pagina con sopra tre
pulsanti, e ognuna di quelle righe tocca a te mantenerla.
Singe ha al suo interno un intero motore per documenti -- RmlUi -- e un menu
scritto per RmlUi è una pagina, come una pagina web, con un foglio di stile.
Manda a capo il testo da sé. Si impagina da sé quando aggiungi una voce. Sa che
cos'è un pulsante, che cos'è uno slider, e a quale controllo sta puntando il
giocatore. Questa lezione ci costruisce un menu di pausa, sopra un gioco che
continua a girare dietro.
[#_two_files]
=== Due file
Una pagina della GUI è fatta di due cose: un *documento* che dice che cosa c'è
sulla pagina, e del *Lua* che dice che cosa fa la pagina. Stanno in file
separati, ed è proprio questo il senso dell'esercizio -- l'impaginazione smette
di essere codice.
Comincia con uno script chiamato `menu.singe` e un gioco così piccolo che sta in
un paragrafo: un'astronave che attraversa lo schermo, e un punteggio che sale
ogni volta che arriva dall'altra parte. Scrivi cose del genere dalla settima
lezione.
[source,lua]
----
dofile("Singe/Framework.singe")
overlaySetResolution(discGetWidth(), discGetHeight())
local ship = spriteLoad(DIR .. "art/spaceship.png")
local shipX = 0
local speed = 2
local score = 0
function onOverlayUpdate()
overlayClear()
shipX = shipX + speed
if shipX > overlayGetWidth() then
shipX = -spriteGetWidth(ship)
score = score + 10
end
spriteDraw(ship, shipX, 300)
return OVERLAY_UPDATED
end
----
Funziona. Adesso il documento. Accanto al tuo script, crea un file chiamato
`pause.rml`:
[source,html]
----
<rml>
<head>
<title>Pause</title>
<link type="text/rcss" href="Singe/gui.rcss"/>
<style>
body { width: 100%; height: 100%; }
#frame { width: 380dp; margin: 80dp auto; }
.row { display: block; margin: 8dp 0; }
label { display: inline-block; width: 100dp; }
button { nav: auto; }
input.range { nav: auto; width: 200dp; }
#status { min-height: 28dp; }
</style>
</head>
<body id="pause">
<div id="frame" class="panel">
<h1>Paused</h1>
<p class="row">Score <span id="score">000000</span></p>
<div class="row"><label>Speed</label><input type="range" id="speed" min="1" max="8" step="1" value="2"/></div>
<div class="row">
<button id="resume" autofocus>Resume</button>
<button id="restart">Restart</button>
<button id="quit">Quit</button>
</div>
<p id="status" class="muted">Arrow keys move, Return chooses.</p>
</div>
</body>
</rml>
----
Se hai mai visto il sorgente di una pagina web, ti sembrerà familiare. Se non
l'hai mai visto, ecco tutto quello che ti serve per leggerlo.
Un documento è fatto di *elementi*. Un elemento comincia con un nome tra
parentesi angolari, `<h1>`, finisce con lo stesso nome e una barra, `</h1>`, e
tutto quello che sta tra i due è al suo interno. Alcuni elementi non hanno
niente dentro e si chiudono da soli con una barra alla fine, come
l'elemento `<input .../>` qui sopra. Gli elementi si annidano, e il rientro mostra
l'annidamento nello stesso modo in cui lo mostra in Lua.
Le parole dentro il tag di apertura sono *attributi*, scritti `name="value"`.
Due ti interessano più degli altri. `id` dà a un elemento un nome con cui il tuo
script può trovarlo, ed è così che Lua e il documento si parlano. `class` mette
l'elemento in un gruppo a cui il foglio di stile può mirare; `panel`, `muted` e
`list` sono gruppi che il foglio di stile del motore conosce già.
Il linguaggio di markup si chiama RML e il linguaggio degli stili si chiama
RCSS. Sono di RmlUi, non di Singe. Il capitolo sulla GUI del Riferimento rimanda
alla documentazione di RmlUi per l'elenco completo di quello che puoi scrivere,
e prima o poi ti servirà. Tutto quello che c'è in questa lezione funziona anche
senza.
[#_show_it]
=== Mostralo
Torna allo script. Sopra `onOverlayUpdate`, aggiungi:
[source,lua]
----
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
local page = guiLoad(gui, DIR .. "pause.rml")
----
`guiNew` crea una *GUI*: un rettangolo di dimensione fissa in cui i documenti
vengono impaginati e disegnati. Restituisce un handle, come fanno `fontLoad` e
`spriteLoad`. Farla grande quanto l'overlay vuol dire che un pixel del documento
è un pixel del tuo overlay, e così i conti che fai a mente restano semplici.
`guiLoad` legge un documento dentro quella GUI e restituisce un secondo handle,
per il documento stesso. Quasi tutte le altre chiamate li vogliono entrambi: la
GUI e il documento che contiene. Una GUI può contenere fino a trentadue
documenti -- una pagina del titolo, una pagina delle opzioni e una pagina di
game over possono stare in una sola -- e possono esistere fino a sedici GUI
contemporaneamente.
Sullo schermo non c'è ancora niente, perché caricare un documento non lo
disegna. Aggiungi una riga a `onOverlayUpdate`, subito prima del `return`:
[source,lua]
----
guiDraw(gui)
----
Eseguilo. Il pannello è lì, sopra l'astronave, con il suo titolo, i suoi tre
pulsanti e il suo slider, e non hai scritto nemmeno una riga di codice di
disegno.
`guiDraw` compone la GUI sopra l'overlay *solo per questo fotogramma*, come ogni
altra chiamata di disegno, quindi va in `onOverlayUpdate` e va chiamata di
nuovo al fotogramma successivo. Il punto in cui la metti decide che cosa sta
sopra: tutto quello che hai disegnato prima sta sotto, e tutto quello che
disegni dopo le va sopra. Un mirino disegnato dopo `guiDraw` non viene mai
nascosto dal menu.
[#_let_the_player_use_it]
=== Lascia che il giocatore lo usi
Fai clic su un pulsante. Non succede niente, e il pulsante non si illumina
nemmeno.
Una GUI nuova è un'immagine, non un pannello di controllo. È voluto: un HUD o un
cartello non devono inghiottire il pulsante di fuoco. Per fare in modo che una
pagina riceva l'input:
[source,lua]
----
guiSetInput(gui, true)
----
Adesso il mouse funziona, i pulsanti si illuminano sotto il puntatore, e con i
tasti freccia ti sposti tra i controlli, perché il documento l'ha chiesto con
`nav: auto`. Su un controller o una levetta le quattro direzioni arrivano
come tasti freccia, il primo pulsante d'azione arriva come Invio, e il secondo
arriva come Esc. Un cabinato con un joystick e due pulsanti usa questa pagina
senza sapere che è un documento.
Del controllo su cui stanno lavorando i tasti si dice che ha il *focus*, e un
pulsante con il focus porta un bordo dorato. Il focus parte da Resume perché
quel pulsante dice `autofocus`: ogni volta che la pagina viene mostrata, il
focus va al controllo segnato così. Toglilo e i tasti freccia non hanno nessun
punto da cui partire finché il giocatore non fa clic su qualcosa, il che, su un
cabinato senza mouse, vuol dire mai.
Fare clic però continua a non fare niente, perché nessuno sta ascoltando.
[#_talk_to_it]
=== Parlagli
Tre chiamate collegano il documento al tuo script, e da sole fanno quasi tutto.
[source,lua]
----
guiSetHandler(gui, page, "resume", "click", function()
closeMenu()
end)
----
`guiSetHandler` dice: quando l'elemento con questo `id` fa questa cosa, chiama
questa funzione. Questo `id` è quello scritto nel documento. Il nome dell'evento è di RmlUi:
`"click"` per i pulsanti, `"change"` per qualsiasi cosa il giocatore regoli, e
un lungo elenco di altri che puoi cercare quando ti servono. Viene tenuta una
funzione per elemento e per evento, quindi impostarne un'altra sostituisce la
precedente, e passare `nil` al posto di una funzione la rimuove.
La tua funzione viene chiamata con cinque argomenti -- la GUI, il documento,
l'id, l'evento e il valore attuale dell'elemento come stringa. Prendine quanti
ne vuoi e ignora il resto, come già ignori gli argomenti in Lua.
[source,lua]
----
guiSetHandler(gui, page, "speed", "change", function(g, d, id, event, value)
speed = math.floor(tonumber(value))
end)
----
Lo slider consegna il suo valore come stringa, e RmlUi formatta i numeri a modo
suo, quindi `4` arriva come `"4.000000"`. `tonumber` trasforma la stringa in un
numero e `math.floor` butta via la parte decimale. Saltare questo passaggio e
usare la stringa come numero è un errore che farai una volta.
[source,lua]
----
guiSetValue(gui, page, "score", string.format("%06d", score))
----
`guiSetValue` scrive dentro un elemento. Per un controllo di un modulo imposta
il valore del controllo; per qualsiasi altra cosa -- un paragrafo, un `div`, lo
`<span id="score">` nel tuo documento -- sostituisce quello che c'è dentro
l'elemento, e la pagina si reimpagina al fotogramma successivo. `guiGetValue` è
la stessa chiamata al contrario, e risponde `nil` per un id che nessun elemento
ha, così puoi chiedere di un elemento che potrebbe non esserci senza morire.
Ecco tutta l'API di tutti i giorni: `guiSetHandler`, `guiSetValue`,
`guiGetValue`. C'è molto di più a disposizione tramite gli oggetti Lua di RmlUi,
raggiungibili dalla variabile globale `rmlui`, e il capitolo sulla GUI del
riferimento mostra come. Farai molta strada prima di averne bisogno.
[#_hide_it_again]
=== Nascondilo di nuovo
Un menu di pausa non sta sempre aperto. `guiHide` toglie un documento dallo
schermo e `guiShow` lo rimette, conservando tutto quello che il giocatore ha
scritto o scelto.
La pausa vera e propria è `singeSetPauseFlag(true)`, che ferma il disco, i video
e i suoni ma *continua a chiamare le tue callback*, così puoi ancora disegnare.
Il motore ha un suo tasto di pausa, che congela completamente lo script, e i due
si darebbero battaglia; `singeSetPauseKeyEnabled(false)` toglie quel tasto al
motore, così `SWITCH_PAUSE` arriva a `onInputPressed` come qualsiasi altro
interruttore. Sulla tastiera quel tasto è *P*.
[source,lua]
----
local function closeMenu()
paused = false
singeSetPauseFlag(false)
guiHide(gui, page)
guiSetInput(gui, false)
end
local function openMenu()
paused = true
singeSetPauseFlag(true)
guiSetValue(gui, page, "score", string.format("%06d", score))
guiShow(gui, page)
guiSetInput(gui, true)
end
----
Spegnere l'input insieme alla pagina non è un abbellimento. Una pagina nascosta
che riceve ancora l'input si mangia i tasti freccia del giocatore per il resto
della partita.
[#_what_the_page_does_not_use_you_still_get]
=== Quello che la pagina non usa arriva comunque a te
Questa è la regola che rende sicuro lasciare una GUI aperta durante il gioco, e
vale la pena dirla chiaramente: *un evento usato da un elemento non raggiunge
mai le tue callback.* Un Invio che ha premuto il pulsante con il focus non
arriva come `onInputPressed`. Una freccia che ha spostato il focus non arriva
come `SWITCH_LEFT`. Un clic su un pulsante non è un interruttore del mouse.
Tutto quello che la pagina non ha usato passa dritto come al solito. È per
questo che `P` chiude questo menu: nessun elemento del documento usa `P`, quindi
il tasto arriva a `onInputPressed` esattamente come quando il menu è chiuso. Ed
è anche per questo che un HUD che riceve l'input lascia comunque passare il
pulsante di fuoco.
Un tasto non segue la regola. RmlUi usa Tab per spostare il focus e non lo
restituisce mai, quindi Singe passa Tab a una GUI solo mentre si sta davvero
scrivendo in un campo di testo. Un gioco che assegna Tab a qualcosa se lo tiene.
[#_style_it]
=== Dagli uno stile
Riguarda il `<head>` del documento. Una riga fa la maggior parte del lavoro:
[source,html]
----
<link type="text/rcss" href="Singe/gui.rcss"/>
----
È il tema che il motore fornisce, ed è per questo che il tuo pannello ha un
bordo, una sfumatura e un titolo dorato senza che tu abbia scelto niente di
tutto ciò. Senza un foglio di stile RmlUi non disegna quasi niente, perché ogni
elemento parte in linea e grande quanto il suo contenuto, quindi collegare
prima questo e sovrascrivere quello che non ti piace è la strada più rapida
verso una pagina che sembri qualcosa.
Il tema ti dà `.panel` (un riquadro con bordo e angoli arrotondati), `.list` (un
riquadro che scorre, le cui righe `div` figlie si evidenziano e prendono il
focus), `.muted` (testo più tenue per i suggerimenti), i titoli, e ogni
controllo dei moduli con i suoi stati di mouse sopra, focus e pressione. Il
capitolo sulla GUI del Riferimento li elenca.
Tutto quello che viene dopo il collegamento è tuo. Il blocco `<style>` in
`pause.rml` sovrascrive il tema per questa pagina, con le regole successive che
vincono su quelle precedenti, e una regola che mira a un `id` batte una che mira
a una `class`. Per un vero gioco metti le tue regole in un file `.rcss` accanto
al documento e collegalo per secondo, così il tuo aspetto sta in un solo file
per tutte le pagine.
L'unità `dp` è un pixel della dimensione propria della GUI -- la tua GUI è
grande quanto l'overlay, quindi `380dp` sono 380 pixel dell'overlay.
`margin: 80dp auto` è il trucco che centra un blocco di larghezza nota: `auto`
divide quello che avanza in parti uguali tra i due lati. Niente misure e niente
conti, cosa che dopo la diciannovesima lezione potresti trovare un po'
irritante.
Per usare un tuo font in un documento, caricalo con `guiLoadFont` prima dei
documenti che lo usano, e chiamalo in RCSS con il nome della famiglia scritto
dentro il file del font, non con il nome del file.
[#_when_not_to_use_any_of_this]
=== Quando non usare niente di tutto questo
Una GUI è una pagina. È la risposta giusta per un menu, una schermata delle
opzioni, una tabella dei punteggi record, una schermata di game over, o un HUD
con un'impaginazione vera.
È la risposta sbagliata per un punteggio nell'angolo. Quello sono due righe di
`fontPrint` della diciannovesima lezione, e avvolgerlo in un documento non ti
dà niente e ti costa una texture. Lo stesso vale per un mirino, una barra della
salute disegnata con `overlayBox` e una riga di testo di debug.
Tre fatti decidono per te.
Una GUI ha bisogno di una GPU. Su una macchina che non ce l'ha -- un vecchio
computer, un Pi piccolo, una macchina senza alcun driver Vulkan, Direct3D 12 o
Metal -- non ci sono GUI, e `guiNew` non risponde educatamente `false`: termina
il tuo gioco. La scena 3D della ventiduesima lezione fa lo stesso, quindi non è
l'ultima volta che te lo chiederai. Chiedi prima a `singeHasGpu()` se il tuo
gioco deve funzionare in entrambi i posti, e tieni pronta una versione semplice
sull'overlay su cui ripiegare. Il menu del motore fa proprio così.
Una GUI costa una texture e un passaggio di impaginazione *a ogni fotogramma,
che tu la disegni o no*. Una GUI con cui hai finito va eliminata con
`guiDelete`, non semplicemente nascosto. Nascondere serve per una pagina che
mostrerai di nuovo tra un minuto.
E un ricaricamento smonta le GUI. `F5`, `-R` e qualsiasi cosa riavvii lo script
si portano via tutte le GUI, e lo script le ricrea quando riparte. Va bene così,
purché tu le costruisca nel corpo dello script e non in qualche angolo che
viene eseguito una volta sola.
[#_the_whole_script_2]
=== Lo script completo
[source,lua]
----
dofile("Singe/Framework.singe")
overlaySetResolution(discGetWidth(), discGetHeight())
singeSetPauseKeyEnabled(false)
local SHIP_Y = 300
local ship = spriteLoad(DIR .. "art/spaceship.png")
local hudFont = fontLoad("Singe/FreeSansBold.ttf", 20)
local shipX = 0
local speed = 2
local score = 0
local paused = false
local gui = nil
local page = nil
local function say(text)
if gui ~= nil then
guiSetValue(gui, page, "status", text)
end
end
local function closeMenu()
paused = false
singeSetPauseFlag(false)
if gui ~= nil then
guiHide(gui, page)
guiSetInput(gui, false)
end
end
local function openMenu()
paused = true
singeSetPauseFlag(true)
if gui ~= nil then
guiSetValue(gui, page, "score", string.format("%06d", score))
guiShow(gui, page)
guiSetInput(gui, true)
end
end
if singeHasGpu() then
gui = guiNew(overlayGetWidth(), overlayGetHeight())
page = guiLoad(gui, DIR .. "pause.rml")
guiHide(gui, page)
guiSetInput(gui, false)
guiSetHandler(gui, page, "resume", "click", function()
closeMenu()
end)
guiSetHandler(gui, page, "restart", "click", function()
score = 0
shipX = 0
closeMenu()
end)
guiSetHandler(gui, page, "quit", "click", function()
singeQuit()
end)
guiSetHandler(gui, page, "speed", "change", function(g, d, id, event, value)
speed = math.floor(tonumber(value))
say("Speed " .. speed)
end)
end
function onInputPressed(what)
if what == SWITCH_PAUSE then
if paused then
closeMenu()
else
openMenu()
end
end
end
function onOverlayUpdate()
overlayClear()
if not paused then
shipX = shipX + speed
if shipX > overlayGetWidth() then
shipX = -spriteGetWidth(ship)
score = score + 10
end
end
spriteDraw(ship, shipX, SHIP_Y)
fontSelect(hudFont)
colorForeground(255, 255, 255)
fontPrint(16, 16, string.format("SCORE %06d", score))
fontPrint(16, 40, "P pauses")
if paused then
if gui ~= nil then
guiDraw(gui)
else
colorForeground(255, 211, 90)
fontPrint(16, 70, "PAUSED -- no GPU, so no menu")
end
end
return OVERLAY_UPDATED
end
function onShutdown()
if gui ~= nil then
guiDelete(gui)
end
spriteUnload(ship)
fontUnload(hudFont)
end
----
[#_what_just_happened_16]
=== Cos'è appena successo
[source,lua]
----
if singeHasGpu() then
gui = guiNew(overlayGetWidth(), overlayGetHeight())
page = guiLoad(gui, DIR .. "pause.rml")
----
Tutta la GUI viene costruita dentro quel blocco `if`, e `gui` resta `nil` su una
macchina che non ha una GPU. Ogni chiamata che tocca la GUI è protetta da
`if gui ~= nil`, e la pausa funziona anche senza menu. Sono quattro righe in
più, e sono la differenza tra «non ha un menu su quella macchina» e «non parte
su quella macchina».
[source,lua]
----
guiHide(gui, page)
guiSetInput(gui, false)
----
`guiLoad` mostra il documento che ha caricato, quindi una pagina che deve
partire nascosta va nascosta subito. Per una GUI nuova l'input è già spento, ma
spegnerlo qui vuol dire che `openMenu` e `closeMenu` sono esattamente l'una lo
specchio dell'altra, e le coppie speculari sono molto più difficili da
sbagliare.
[source,lua]
----
guiSetHandler(gui, page, "restart", "click", function()
score = 0
shipX = 0
closeMenu()
end)
----
L'handler è una funzione senza nome, scritta dove viene usata. Lua ti lascia
scrivere `function(...) ... end` ovunque serva un valore, e siccome
`guiSetHandler` vuole una funzione, puoi dargliene una sul momento invece di
darle prima un nome e poi passare il nome. Raggiunge `score`, `shipX` e
`closeMenu` perché sono
dichiarati sopra di lei nello stesso file, ed è proprio per questo che gli
handler vengono impostati sotto le cose che toccano.
[source,lua]
----
if not paused then
shipX = shipX + speed
----
L'indicatore di pausa ferma il disco e i suoni, non i tuoi conti.
`onOverlayUpdate` continua a essere chiamata -- è proprio questo il punto,
perché altrimenti il menu non potrebbe essere disegnato -- quindi qualsiasi cosa
tua debba fermarsi durante la pausa, la fermi tu.
[#_try_it_20]
=== Prova tu
. *Aggiungi un quarto pulsante.* Copia la riga `<button id="quit">`, dalle un
nuovo id e un nuovo testo, e aggiungi un handler per il nuovo pulsante. Nota
che non c'è stato niente da rimisurare o da spostare.
. *Cambiagli stile.* Nel blocco `<style>`, cambia `#frame` in
`width: 260dp; margin: 20dp;` e guarda il pannello spostarsi e il testo
andare a capo di nuovo da solo.
. *Togli il `guiSetInput(gui, true)` in `openMenu`.* La pagina compare ancora.
Capisci, solo guardando lo schermo, che cosa hai perso.
. *Lascia la pagina sempre visibile.* Cancella le chiamate a `guiHide`, cambia
entrambe le chiamate `guiSetInput(gui, false)` in `true` e disegna la GUI a
ogni fotogramma invece che solo durante la pausa. Poi premi i tasti freccia e
Invio mentre l'astronave vola. La pagina se li prende per sé: il focus si
sposta, lo slider cambia la velocità, e Invio preme il pulsante che ha il
focus. Un gioco che si guidasse con quei tasti non li sentirebbe mai. È per
questo che un HUD è `guiSetInput(gui, false)`.
. *Metti il punteggio sulla pagina invece che nell'HUD.* Chiama `guiSetValue`
per `score` a ogni fotogramma invece che solo in `openMenu`, e cancella la
riga `fontPrint`. Poi decidi quale dei due preferisci davvero, e perché.
[#_break_it_on_purpose_19]
=== Rompilo apposta
Cambia un id in `pause.rml`, da `id="resume"` a `id="continue"`, e lascia stare
lo script. Singe parte e muore:
----
52:guiSetHandler: No element "resume" in document 0 of GUI 0.
----
Leggilo come leggi qualsiasi errore del motore. La riga, la chiamata che si è
lamentata, poi la lamentela -- e questa lamentela è precisa. Non sta dicendo
che il documento è rotto; il documento si è caricato perfettamente. Sta dicendo
che hai chiesto un elemento con un nome che lì dentro non ha nessuno.
`guiSetHandler` e `guiSetValue` muoiono entrambe se manca un id, perché un
handler che volevi collegare e non hai collegato è un bug di cui vuoi sapere
subito. `guiGetValue` è l'eccezione: risponde `nil`, così puoi chiedere se una
cosa c'è.
L'altro errore che vale la pena incontrare è quello che ottieni quando non si
carica il documento stesso. Rinomina `pause.rml` ed esegui. Stavolta Singe
nomina il file e cita il motivo dato da RmlUi, e i brontolii di RmlUi sul tuo
markup e sui tuoi stili finiscono nella console che il caricamento riesca o no.
Leggi quella console la prima volta che una pagina esce vuota; di solito ti avrà
già detto il perché.
[#_what_you_learned_20]
=== Cosa hai imparato
* Una GUI è un rettangolo di dimensione fissa in cui i documenti vengono
impaginati e disegnati; `guiNew` ne crea una e `guiDelete` la libera.
* `guiLoad` mette un documento RML in una GUI e lo mostra; `guiHide`, `guiShow`
e `guiClose` lo gestiscono in seguito.
* `guiDraw` compone la GUI sopra l'overlay per un fotogramma, quindi va in
`onOverlayUpdate` come ogni altra chiamata di disegno.
* Una GUI nuova si limita a mostrare; è `guiSetInput` a renderla utilizzabile.
* `guiSetHandler` esegue la tua funzione quando un elemento viene cliccato o
cambiato, `guiSetValue` scrive dentro un elemento e `guiGetValue` ne legge
uno.
* Un evento usato da un elemento non raggiunge mai le tue callback; tutto il
resto passa come al solito.
* `Singe/gui.rcss` è il tema fornito con il motore; collegalo per primo e
sovrascrivilo in un blocco `<style>` o in un foglio tutto tuo.
* Una GUI ha bisogno di una GPU, quindi chiedi a `singeHasGpu()` se il tuo gioco
deve funzionare anche senza.
* Ogni GUI viene elaborata a ogni fotogramma, che tu la disegni o no, quindi
elimina quelle con cui hai finito.
* Un punteggio nell'angolo sono due righe di `fontPrint`, non un documento.
[#_next_time_20]
=== La prossima volta
C'è ancora un tipo di testo sullo schermo, e ha un sistema tutto suo per un
motivo che non ha niente a che fare con il suo aspetto. La ventunesima lezione
parla di sottotitoli, e dei giocatori che senza sottotitoli non possono giocare
al tuo gioco.