[#_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.