617 lines
21 KiB
Text
617 lines
21 KiB
Text
[#_lesson_18_quick_time_events]
|
|
== Lezione 18: Quick-time event
|
|
|
|
image::learn/18-qte.png[La lezione finita, 480]
|
|
|
|
Compare una richiesta a schermo. Hai mezzo secondo. Premi la cosa giusta e la
|
|
storia va avanti; premi quella sbagliata, o niente del tutto, e va da un'altra
|
|
parte.
|
|
|
|
Questa è tutta l'idea, e un numero sorprendente di giochi video è fatto quasi
|
|
solo di questo. Hai già tutti i pezzi: la quattordicesima lezione ti ha dato i
|
|
fotogrammi, la sesta un elenco di record, la terza i comandi e la sedicesima
|
|
le diramazioni. Questa lezione li mette insieme e aggiunge una cosa davvero
|
|
nuova -- una finestra di tempo, e che cosa succede quando si chiude.
|
|
|
|
[#_the_clock_is_the_film]
|
|
=== L'orologio è il filmato
|
|
|
|
Qui tutto si misura in *fotogrammi*, non in secondi, e vale la pena fermarsi su
|
|
questo, perché è l'unica decisione che fa funzionare tutto.
|
|
|
|
La quattordicesima lezione diceva che un fotogramma è l'indirizzo di un momento
|
|
del filmato. La richiesta deve comparire quando l'attore comincia a caricare il
|
|
pugno, e la finestra deve chiudersi quando il pugno arriva. Quelli non sono
|
|
tempi, sono punti del filmato, e li hai trovati scorrendolo un passo alla volta.
|
|
Se scrivi la finestra in secondi devi far partire un cronometro quando compare
|
|
la richiesta, e a quel punto i due orologi -- il tuo e quello del filmato --
|
|
possono andare fuori sincrono. Se il video scatta su una macchina lenta, il tuo
|
|
cronometro continua a correre e il giocatore viene punito per il suo hardware.
|
|
|
|
I fotogrammi non possono andare fuori sincrono, perché c'è un orologio solo.
|
|
`discGetFrame()` è l'ora, sempre. Una finestra è due numeri di fotogramma, e
|
|
«quanto manca» è una sottrazione.
|
|
|
|
Eseguilo con lo stesso video di prima:
|
|
|
|
----
|
|
Singe -R -v Singe/menuBackground.mkv moves
|
|
----
|
|
|
|
Sempre la griglia viola e il tramonto. Immagina uno scontro.
|
|
|
|
[#_a_move_is_a_record]
|
|
=== Una mossa è un record
|
|
|
|
Ecco la tabella su cui si regge tutta la lezione:
|
|
|
|
[source,lua]
|
|
----
|
|
local moves = {
|
|
{ first = 60, last = 105, switch = SWITCH_LEFT, name = "LEFT" },
|
|
{ first = 140, last = 185, switch = SWITCH_UP, name = "UP" },
|
|
{ first = 220, last = 265, switch = SWITCH_RIGHT, name = "RIGHT" },
|
|
{ first = 300, last = 345, switch = SWITCH_DOWN, name = "DOWN" },
|
|
}
|
|
----
|
|
|
|
Quattro mosse. Ognuna dice quando si apre la finestra, quando si chiude, quale
|
|
interruttore le risponde e che cosa mettere sullo schermo. Quarantacinque
|
|
fotogrammi sono circa un secondo e mezzo alla velocità di questo video, il che
|
|
è generoso; i giochi veri spesso ti danno meno della metà.
|
|
|
|
Tutto ciò che fa di una mossa una mossa sta in quella tabella. Nel codice più
|
|
sotto non c'è niente che sappia che le mosse sono quattro, o che la seconda è
|
|
su, o che la prima finestra si apre al fotogramma sessanta. Aggiungere una
|
|
quinta mossa vuol dire aggiungere una riga. Cambiare i tempi di tutte le mosse
|
|
vuol dire modificare dei numeri. Nessuno deve leggere una funzione per cambiare
|
|
lo scontro.
|
|
|
|
Non è un trucco per questa lezione. È la differenza tra un gioco che puoi
|
|
mettere a punto e uno che non puoi, e farlo fin dall'inizio non costa niente.
|
|
Quando ti accorgi che stai per scrivere `if frame == 60 then` per la quarta
|
|
volta, fermati e fai invece una tabella.
|
|
|
|
[#_what_happens_when]
|
|
=== Che cosa succede quando
|
|
|
|
A una mossa possono succedere tre cose, e sono tre esiti diversi, non due:
|
|
|
|
* Il giocatore preme l'interruttore giusto dentro la finestra. Assegna i punti e
|
|
si va avanti.
|
|
* Il giocatore preme un interruttore *sbagliato* dentro la finestra. Ci ha
|
|
provato e ha fallito.
|
|
* La finestra si chiude senza nessuna risposta. Si è bloccato.
|
|
|
|
Il codice pigro tratta allo stesso modo gli ultimi due. I giochi non lo fanno
|
|
quasi mai, perché giocandoci danno sensazioni completamente diverse: tirare a
|
|
indovinare e sbagliare è un errore, bloccarsi è un errore diverso, e di solito
|
|
il filmato va in un posto diverso per ciascuno. Qui la risposta sbagliata manda
|
|
il disco al fotogramma `362` e la risposta mancata lo manda al fotogramma `388`,
|
|
il che su questo filmato vuol dire due quantità di tramonto un po' diverse. Su
|
|
un filmato vero sarebbero due inquadrature diverse di te che perdi.
|
|
|
|
Quindi ogni record mette su un quinto campo mentre la partita è in corso --
|
|
`answer` -- che è `nil` finché alla mossa non succede qualcosa, e poi una di
|
|
tre stringhe:
|
|
|
|
[source,lua]
|
|
----
|
|
move.answer = "right"
|
|
move.answer = "wrong"
|
|
move.answer = "late"
|
|
----
|
|
|
|
`nil` è il valore che Lua ti dà per un campo che nessuno ha impostato. L'hai
|
|
incontrato nella terza lezione. Qui si guadagna il pane: «nessuna risposta
|
|
ancora» è davvero uno stato diverso da tutte e tre le risposte, e `nil` lo dice
|
|
senza che tu debba inventarti una parola apposta.
|
|
|
|
[#_finding_the_move_that_is_being_asked]
|
|
=== Trovare la mossa richiesta
|
|
|
|
Tutto il resto ha bisogno di sapere una cosa: quale mossa, se ce n'è una, è
|
|
aperta in questo momento?
|
|
|
|
[source,lua]
|
|
----
|
|
local function moveAt(frame)
|
|
for _, move in ipairs(moves) do
|
|
if move.answer == nil and frame >= move.first and frame <= move.last then
|
|
return move
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
----
|
|
|
|
Scorri l'elenco, e rispondi con la prima mossa la cui finestra copre questo
|
|
fotogramma e che non ha ancora avuto risposta. Se non corrisponde niente,
|
|
rispondi `nil`.
|
|
|
|
Vale la pena notare che restituisce `nil` apposta. Una funzione che a volte non
|
|
ha una risposta dovrebbe dirlo, e «nessuna mossa è aperta» è una risposta più
|
|
che valida. Chi la chiama scrive allora `if move ~= nil then`, e il codice che
|
|
disegna la richiesta non viene mai eseguito quando non c'è niente da chiedere.
|
|
|
|
La verifica `move.answer == nil` è ciò che impedisce a una mossa di essere
|
|
chiesta due volte. Nell'istante in cui il giocatore risponde giusto, la
|
|
richiesta sparisce anche se la finestra è ancora aperta -- ed è proprio quello
|
|
che deve fare, perché ha risposto.
|
|
|
|
[#_reading_the_answer]
|
|
=== Leggere la risposta
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if not isDirection(what) then
|
|
return
|
|
end
|
|
|
|
local move = moveAt(discGetFrame())
|
|
if move == nil then
|
|
return
|
|
end
|
|
|
|
if what == move.switch then
|
|
move.answer = "right"
|
|
score = score + RIGHT_SCORE
|
|
else
|
|
move.answer = "wrong"
|
|
endRun("WRONG WAY", WRONG_FRAME)
|
|
end
|
|
end
|
|
----
|
|
|
|
Leggila dall'alto in basso. Se l'interruttore non è una delle quattro
|
|
direzioni, non sono affari nostri -- vattene. Se nessuna mossa è aperta, premere
|
|
una direzione non vuol dire niente -- vattene. Altrimenti c'è esattamente una
|
|
mossa con cui confrontare, e un confronto da fare.
|
|
|
|
Quelle due righe `return` in anticipo sono delle guardie: buttano fuori i casi
|
|
che non ti interessano, così il codice sotto viene eseguito solo nell'unica
|
|
situazione per cui è stato scritto. Scriverlo al contrario, con il lavoro vero
|
|
annidato tre livelli più in fondo dentro tre blocchi `if`, dice la stessa cosa
|
|
ed è molto più difficile da leggere.
|
|
|
|
`isDirection` è una funzione minuscola che non fa altro che tenere corta quella
|
|
prima riga:
|
|
|
|
[source,lua]
|
|
----
|
|
local function isDirection(what)
|
|
return what == SWITCH_UP or what == SWITCH_DOWN or what == SWITCH_LEFT or what == SWITCH_RIGHT
|
|
end
|
|
----
|
|
|
|
Nota che restituisce direttamente il confronto invece di dire `if ... then
|
|
return true else return false end`. Un confronto è già `true` o `false`.
|
|
Verificare se qualcosa è vero per poter dire che è vero è un'abitudine da
|
|
perdere presto.
|
|
|
|
[#_the_window_closing]
|
|
=== La finestra che si chiude
|
|
|
|
Nessuno preme niente, e i fotogrammi continuano a passare. Qualcosa deve
|
|
accorgersene. Quel qualcosa è `onOverlayUpdate`, perché viene eseguita a ogni
|
|
fotogramma, che il giocatore faccia qualcosa o no:
|
|
|
|
[source,lua]
|
|
----
|
|
if not runOver then
|
|
for _, move in ipairs(moves) do
|
|
if move.answer == nil and frame > move.last then
|
|
move.answer = "late"
|
|
endRun("TOO SLOW", LATE_FRAME)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
Una mossa che non ha avuto risposta e il cui ultimo fotogramma è passato è
|
|
stata mancata. `break` esce subito dal ciclo, perché una volta finita la
|
|
partita non ha senso guardare il resto.
|
|
|
|
`endRun` è la diramazione, ed è la stessa diramazione che hai scritto nella
|
|
sedicesima lezione:
|
|
|
|
[source,lua]
|
|
----
|
|
local function endRun(reason, frame)
|
|
verdict = reason
|
|
runOver = true
|
|
discSkipToFrame(frame)
|
|
end
|
|
----
|
|
|
|
Tre righe. Ricordati che cosa dire, ricordati che la partita è finita, e manda
|
|
il filmato da un'altra parte. Un quick-time event mancato non è un meccanismo
|
|
speciale. È una scelta che il giocatore ha fatto non facendone nessuna, e il
|
|
filmato si dirama su di essa esattamente come si dirama su una porta che ha
|
|
scelto.
|
|
|
|
[#_a_gauge_that_drains]
|
|
=== Un indicatore che si svuota
|
|
|
|
Il giocatore non vede i numeri dei fotogrammi. Deve vedere il tempo che finisce,
|
|
e deve vederlo senza distogliere lo sguardo dall'azione, il che vuol dire una
|
|
forma che cambia invece di un numero che conta.
|
|
|
|
Due numeri ti danno tutto:
|
|
|
|
[source,lua]
|
|
----
|
|
local left = move.last - frame
|
|
local span = move.last - move.first
|
|
----
|
|
|
|
`span` è quanto è lunga la finestra. `left` è quanta ne resta. La barra è
|
|
`left` su `span` della larghezza piena:
|
|
|
|
[source,lua]
|
|
----
|
|
fillBar(GAUGE_X, GAUGE_Y, GAUGE_W * left // span, GAUGE_H)
|
|
----
|
|
|
|
`//` è la divisione che butta via la parte frazionaria, ed è quello che vuoi
|
|
qui, perché una barra larga centotrentatré pixel e un terzo non esiste.
|
|
Moltiplica prima di dividere -- `GAUGE_W * left // span` e non `GAUGE_W * (left //
|
|
span)` -- altrimenti la parte frazionaria viene buttata via quando è ancora
|
|
l'unica informazione che hai, e la barra salta da piena a vuota senza niente in
|
|
mezzo.
|
|
|
|
Nell'overlay non c'è un rettangolo pieno. `overlayBox` disegna un contorno, che
|
|
è quello che ti serve per la cornice intorno all'indicatore, e il riempimento
|
|
lo fai tu:
|
|
|
|
[source,lua]
|
|
----
|
|
local function fillBar(x, y, width, height)
|
|
if width < 1 then
|
|
return
|
|
end
|
|
for row = 0, height - 1 do
|
|
overlayLine(x, y + row, x + width - 1, y + row)
|
|
end
|
|
end
|
|
----
|
|
|
|
Diciotto linee orizzontali impilate una sull'altra. La guardia in cima conta:
|
|
all'ultimissimo fotogramma della finestra `left` vale zero, la larghezza vale
|
|
zero, e senza la guardia `x + width - 1` sta un pixel a *sinistra* di `x` e
|
|
disegni una piccola linea all'indietro invece di niente.
|
|
|
|
L'ultimo tocco è un cambio di colore quando la situazione si fa disperata:
|
|
|
|
[source,lua]
|
|
----
|
|
if left * 3 < span then
|
|
colorForeground(255, 60, 60, 255)
|
|
else
|
|
colorForeground(255, 200, 0, 255)
|
|
end
|
|
----
|
|
|
|
La barra diventa rossa nel suo ultimo terzo. Scritto così -- moltiplicando il
|
|
tempo che resta invece di dividere la durata -- resta in numeri interi e
|
|
funziona per una finestra di qualsiasi lunghezza.
|
|
|
|
[#_the_whole_thing_10]
|
|
=== Tutto insieme
|
|
|
|
[source,lua]
|
|
----
|
|
-- Impara a programmare con Singe -- Lezione 18: Quick-time event
|
|
|
|
dofile("Singe/Framework.singe")
|
|
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
|
|
local FIRST_FRAME = 20
|
|
local WRONG_FRAME = 362
|
|
local LATE_FRAME = 388
|
|
local LAST_FRAME = 415
|
|
|
|
local PROMPT_COL = 54
|
|
local PROMPT_ROW = 22
|
|
local GAUGE_X = 210
|
|
local GAUGE_Y = 330
|
|
local GAUGE_W = 300
|
|
local GAUGE_H = 18
|
|
|
|
local RIGHT_SCORE = 100
|
|
|
|
local moves = {
|
|
{ first = 60, last = 105, switch = SWITCH_LEFT, name = "LEFT" },
|
|
{ first = 140, last = 185, switch = SWITCH_UP, name = "UP" },
|
|
{ first = 220, last = 265, switch = SWITCH_RIGHT, name = "RIGHT" },
|
|
{ first = 300, last = 345, switch = SWITCH_DOWN, name = "DOWN" },
|
|
}
|
|
|
|
local score = 0
|
|
local runOver = false
|
|
local verdict = ""
|
|
|
|
|
|
local function fillBar(x, y, width, height)
|
|
if width < 1 then
|
|
return
|
|
end
|
|
for row = 0, height - 1 do
|
|
overlayLine(x, y + row, x + width - 1, y + row)
|
|
end
|
|
end
|
|
|
|
|
|
local function isDirection(what)
|
|
return what == SWITCH_UP or what == SWITCH_DOWN or what == SWITCH_LEFT or what == SWITCH_RIGHT
|
|
end
|
|
|
|
|
|
local function moveAt(frame)
|
|
for _, move in ipairs(moves) do
|
|
if move.answer == nil and frame >= move.first and frame <= move.last then
|
|
return move
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
|
|
local function startRun()
|
|
score = 0
|
|
runOver = false
|
|
verdict = ""
|
|
for _, move in ipairs(moves) do
|
|
move.answer = nil
|
|
end
|
|
discSkipToFrame(FIRST_FRAME)
|
|
end
|
|
|
|
|
|
local function endRun(reason, frame)
|
|
verdict = reason
|
|
runOver = true
|
|
discSkipToFrame(frame)
|
|
end
|
|
|
|
|
|
function onInputPressed(what)
|
|
if runOver then
|
|
if what == SWITCH_START1 then
|
|
startRun()
|
|
end
|
|
return
|
|
end
|
|
|
|
if not isDirection(what) then
|
|
return
|
|
end
|
|
|
|
local move = moveAt(discGetFrame())
|
|
if move == nil then
|
|
return
|
|
end
|
|
|
|
if what == move.switch then
|
|
move.answer = "right"
|
|
score = score + RIGHT_SCORE
|
|
else
|
|
move.answer = "wrong"
|
|
endRun("WRONG WAY", WRONG_FRAME)
|
|
end
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
local frame = discGetFrame()
|
|
|
|
if not runOver then
|
|
for _, move in ipairs(moves) do
|
|
if move.answer == nil and frame > move.last then
|
|
move.answer = "late"
|
|
endRun("TOO SLOW", LATE_FRAME)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if frame >= LAST_FRAME and discGetState() == DISC_PLAYING then
|
|
if not runOver then
|
|
verdict = "CLEAR"
|
|
runOver = true
|
|
end
|
|
discSearch(LAST_FRAME)
|
|
end
|
|
|
|
overlayClear()
|
|
|
|
local move = nil
|
|
if not runOver then
|
|
move = moveAt(frame)
|
|
end
|
|
|
|
if move ~= nil then
|
|
local left = move.last - frame
|
|
local span = move.last - move.first
|
|
|
|
overlayPrint(PROMPT_COL, PROMPT_ROW, "PRESS " .. move.name)
|
|
|
|
colorForeground(255, 255, 255, 255)
|
|
overlayBox(GAUGE_X - 2, GAUGE_Y - 2, GAUGE_X + GAUGE_W + 1, GAUGE_Y + GAUGE_H + 1)
|
|
|
|
if left * 3 < span then
|
|
colorForeground(255, 60, 60, 255)
|
|
else
|
|
colorForeground(255, 200, 0, 255)
|
|
end
|
|
fillBar(GAUGE_X, GAUGE_Y, GAUGE_W * left // span, GAUGE_H)
|
|
end
|
|
|
|
overlayPrint(2, 1, "SCORE " .. score .. " FRAME " .. frame)
|
|
|
|
if runOver then
|
|
overlayPrint(2, 3, verdict)
|
|
overlayPrint(2, 4, "PRESS 1 TO RUN IT AGAIN")
|
|
end
|
|
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
startRun()
|
|
----
|
|
|
|
I tasti freccia rispondono alle richieste. Azzeccale tutte e quattro e il
|
|
filmato va fino in fondo e si ferma lì su `CLEAR`. Sbagliane una, o resta con le
|
|
mani in mano, e il filmato salta a un fotogramma diverso con una parola diversa
|
|
sullo schermo. `1` lo fa ripartire.
|
|
|
|
[#_what_just_happened_14]
|
|
=== Cos'è appena successo
|
|
|
|
La maggior parte l'hai già letta. Quattro pezzi meritano una seconda occhiata.
|
|
|
|
[source,lua]
|
|
----
|
|
if frame >= LAST_FRAME and discGetState() == DISC_PLAYING then
|
|
----
|
|
|
|
La fine del filmato va intercettata, altrimenti il filmato va oltre la fine e
|
|
non c'è più niente da vedere. `discSearch` va a un fotogramma e lì si *mette in
|
|
pausa*, ed è esattamente quello che vuole un'inquadratura finale. La metà con
|
|
`discGetState()` impedisce che questo succeda di nuovo a ognuno dei fotogrammi
|
|
successivi: una volta in pausa il disco non è più `DISC_PLAYING`, quindi la
|
|
condizione è falsa e il salto non viene ripetuto sessanta volte al secondo.
|
|
`discGetState` e le costanti `DISC_*` sono nel Riferimento, sotto
|
|
`discGetState`.
|
|
|
|
[source,lua]
|
|
----
|
|
local move = nil
|
|
if not runOver then
|
|
move = moveAt(frame)
|
|
end
|
|
----
|
|
|
|
`onOverlayUpdate` chiede di nuovo qual è la mossa aperta, invece di farselo
|
|
dire da `onInputPressed`. È voluto. C'è esattamente una funzione che sa quale
|
|
mossa è aperta, e tutti chiedono a lei. Se la richiesta venisse da una
|
|
variabile tenuta aggiornata dal codice dei comandi, allora due pezzi di codice
|
|
si ricorderebbero lo stesso fatto, e prima o poi uno dei due sbaglierebbe.
|
|
Quando devi scegliere tra chiedere e ricordare, chiedi.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(PROMPT_COL, PROMPT_ROW, "PRESS " .. move.name)
|
|
----
|
|
|
|
`overlayPrint` conta in celle di carattere, come fa dalla prima lezione, quindi
|
|
la richiesta è posizionata a occhio: la colonna cinquantaquattro è abbastanza
|
|
vicina al centro per parole di questa lunghezza. È abbastanza vicina, non
|
|
giusta, e «PRESS RIGHT» finisce tre caratteri più a destra di «PRESS UP». La
|
|
diciannovesima lezione carica un vero font, e con lui la possibilità di
|
|
misurare una stringa in pixel e centrarla come si deve.
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if runOver then
|
|
if what == SWITCH_START1 then
|
|
startRun()
|
|
end
|
|
return
|
|
end
|
|
----
|
|
|
|
Una callback, due lavori completamente diversi, scelti da `runOver`. Mentre la
|
|
partita è in corso le direzioni sono mosse; una volta finita, l'unico tasto che
|
|
vuol dire qualcosa è start. Una variabile che decide quali regole valgono è uno
|
|
*stato*, che la dodicesima lezione chiamava proprio così, e un gioco è fatto
|
|
soprattutto di un piccolo numero di stati.
|
|
|
|
[#_try_it_18]
|
|
=== Prova tu
|
|
|
|
. *Rendilo più difficile.* Cambia il `last` della prima mossa da `105` a `75`.
|
|
Quindici fotogrammi sono mezzo secondo. Giocaci e scopri se ce la fai.
|
|
. *Aggiungi una mossa.* Metti una quinta riga nella tabella con una finestra tra
|
|
i fotogrammi `370` e `400`, e porta `LAST_FRAME` a `418`. Non dovresti dover
|
|
toccare nessun'altra riga. Se devi farlo, qualcosa nel codice sa troppo della
|
|
tabella.
|
|
. *Dai punti alla velocità.* Assegna più punti a chi risponde presto. In
|
|
`onInputPressed`, dove viene premiata la risposta giusta, calcola `left` e
|
|
`span` a partire da `move` e `discGetFrame()` come fa l'indicatore, e aggiungi
|
|
`RIGHT_SCORE * left // span` invece di `RIGHT_SCORE`. Decidi se così il gioco
|
|
diventa migliore o solo più movimentato.
|
|
. *Da' alla risposta sbagliata una conseguenza sua.* Invece di far finire la
|
|
partita, fai costare cinquanta punti a una pressione sbagliata e lascia la
|
|
finestra aperta per riprovare. Dovrai smettere di impostare `move.answer`, e
|
|
incontrerai il motivo per cui veniva impostata.
|
|
. *Due richieste insieme.* Sovrapponi le finestre di due mosse e guarda che cosa
|
|
succede. `moveAt` restituisce la prima corrispondenza, quindi ne viene chiesta
|
|
sempre una sola. Decidi che cosa vorresti, e che cosa ci vorrebbe.
|
|
|
|
[#_break_it_on_purpose_17]
|
|
=== Rompilo apposta
|
|
|
|
Cancella il `break` dal ciclo che trova le mosse mancate, e manca la prima
|
|
apposta. La partita finisce, il disco salta, e sembra tutto a posto.
|
|
|
|
Ora metti `debugPrint(move.name .. " missed")` accanto a `move.answer = "late"`
|
|
e rifallo. La console mostra una riga, come ti aspetteresti.
|
|
|
|
Poi togli anche il blocco `if not runOver then` intorno al ciclo, e manca di nuovo la
|
|
prima mossa:
|
|
|
|
----
|
|
LEFT missed
|
|
UP missed
|
|
RIGHT missed
|
|
DOWN missed
|
|
----
|
|
|
|
La prima riga arriva quando la finestra si chiude, come prima. Le altre tre
|
|
arrivano insieme proprio al fotogramma successivo, nell'istante in cui il disco
|
|
atterra sul 388: ogni finestra rimasta ormai è nel passato, quindi ogni mossa
|
|
senza risposta viene mancata tutta in una volta, e ognuna chiama `endRun` e fa
|
|
un altro salto. Il gioco più o meno funziona ancora, ed è questo che lo rende
|
|
pericoloso. Tre salti in più in un solo fotogramma, tre sovrascritture del
|
|
verdetto, e niente sullo schermo lo dice.
|
|
|
|
Anche qui non c'è nessun messaggio di errore. La lezione è che quel `break` e
|
|
quella guardia non sono questione di ordine. Sono le due righe che dicono «questo
|
|
è già stato deciso», e il codice che continua a decidere una cosa che ha già
|
|
deciso è una delle fonti di bug più affidabili che scriverai mai.
|
|
|
|
[#_what_you_learned_18]
|
|
=== Cosa hai imparato
|
|
|
|
* Un quick-time event è una richiesta, una finestra e una conseguenza.
|
|
* Misura la finestra in fotogrammi, perché il filmato è l'orologio e di
|
|
orologio ce ne deve essere sempre uno solo.
|
|
* Tieni le mosse in una tabella di record, così aggiungere una mossa vuol dire
|
|
aggiungere una riga.
|
|
* Giusto, sbagliato e nessuna risposta sono tre esiti, e di solito portano in
|
|
tre posti diversi.
|
|
* `nil` in un campo di un record è uno stato vero: nessuno ha ancora risposto.
|
|
* Una sola funzione risponde a «quale mossa è aperta», e tutti chiedono a lei
|
|
invece di tenerne una copia.
|
|
* Le clausole di guardia -- un `return` anticipato per i casi che non ti
|
|
interessano -- evitano di annidare il lavoro vero.
|
|
* `//` divide e butta via la parte frazionaria; moltiplica prima di dividere.
|
|
* `overlayBox` disegna un contorno, quindi una barra piena è una pila di
|
|
chiamate a `overlayLine`.
|
|
* Una mossa mancata è una diramazione, e la prende `discSkipToFrame`, esattamente
|
|
come nella sedicesima lezione.
|
|
|
|
[#_next_time_18]
|
|
=== La prossima volta
|
|
|
|
La terza parte è finita. Sai riprodurre un filmato, aspettarlo, diramarlo,
|
|
mirarci e misurare il tempo del giocatore contro di lui. Quello che hai scritto
|
|
sopra a tutto questo è il font della console del motore, che è largo sei pixel e
|
|
pensato per la diagnostica. La diciannovesima lezione carica un vero carattere
|
|
tipografico, lo misura, e fa sembrare le parole sullo schermo parte del gioco.
|