542 lines
19 KiB
Text
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.
|