432 lines
15 KiB
Text
432 lines
15 KiB
Text
[#_lesson_15_the_other_way_to_write_it]
|
|
== Lezione 15: L'altro modo di scriverlo
|
|
|
|
image::learn/15-threaded.png[La lezione finita, 480]
|
|
|
|
Tutte le lezioni finora sono state scritte allo stesso modo. Scrivi funzioni il
|
|
cui nome comincia con `on`, Singe le chiama quando gli pare, e tu non dici mai
|
|
in che ordine succede qualcosa. Questo è il modello *guidato dagli eventi*, ed
|
|
è così che funziona la maggior parte dei motori di gioco.
|
|
|
|
Singe ha un secondo modello. Con questo scrivi il tuo gioco come un elenco di
|
|
passi, dalla prima riga all'ultima, e hai il permesso di aspettare. In questa
|
|
lezione scriverai la stessa breve sequenza video in tutti e due i modi e vedrai
|
|
la differenza con i tuoi occhi.
|
|
|
|
[#_the_sequence]
|
|
=== La sequenza
|
|
|
|
Ecco che cosa fa il gioco, in italiano:
|
|
|
|
. Fai partire il filmato dall'inizio e riproducilo fino al fotogramma 120.
|
|
. Fermati lì e chiedi al giocatore di premere la barra spaziatrice.
|
|
. Quando la preme, continua a riprodurre fino al fotogramma 300.
|
|
. Fermati lì, di' «The end.», aspetta quattro secondi ed esci.
|
|
|
|
Quattro passi. Rileggili e nota che ognuno contiene la parola «poi», detta o
|
|
sottintesa.
|
|
|
|
[#_the_way_you_know]
|
|
=== Il modo che conosci
|
|
|
|
Crea un file chiamato `events.singe` nella cartella `movie` della
|
|
quattordicesima lezione, e scrivi questo:
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
|
|
local step = 1
|
|
local endedAt = 0
|
|
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 and step == 2 then
|
|
step = 3
|
|
discPlay()
|
|
end
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
local frame = discGetFrame()
|
|
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Frame " .. frame)
|
|
|
|
if step == 1 then
|
|
if frame >= 120 then
|
|
discPause()
|
|
step = 2
|
|
end
|
|
elseif step == 2 then
|
|
overlayPrint(2, 4, "Press the space bar to go on.")
|
|
elseif step == 3 then
|
|
if frame >= 300 then
|
|
discPause()
|
|
endedAt = singeGetTicks()
|
|
step = 4
|
|
end
|
|
elseif step == 4 then
|
|
overlayPrint(2, 4, "The end.")
|
|
if singeGetTicks() - endedAt > 4000 then
|
|
singeQuit()
|
|
end
|
|
end
|
|
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
discSearch(0)
|
|
discPlay()
|
|
----
|
|
|
|
Eseguilo. Dare il nome della cartella avvierebbe `movie.singe`, quindi dai
|
|
invece il nome dello script, come ti ha mostrato la prima lezione:
|
|
|
|
----
|
|
Singe -R -v Singe/menuBackground.mkv movie/events.singe
|
|
----
|
|
|
|
Funziona. Fa esattamente i quattro passi. `singeGetTicks` è nuova -- restituisce
|
|
il numero di millisecondi trascorsi dall'avvio del motore, che è il modo normale
|
|
di misurare un intervallo di tempo -- e `singeQuit` termina il gioco, che è
|
|
quello che il motore fa per te quando premi Esc.
|
|
|
|
Ora leggi lo script e prova a trovarci i quattro passi. Ci sono, ma rivoltati
|
|
come un calzino. L'ordine della storia è scritto nei *valori* di `step`, non
|
|
nell'ordine delle righe, e `step` viene impostata in una funzione e letta in
|
|
un'altra. Per rispondere a «che cosa succede dopo che il giocatore preme
|
|
spazio?» devi tenerti in testa tutto quanto in una volta.
|
|
|
|
E questa è la versione facile. Dentro ci sono due decisioni.
|
|
|
|
[#_the_other_way]
|
|
=== L'altro modo
|
|
|
|
Crea un nuovo file, `steps.singe`, e scrivi questo:
|
|
|
|
[source,lua]
|
|
----
|
|
local message = ""
|
|
local pressed = false
|
|
|
|
|
|
local function drawFrame()
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Frame " .. discGetFrame())
|
|
overlayPrint(2, 4, message)
|
|
end
|
|
|
|
|
|
local function playTo(frame)
|
|
while discGetFrame() < frame do
|
|
drawFrame()
|
|
singeYield()
|
|
end
|
|
end
|
|
|
|
|
|
local function waitForButton()
|
|
pressed = false
|
|
while not pressed do
|
|
drawFrame()
|
|
singeYield()
|
|
end
|
|
end
|
|
|
|
|
|
local function waitSeconds(seconds)
|
|
local stopTime = singeGetTicks() + seconds * 1000
|
|
|
|
while singeGetTicks() < stopTime do
|
|
drawFrame()
|
|
singeYield()
|
|
end
|
|
end
|
|
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 then
|
|
pressed = true
|
|
end
|
|
end
|
|
|
|
|
|
function singeMain()
|
|
discSearch(0)
|
|
discPlay()
|
|
playTo(120)
|
|
discPause()
|
|
|
|
message = "Press the space bar to go on."
|
|
waitForButton()
|
|
|
|
message = ""
|
|
discPlay()
|
|
playTo(300)
|
|
discPause()
|
|
|
|
message = "The end."
|
|
waitSeconds(4)
|
|
end
|
|
|
|
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
|
|
Eseguilo allo stesso modo, finendo con `movie/steps.singe`. Fa le stesse
|
|
quattro cose.
|
|
|
|
Ora leggi `singeMain`. I quattro passi sono i quattro paragrafi, in ordine,
|
|
dall'alto in basso, e non c'è nessuna variabile `step` da nessuna parte, perché
|
|
il punto in cui sei nella storia *è* il punto in cui sei nella funzione. Questo
|
|
è tutto il senso della lezione.
|
|
|
|
[#_what_just_happened_11]
|
|
=== Cos'è appena successo
|
|
|
|
[source,lua]
|
|
----
|
|
function singeMain()
|
|
----
|
|
|
|
`singeMain` è un nome che Singe cerca, come `onOverlayUpdate`. Se il tuo script
|
|
la definisce, il framework esegue il tuo gioco a partire da lì: viene chiamata
|
|
una volta, all'inizio, e il tuo gioco dura quanto dura. Quando la funzione
|
|
finisce, il gioco si chiude.
|
|
|
|
[source,lua]
|
|
----
|
|
singeYield()
|
|
----
|
|
|
|
Questa è la chiamata che rende tutto possibile, e vuol dire «lascia che passi un
|
|
fotogramma».
|
|
|
|
Singe non sta facendo due cose contemporaneamente -- qui niente gira in
|
|
parallelo, qualunque cosa suggerisca la parola *threaded*. Quello che succede è
|
|
che `singeYield` ferma la tua funzione nel punto in cui si trova, restituisce il
|
|
controllo al motore e si ricorda il punto esatto. Il motore disegna il
|
|
fotogramma, legge i comandi, fa avanzare il video, e poi fa ripartire la tua
|
|
funzione dalla riga dopo il `singeYield`, con ogni variabile esattamente come
|
|
l'avevi lasciata.
|
|
|
|
Quindi un ciclo `while` con dentro un `singeYield` è un modo di aspettare. È
|
|
l'unico modo di aspettare, e devi metterne uno in ogni ciclo che sta aspettando
|
|
qualcosa. È un patto che fai con il motore: puoi startene seduto ad aspettare,
|
|
purché gli restituisca il fotogramma sessanta volte al secondo.
|
|
|
|
[source,lua]
|
|
----
|
|
local function playTo(frame)
|
|
while discGetFrame() < frame do
|
|
drawFrame()
|
|
singeYield()
|
|
end
|
|
end
|
|
----
|
|
|
|
Singe non ha una funzione «aspetta finché il video arriva al fotogramma 120».
|
|
Non gliene serve una, perché puoi scriverla in quattro righe, e l'hai appena
|
|
fatto. Continua finché il disco non è arrivato al fotogramma che vuoi;
|
|
restituisci un fotogramma a ogni giro.
|
|
|
|
`waitForButton` ha la stessa forma con una domanda diversa, e `waitSeconds` ha
|
|
di nuovo la stessa forma, con un orologio. Tre piccole funzioni, scritte una
|
|
volta sola, e grazie a loro `singeMain` si legge come le frasi in italiano
|
|
all'inizio della lezione. È proprio a questo che, come diceva la quinta
|
|
lezione, servono le funzioni tutte tue.
|
|
|
|
[source,lua]
|
|
----
|
|
drawFrame()
|
|
singeYield()
|
|
----
|
|
|
|
Ogni ciclo di attesa disegna prima di cedere il controllo, ed è questa la parte
|
|
che sorprende la gente. Nel modello a thread non c'è una `onOverlayUpdate` tua,
|
|
quindi niente viene disegnato se non lo disegni tu. Quello che disegni prima di
|
|
un `singeYield` è ciò che compare in quel fotogramma. Smetti di disegnare e
|
|
l'overlay smette di cambiare.
|
|
|
|
Ecco perché esiste `message`. `singeMain` la imposta, `drawFrame` la stampa, e i
|
|
tre cicli di attesa chiamano tutti `drawFrame`. È un prezzo piccolo per riavere
|
|
la storia in ordine.
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
|
|
In *fondo*. Questo conta più di qualsiasi altra cosa nella lezione, quindi vale
|
|
la pena sapere perché.
|
|
|
|
`dofile` esegue un altro script dentro il tuo, e una delle cose che fa il
|
|
framework mentre viene eseguito è cercare una funzione chiamata `singeMain`. Se
|
|
ne trova una, costruisce il tuo gioco intorno a lei e scrive una
|
|
`onOverlayUpdate` tutta sua, il cui unico compito è far ripartire la tua
|
|
`singeMain` una volta per fotogramma. Se non la trova, non fa niente del genere,
|
|
e il tuo gioco è un normale gioco guidato dagli eventi.
|
|
|
|
Quindi il framework va caricato *dopo* che `singeMain` esiste. Caricalo in cima,
|
|
come hai fatto in ogni lezione dalla settima in poi, e nel momento in cui guarda
|
|
non c'è ancora nessuna `singeMain`, e ottieni un gioco senza il gioco dentro.
|
|
|
|
[#_the_rules]
|
|
=== Le regole
|
|
|
|
Sono quattro, e insieme coprono ogni errore che puoi fare qui.
|
|
|
|
*Il framework va per ultimo.* Per il motivo appena visto.
|
|
|
|
*Non scrivere una tua `onOverlayUpdate`.* Il framework ne ha già scritta una, e
|
|
ce ne può essere una sola: vince quella scritta per ultima, e l'altra viene
|
|
buttata via senza una parola. Se la tua sta sopra il `dofile`, sparisce la tua e
|
|
i tuoi disegni non compaiono mai. Se sta sotto, sparisce quella del framework e
|
|
la tua `singeMain` non viene più eseguita. Disegna invece dall'interno di
|
|
`singeMain`.
|
|
|
|
*Ogni ciclo che aspetta ha bisogno di un `singeYield`.* Un ciclo senza non
|
|
restituisce mai il fotogramma. La finestra smette di ridisegnarsi, smette di
|
|
rispondere, e il gioco è andato; per uscirne devi chiudere il terminale. Non
|
|
c'è nessun messaggio di errore, perché dal punto di vista del motore la tua
|
|
funzione non ha ancora finito.
|
|
|
|
*Quando `singeMain` finisce, il gioco si chiude.* Il che è comodo quando lo
|
|
vuoi e una sorpresa quando non lo vuoi. Un gioco che deve andare avanti per
|
|
sempre finisce con un ciclo da cui non si esce mai:
|
|
|
|
[source,lua]
|
|
----
|
|
while true do
|
|
drawFrame()
|
|
singeYield()
|
|
end
|
|
----
|
|
|
|
[#_input_still_arrives]
|
|
=== I comandi arrivano lo stesso
|
|
|
|
Guarda di nuovo dove viene impostata `pressed`:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 then
|
|
pressed = true
|
|
end
|
|
end
|
|
----
|
|
|
|
Quella è una callback, in uno script scritto come un elenco di passi. I due
|
|
modelli sono in funzione insieme, ed è voluto.
|
|
|
|
Quando esiste `singeMain`, ti viene tolta solo `onOverlayUpdate`. Tutte le
|
|
altre -- `onInputPressed`, `onInputReleased`, `onKeyPressed`,
|
|
`onSoundCompleted`, `onShutdown`, tutte quante -- vengono chiamate esattamente
|
|
come prima, negli intervalli tra uno yield e l'altro. Quindi il modo normale di
|
|
scrivere un gioco con questo modello è quello che hai appena usato: la storia in
|
|
`singeMain`, e una piccola callback che coglie un evento e lascia un biglietto
|
|
che la storia troverà.
|
|
|
|
`waitForButton` imposta `pressed` a `false` prima di cominciare ad aspettare, e
|
|
così butta via qualunque cosa il giocatore abbia pestato prima. Togli quella
|
|
riga e un pulsante premuto durante il video conta come la risposta a una
|
|
domanda che non è ancora stata fatta.
|
|
|
|
[#_which_one_to_use]
|
|
=== Quale usare
|
|
|
|
Nessuno dei due modelli è il migliore. Servono a lavori diversi, e un vero gioco
|
|
usa quello che va meglio.
|
|
|
|
Il modello a thread è adatto a tutto ciò che è una *sequenza*. Riproduci
|
|
questo, poi chiedi, poi riproduci quello. Un'introduzione. Un filmato di
|
|
intermezzo. Un tutorial. Uno scontro con un boss in tre fasi. Qualunque cosa
|
|
descriveresti spontaneamente come un elenco numerato è una `singeMain` che
|
|
aspetta di essere scritta, e il video è la cosa più sequenziale che ci sia, ed
|
|
è per questo che la terza parte ci arriva qui.
|
|
|
|
Il modello guidato dagli eventi è adatto a tutto ciò che è una *situazione*.
|
|
Dieci nemici e un giocatore, tutti in movimento, tutti che si scontrano, nessuno
|
|
in un ordine preciso. Il gioco che hai scritto nella settima lezione non
|
|
contiene nessuna sequenza: ogni fotogramma è la stessa domanda fatta di nuovo,
|
|
che è esattamente la forma di `onOverlayUpdate`. Scrivilo come un elenco di
|
|
passi e non guadagni niente, e perdi in chiarezza.
|
|
|
|
Quando un gioco è tutte e due le cose -- e quasi tutti lo sono -- si scrive in
|
|
tutti e due i modi. `singeMain` attraversa i livelli in ordine, e dentro ogni
|
|
livello un ciclo fa il lavoro della settima lezione sessanta volte al secondo,
|
|
finché il livello non finisce.
|
|
|
|
[#_try_it_15]
|
|
=== Prova tu
|
|
|
|
. *Aggiungi un passo.* Dopo `waitForButton`, fai invece tornare indietro il
|
|
filmato: `discSkipToFrame(0)`, poi `playTo(60)`, poi continua come prima. Due
|
|
righe nuove in un posto solo, e nient'altro nello script deve saperlo.
|
|
. *Aggiungi lo stesso passo all'altro.* Ora fallo in `events.singe`. Conta
|
|
quanti punti hai dovuto toccare.
|
|
. *Scrivi `waitFrames`.* Una quarta funzione di aiuto, accanto alle altre tre,
|
|
che aspetta un certo numero di fotogrammi di video invece di un certo numero
|
|
di secondi. Usa `discGetFrame` e ricordati da dove sei partito.
|
|
. *Togli uno yield.* Cancella il `singeYield()` dentro `playTo` ed eseguilo.
|
|
Tieniti pronto a chiudere il terminale, perché nient'altro lo ferma. Ora
|
|
rimettilo e cancella invece `drawFrame()`, e da quello che vedi capisci perché
|
|
il contatore dei fotogrammi si ferma ma il filmato no.
|
|
. *Fanne un ciclo.* Metti tutta `singeMain` dentro `while true do ...
|
|
end` così la sequenza ricomincia invece di uscire. Esc ti fa ancora uscire,
|
|
perché quello non è mai stato compito tuo.
|
|
|
|
[#_break_it_on_purpose_14]
|
|
=== Rompilo apposta
|
|
|
|
Sposta la riga `dofile("Singe/Framework.singe")` dal fondo di `steps.singe` alla
|
|
cima, ed eseguilo.
|
|
|
|
Non c'è nessun errore. Non viene stampato niente. La finestra si apre, il primo
|
|
fotogramma del filmato resta lì, e non succede mai nient'altro.
|
|
|
|
Questo è il tipo di bug peggiore, ed è il motivo per cui vale la pena
|
|
incontrarlo apposta. Dal punto di vista del motore non è andato storto niente:
|
|
il tuo script è stato caricato, il framework è stato caricato, e nessuno dei due
|
|
ha definito una `onOverlayUpdate`, quindi il motore non ha niente da chiamare.
|
|
Il disco è fermo al fotogramma uno, in pausa, esattamente come l'ha lasciato il
|
|
motore, perché il `discPlay` che l'avrebbe fatto partire sta dentro una
|
|
`singeMain` che nessuno esegue mai.
|
|
|
|
Quando un gioco a thread non fa proprio niente, guarda prima l'ultima riga del
|
|
file. È quasi sempre quella.
|
|
|
|
[#_what_you_learned_15]
|
|
=== Cosa hai imparato
|
|
|
|
* Singe ha due modi di scrivere uno script, e scegli definendo `singeMain`
|
|
oppure no.
|
|
* Nel modello a thread il tuo gioco è un elenco di passi e va dall'alto in
|
|
basso.
|
|
* `singeYield()` restituisce un fotogramma al motore e continua dalla riga
|
|
successiva.
|
|
* Aspettare è un ciclo `while` con dentro un `singeYield`. La funzione di attesa
|
|
che ti serve la scrivi tu; il motore non ne fornisce una.
|
|
* Niente viene disegnato se non lo disegni tu, prima di uno yield, a ogni
|
|
fotogramma.
|
|
* `dofile("Singe/Framework.singe")` va alla *fine* di uno script a thread.
|
|
* Non scrivere una tua `onOverlayUpdate` quando esiste `singeMain`.
|
|
* Tutte le altre callback vengono ancora chiamate, quindi eventi e passi si
|
|
mescolano liberamente.
|
|
* Quando `singeMain` finisce, finisce il gioco.
|
|
* Le sequenze vogliono il modello a thread; le situazioni quello guidato dagli
|
|
eventi.
|
|
|
|
[#_next_time_15]
|
|
=== La prossima volta
|
|
|
|
Ora sai scrivere «riproduci questo, poi chiedi, poi riproduci quello» in tre
|
|
righe. Un gioco video a diramazioni non è altro che quella frase, qualche
|
|
centinaio di volte, con le risposte che decidono quale riga viene dopo. La
|
|
sedicesima lezione ne costruisce uno, e la parte interessante si rivela non
|
|
essere affatto il video. È tenere la mappa di che cosa porta dove in una forma
|
|
che riesci ancora a leggere quando il gioco ha quaranta scene.
|