[#_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]
----
Pause
Paused
Score 000000
Arrow keys move, Return chooses.
----
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, `
`, finisce con lo stesso nome e una barra, `
`, 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 `` 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
`` 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 `` del documento. Una riga fa la maggior parte del lavoro:
[source,html]
----
----
È 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 `