[#_lesson_16_branching] == Lezione 16: Diramazioni image::learn/16-branching.png[La lezione finita, 480] Un gioco video a diramazioni è un film con dei bivi. Una scena va avanti, il giocatore sceglie, e dopo va avanti una scena diversa. Fallo venti o trenta volte e hai un gioco che la gente rigiocherà per anni cercando il finale che non ha ancora visto. In questa lezione ne costruirai uno. Ha quattro scene, due scelte, un timer per ogni scelta, una morte e tre vite. Il codice che lo fa girare è di una trentina di righe, e resterebbe di una trentina di righe anche se il gioco avesse duecento scene -- perché il gioco in sé non sarà affatto scritto nel codice. [#_imagine_it_is_a_dragon] === Immagina che sia un drago Il video che hanno tutti è `Singe/menuBackground.mkv`, gli stessi 420 fotogrammi che guidi dalla quattordicesima lezione. È una palla di fuoco, un drago di carta, la parola `SINGE`, e poi una griglia magenta che scorre via per sempre sotto un sole arancione. Non è un dramma. Non c'è nessuno dentro che possa essere in pericolo, e al tramonto non importa che cosa decidi. Sta' al gioco lo stesso. Per due minuti il drago è abbastanza vero, e tutto quello che stai per scrivere funziona esattamente allo stesso modo quando il filmato è uno stuntman che cade da una scogliera. Ecco la mappa, e i numeri dei fotogrammi sono quelli che hai trovato da solo nella quattordicesima lezione: [cols="1,1,3",options="header"] |=== | Scena | Fotogrammi | Che cosa succede | `arrive` | da 0 a 119 | Fuoco, e il drago atterra. *Sinistra o destra?* | `sunset` | da 120 a 259 | Sputa fuoco. *Ti abbassi a sinistra o corri a destra?* | `pit` | da 260 a 339 | Vieni mangiato. Perdi una vita. | `escape` | da 340 a 410 | La griglia scorre via e ne sei uscito. |=== La sinistra è sempre la scelta giusta, per così dire: a sinistra da `arrive` si va a `sunset`, a sinistra da `sunset` si arriva a `escape`. A destra si finisce nel `pit` tutte e due le volte, e lo stesso succede se ci metti troppo. [#_the_map_is_data_not_code] === La mappa è fatta di dati, non di codice Ecco tutto il gioco, ed è una tabella. [source,lua] ---- local scenes = { arrive = { first = 0, last = 119, ask = 30, prompt = "The dragon lands. Left or Right?", left = "sunset", right = "pit", goesTo = "pit" }, sunset = { first = 120, last = 259, ask = 170, prompt = "It breathes fire. Duck Left or run Right?", left = "escape", right = "pit", goesTo = "pit" }, pit = { first = 260, last = 339, death = true, goesTo = "arrive" }, escape = { first = 340, last = 410 } } ---- Una tabella con dei nomi sulla sinistra invece dei numeri, esattamente come nella sesta lezione, e ogni valore che contiene è un'altra tabella. Ognuna di quelle tabelle interne è un *record*: una scena, descritta campo per campo. `scenes.arrive` è la prima scena. `scenes.arrive.first` vale `0`. `scenes.arrive.left` è la stringa `"sunset"`, che è il nome di un'altra scena nella stessa tabella -- ed è così che si scrivono i bivi. Una scena non contiene la scena che viene dopo. Contiene il *nome* della scena che viene dopo, e il nome viene cercato quando arriva il momento. I campi vogliono dire questo: `first` e `last`:: I fotogrammi tra cui va la scena. `ask`:: Il fotogramma in cui compare la domanda. Il giocatore ha da `ask` a `last` per rispondere, e questo è il timer. `prompt`:: Che cosa dice la domanda. `left` e `right`:: La scena a cui porta ciascuna risposta. `goesTo`:: Dove va la scena quando nessuno ha scelto -- perché il giocatore ha finito il tempo, o perché una domanda non c'era proprio. Per `pit`, che è una morte, è il punto a cui torna il nuovo tentativo. `death`:: Presente, e `true`, solo in una scena che costa una vita. Su quella tabella vale la pena notare tre cose. La prima è che è il documento di progetto. Puoi darla a qualcuno che non ha mai scritto una riga di Lua e ci leggerà dentro il gioco. Quando vuoi che il drago si divida in tre strade, aggiungi un campo. Quando vuoi un'altra scena, aggiungi un altro record. Non vai a cercare il codice che riproduce le scene, perché non lo cambi. La seconda è che i record non hanno tutti la stessa forma. `pit` non ha `ask`, né `prompt`, né `left` -- dentro non c'è niente da scegliere. `escape` non ha `goesTo`, perché è la fine e non porta da nessuna parte. Un campo che non c'è in una tabella si legge come `nil`, che è la parola che hai incontrato nella terza lezione per «lì non c'è niente», e chiedere `scenes.escape.goesTo` non è un errore. È `nil`, e `nil` è una risposta. Il codice che legge questi record deve essere pronto a riceverla, e tra un minuto vedrai l'unica riga che lo è. La terza è quello che la tabella *non* contiene: niente lì dentro disegna, riproduce o aspetta. È una descrizione. Questa separazione è tutto il trucco della lezione. [#_one_function_that_plays_any_scene] === Una funzione che riproduce qualsiasi scena Questa è la metà fatta di codice, e c'è una sola funzione di una certa lunghezza. È scritta con il modello a thread della quindicesima lezione, perché un gioco a diramazioni è la cosa più sequenziale che ci sia. [source,lua] ---- local function playScene(name) local scene = scenes[name] discSkipToFrame(scene.first) while discGetFrame() < scene.last do local frame = discGetFrame() local asking = scene.ask ~= nil and frame >= scene.ask if not asking then answer = nil elseif answer == "left" or answer == "right" then return scene[answer] end drawScene(scene, frame, asking) singeYield() end discPause() if scene.death then lives = lives - 1 if lives == 0 then ending = "The dragon wins. Game over." return nil end waitForGo("Press the space bar to try again.") end return scene.goesTo end ---- Prende il nome di una scena, la riproduce e restituisce il nome della scena che viene dopo. Questo è tutto il suo contratto, e il gioco a quel punto sono tre righe: [source,lua] ---- function singeMain() local scene = "arrive" while scene ~= nil do scene = playScene(scene) end waitForGo(ending .. " Space to quit.") end ---- Comincia da `arrive`. Riproduci la scena che hai in mano, e tieni in mano quella che ti restituisce. Quando ti restituisce `nil` non c'è una scena successiva, il ciclo finisce e la partita è finita. [#_the_whole_thing_8] === Tutto insieme Crea `dragon.singe` nella tua cartella `movie` e scrivici questo. È lo script più lungo del libro finora, e ogni suo pezzo è stato spiegato, tranne le due piccole funzioni di aiuto, che vengono dopo. [source,lua] ---- local FPS = 30 local scenes = { arrive = { first = 0, last = 119, ask = 30, prompt = "The dragon lands. Left or Right?", left = "sunset", right = "pit", goesTo = "pit" }, sunset = { first = 120, last = 259, ask = 170, prompt = "It breathes fire. Duck Left or run Right?", left = "escape", right = "pit", goesTo = "pit" }, pit = { first = 260, last = 339, death = true, goesTo = "arrive" }, escape = { first = 340, last = 410 } } local answer = nil local ending = "You got out alive." local lives = 3 local function drawScene(scene, frame, asking) overlayClear() overlayPrint(2, 2, "Lives: " .. lives) if scene.death then overlayPrint(2, 4, "That did not go well.") elseif asking then overlayPrint(2, 4, scene.prompt) overlayPrint(2, 6, math.ceil((scene.last - frame) / FPS) .. " seconds left.") end end local function waitForGo(text) answer = nil while answer ~= "go" do overlayClear() overlayPrint(2, 2, "Lives: " .. lives) overlayPrint(2, 4, text) singeYield() end end local function playScene(name) local scene = scenes[name] discSkipToFrame(scene.first) while discGetFrame() < scene.last do local frame = discGetFrame() local asking = scene.ask ~= nil and frame >= scene.ask if not asking then answer = nil elseif answer == "left" or answer == "right" then return scene[answer] end drawScene(scene, frame, asking) singeYield() end discPause() if scene.death then lives = lives - 1 if lives == 0 then ending = "The dragon wins. Game over." return nil end waitForGo("Press the space bar to try again.") end return scene.goesTo end function onInputPressed(what) if what == SWITCH_LEFT then answer = "left" elseif what == SWITCH_RIGHT then answer = "right" elseif what == SWITCH_BUTTON1 then answer = "go" end end function singeMain() local scene = "arrive" while scene ~= nil do scene = playScene(scene) end waitForGo(ending .. " Space to quit.") end dofile("Singe/Framework.singe") ---- Eseguilo: ---- Singe -R -v Singe/menuBackground.mkv movie/dragon.singe ---- Giocaci quattro o cinque volte. Va' a sinistra due volte ed esci. Va' a destra e fatti mangiare. Resta con le mani in mano e fatti mangiare lo stesso. Perdi tutte e tre le vite. [#_what_just_happened_12] === Cos'è appena successo [source,lua] ---- discSkipToFrame(scene.first) while discGetFrame() < scene.last do ---- Una scena è un salto seguito da un'attesa, cioè lo schema della quindicesima lezione con i numeri che escono dal record invece di essere scritti a mano. `discSkipToFrame` va a un fotogramma e riproduce da lì, ed è proprio questo un cambio di scena. Il numero del fotogramma è giusto nell'istante in cui la chiamata ritorna, quindi il `while` non pensa mai di essere ancora nella scena che hai appena lasciato. [source,lua] ---- local asking = scene.ask ~= nil and frame >= scene.ask ---- L'unica riga pronta per un campo che manca, e il motivo per cui la scena della morte può fare a meno di `ask`. `and` vuol dire che tutte e due le metà devono essere vere, come nella terza lezione, e Lua controlla prima la metà di sinistra. Se `scene.ask` è `nil` la metà di sinistra è falsa, Lua si ferma lì, e il confronto a destra non avviene mai. Ed è meglio così, perché confrontare `nil` con un numero è un errore, e lo incontrerai alla fine di questa lezione. `asking` è la risposta a «la domanda è sullo schermo in questo momento?», calcolata una volta per fotogramma e usata tre volte. [source,lua] ---- if not asking then answer = nil elseif answer == "left" or answer == "right" then return scene[answer] end ---- Finché la domanda non c'è, qualunque cosa prema il giocatore viene buttata via. Deve esserlo, altrimenti un giocatore che si appoggia sulla freccia sinistra durante il filmato risponde a una domanda che non ha letto. Una volta che la domanda c'è, una risposta fa finire subito la scena. `return scene[answer]` merita una seconda occhiata: `answer` contiene la stringa `"left"` o `"right"`, e `scene["left"]` è la stessa cosa di `scene.left`. Mettere il nome tra parentesi quadre invece che dopo un punto ti permette di cercare un campo il cui nome sta in una variabile. Due righe di codice per tutte e due le risposte, e con cinque risposte sarebbero ancora due righe. Il ramo `elseif` spiega anche perché passano solo `"left"` e `"right"`. La barra spaziatrice imposta `answer` a `"go"`, e `scene["go"]` è `nil`, quindi una barra spaziatrice premuta per sbaglio durante una domanda farebbe finire il gioco in silenzio. Di' che cosa accetti. [source,lua] ---- discPause() ---- Subito dopo il ciclo, ed è facile dimenticarlo. La scena è arrivata al suo ultimo fotogramma, ma il disco non ha idea che la scena sia finita -- va avanti dritto in qualunque filmato si trovi dopo nel file, che in un gioco vero è il mezzo di una scena di tutt'altra parte. Fermalo nell'istante in cui smetti di volerlo. [source,lua] ---- if scene.death then lives = lives - 1 ---- `scene.death` è `nil` per ogni scena tranne `pit`, e in un `if` `nil` conta come falso, quindi questo si legge «se questa scena era una morte». Non devi scrivere `death = false` negli altri tre record, e non dovresti: l'assenza del campo lo dice altrettanto bene, e così c'è un solo posto che decide quali scene sono mortali. Il nuovo tentativo sono le due righe sotto. Togli una vita e, se ne restano, fermati sul fotogramma e aspetta la barra spaziatrice. Poi `return scene.goesTo` rimanda il giocatore ad `arrive`, ed è il record a saperlo. Cambia `goesTo` in `"sunset"` e il nuovo tentativo diventa un checkpoint invece di una ripartenza da capo -- una parola, e la difficoltà del gioco cambia. [source,lua] ---- overlayPrint(2, 6, math.ceil((scene.last - frame) / FPS) .. " seconds left.") ---- Il timer. `scene.last - frame` è quanti fotogrammi restano, e dividerlo per i fotogrammi in un secondo lo trasforma in secondi. `math.ceil` arrotonda un numero per eccesso all'intero successivo, quindi il conto alla rovescia dice `3`, `2`, `1` e mai `0.4` o `2.966666`. `FPS` vale `30` perché questo video va a poco più di trenta fotogrammi al secondo. È l'unico numero dello script che è una stima, ed è una stima con un nome in cima al file invece di un `30` sepolto in mezzo a una riga, così quando cambi video c'è esattamente una cosa da cambiare. Nota rispetto a che cosa si misura il timer: il filmato. Il giocatore fa a gara con il video, non con un orologio, quindi la domanda non può mai durare più dell'inquadratura a cui appartiene. È per questo che i giochi a diramazioni contano in fotogrammi. [#_why_not_write_it_out_by_hand] === Perché non scriverlo tutto a mano Potresti scrivere questo gioco come un'unica lunga funzione. Riproduci i fotogrammi da 0 a 119; se sinistra, riproduci da 120 a 259; se di nuovo sinistra, riproduci da 340 a 410; altrimenti riproduci da 260 a 339, togli una vita, torna all'inizio. Con quattro scene sarebbe più corto di quello che hai. Con quaranta scene sarebbe un disastro, e vale la pena essere precisi sul perché. I numeri dei fotogrammi sarebbero sparsi per tutto il codice, ognuno scritto una volta e mai con un nome, quindi cambiare il punto in cui comincia una scena vorrebbe dire trovare ogni posto in cui è nominata. La forma della storia sarebbe sepolta nella forma delle istruzioni `if`, quindi nessuno potrebbe vedere il gioco senza leggerlo tutto. E aggiungere una scena vorrebbe dire modificare il codice che riproduce, cioè il codice che già funziona -- ogni nuova scena un'altra occasione per romperne una vecchia. La versione che hai scritto ha un pezzo di codice che riproduce le scene e un pezzo di dati che dice quali sono le scene. Aggiungere una scena tocca solo i dati. Vale la mezz'ora in più che ci è voluta, ed è la stessa idea che sta sotto il file `games.dat` della tredicesima lezione: descrivi la cosa, poi scrivi un pezzo di codice che legge la descrizione. [#_try_it_16] === Prova tu . *Aggiungi un finale.* Da' a `escape` una domanda e una seconda scena finale che riusa fotogrammi da qualche parte del filmato. Cambia solo la tabella. . *Rendilo equo.* Abbassa `ask` in tutte e due le scene così il giocatore ha cinque secondi invece di tre, poi alzalo così ne ha uno. Trova il numero che useresti davvero nel gioco finito. . *Sposta il checkpoint.* Cambia il `goesTo` di `pit` in `"sunset"` e giocaci. Poi pensa a che cosa succede se un giocatore muore in `arrive` -- e se la scena della morte dovrebbe sapere da dove è arrivata invece di sentirselo dire. . *Conta quello che il giocatore ha visto.* Aggiungi una tabella accanto a `scenes` che registra quali nomi di scena sono stati riprodotti, e mostra il conto nell'angolo. Adesso hai un gioco che può dire «hai trovato tre finali su cinque». . *Rompi la mappa.* Cambia il `left` di `arrive` in `"cave"`, una scena che non esiste. Eseguilo, va' a sinistra e leggi che cosa dice Lua. Quell'errore è il più comune in un gioco guidato dai dati, e conoscerne la forma ti fa risparmiare un'ora. [#_break_it_on_purpose_15] === Rompilo apposta Togli la guardia dalla riga di `asking`, così diventa: [source,lua] ---- local asking = frame >= scene.ask ---- Eseguilo e va' a destra. La scena `arrive` va bene, perché `arrive` ha un `ask`. Poi comincia la scena `pit`, che non ce l'ha, e il gioco si ferma con questo: ---- Error executing function 'onOverlayUpdate': movie/dragon.singe:68: attempt to compare nil with number stack traceback: [C]: in function 'error' Singe/Framework.singe:603: in function 'onOverlayUpdate' ---- Leggilo come ti ha insegnato la prima lezione. Prima vengono il file e la riga, e sono esattamente giusti: la riga 68 è quella che hai appena cambiato. La lamentela è il resto: hai chiesto a Lua se un numero era maggiore di `nil`, e Lua non ne ha idea. `scene.ask` è `nil` perché il record `pit` non ha un campo `ask`, e un campo che non c'è è `nil`. Una parte di quel messaggio merita una spiegazione, perché è una bugia per omissione. La funzione nominata è `onOverlayUpdate`, che non hai scritto tu. È quella del framework, quella che secondo la quindicesima lezione viene installata per far andare la tua `singeMain`. L'errore è uscito passando da lì, quindi quello è il nome che il motore aveva sotto mano, e il traceback sotto -- l'elenco delle funzioni che erano in esecuzione, dalla più interna -- può mostrare soltanto il framework che passa avanti il tuo errore. Ignorali tutti e due. Il file e la riga in cima sono tuoi, e sono quelli veri. [#_what_you_learned_16] === Cosa hai imparato * Un gioco a diramazioni è un insieme di scene, ognuna delle quali nomina le scene che vengono dopo. * Tieni le scene come dati -- una tabella di record -- e il codice che le riproduce resta una sola funzione per quanto grande diventi il gioco. * Un record contiene il nome della scena successiva, non la scena, e il nome viene cercato quando serve. * Un campo che non c'è in un record è `nil`, che non è un errore, e il codice che legge i record se lo deve aspettare. * `scene[answer]` cerca il campo il cui nome sta in una variabile; `scene.left` e `scene["left"]` sono lo stesso campo. * Metti il timer sul video: una scelta dura da un fotogramma a un altro, quindi non può mai durare più della sua inquadratura. * Butta via qualunque cosa il giocatore abbia premuto prima che comparisse la domanda. * Metti in pausa il disco nell'istante in cui una scena finisce, o va avanti nel filmato della scena successiva. * Un campo mancante che sta per falso -- `death` in tre record su quattro -- mantiene la descrizione breve e onesta. * Confrontare `nil` con un numero è un errore, e `and` è il modo per evitarlo. [#_next_time_16] === La prossima volta Il tuo gioco chiede sinistra o destra. Le macchine da sala giochi in cui è cresciuto questo genere di gioco chiedevano qualcosa di più difficile: punta la cosa sullo schermo e sparale. La diciassettesima lezione parla di pistole ottiche -- dove sta mirando il giocatore, che cosa ne fa il gioco, e la calibrazione di cui un vero cabinato non può fare a meno.