628 lines
23 KiB
Text
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.
|