singe/docs/it/lessons/15-threaded.adoc

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.