singe/docs/it/lessons/16-branching.adoc

542 lines
19 KiB
Text

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