singe/docs/it/lessons/12-score.adoc

552 lines
21 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[#_lesson_12_score_lives_and_game_over]
== Lezione 12: Punteggio, vite e game over
image::learn/12-score.png[La lezione finita, 480]
Il tuo gioco tiene il punteggio, toglie le vite e dice GAME OVER. È quasi un
gioco. Mancano ancora tre cose. Non ha una porta d'ingresso: è nel mezzo
dell'azione nell'istante in cui si carica. Dimentica: esci, e il punteggio
migliore che qualcuno abbia mai fatto se ne va con lui. E tiene traccia di
quello che sta facendo con due variabili vero-o-falso, `over` e `dying`, che è
la trappola da cui ti metteva in guardia la settima lezione.
Questa lezione le sistema tutte e tre. Una variabile per quello che sta facendo
il gioco, una schermata del titolo da cui partire, un punteggio record che
domani c'è ancora e una seconda partita che comincia esattamente come la prima.
[#_a_game_is_always_in_exactly_one_state]
=== Un gioco è sempre in un solo stato
Guarda un cabinato qualunque per due minuti. Mostra un titolo e aspetta
qualcuno. Qualcuno preme start e lui gioca. Il giocatore finisce le vite e lui
lo dice, poi aspetta di nuovo. La macchina fa sempre esattamente una di queste
cose.
Dodge, nella settima lezione, lo teneva in una variabile che conteneva una
parola: `"waiting"`, `"playing"` o `"over"`. Il gioco della nona lezione è
tornato a un `over` vero-o-falso, e la decima lezione gli ha messo accanto
`dying`. Conta che cosa possono dire due variabili così. Ognuna è `true` o
`false`, quindi insieme fanno quattro combinazioni. Tre vogliono dire qualcosa:
si gioca, si muore e la partita è finita. La quarta, tutte e due insieme, è
un'assurdità, e il tuo codice deve stare attento a non produrla mai. E ci sta
attento: guarda `endGame`, che rimette `dying` a `false` prima di mettere
`over` a `true`. Ora aggiungi una schermata del titolo. È un terzo flag, otto
combinazioni, e quattro sono assurdità. È esattamente la trappola della settima
lezione, ed è il posto dove i piccoli giochi vanno a morire.
Quindi fai come Dodge. Quattro stati, una variabile. Metti i nomi degli stati
sotto le tue altre costanti:
[source,lua]
----
local TITLE = "title"
local PLAYING = "playing"
local DYING = "dying"
local OVER = "over"
----
Poi togli `over` e `dying` dalle tue variabili, e al loro posto metti una riga
sola:
[source,lua]
----
local state = TITLE
----
Dodge usava direttamente le parole. I nomi in maiuscolo non sono più furbi, e
non acchiappano un errore di battitura: Lua tratta un nome scritto male come
`nil` e una parola scritta male come una parola che non corrisponde a niente, e
in tutti e due i casi ottieni un gioco che smette di rispondere senza dire
niente. Quello che i nomi ti comprano è un elenco. Ogni stato in cui il gioco
può trovarsi è scritto una volta sola, in cima al file, dove li vedi tutti e
quattro con un'occhiata.
Dovunque il gioco impostava un flag, adesso imposta lo stato. In
`updateRocks`, dove se ne va l'ultima vita:
[source,lua]
----
if lives <= 0 then
state = DYING
deathChannel = channel
----
In `endGame` due righe diventano una, e la combinazione assurda sparisce perché
non resta niente da combinare:
[source,lua]
----
function endGame()
deathChannel = -1
state = OVER
musicStop(theme, 1500)
end
----
E in `startGame` le due righe che azzeravano tutti e due i flag diventano
`state = PLAYING`.
[#_one_place_that_decides]
=== Un solo posto che decide
A ogni fotogramma `onOverlayUpdate` prende le due decisioni che contano: che
cosa si muove, e quali parole ci vanno sopra. Che cosa si muove è un controllo
solo:
[source,lua]
----
if state == PLAYING then
updateShip()
updateShots()
updateRocks()
end
----
Prende il posto di `if not over and not dying then`, e dice quello che
intende. Sulla schermata del titolo, mentre suona l'ultima esplosione e dopo
la fine della partita, non si muove niente. Lo ottieni non chiamando le
funzioni che muovono le cose. Fermare un gioco non è una funzionalità speciale;
è lasciare fuori delle cose.
Il mondo viene comunque disegnato a ogni fotogramma, qualunque sia lo stato,
esattamente come lo disegnava Dodge. Cambiano solo le parole. Sostituisci la
riga `drawHud()` con questa:
[source,lua]
----
if state == TITLE then
drawTitle()
else
drawHud()
end
----
`drawTitle` è nuova, e va dopo `drawStars`:
[source,lua]
----
function drawTitle()
overlayPrint(25, 6, "R O C K S")
overlayPrint(25, 9, "BEST " .. best)
overlayPrint(22, 12, "PRESS 1 TO PLAY")
end
----
`best` è il punteggio migliore finora, e arriva nella prossima sezione; finché
non arriva, il gioco non parte. Prima della prima partita l'elenco delle rocce
è vuoto, quindi il titolo mostra le stelle che scorrono e la nave che aspetta
in fondo, ed è tutto ciò che serve a una schermata del titolo.
Una riga se ne deve andare, altrimenti il titolo non lo vedrai mai. Cancella lo
`startGame()` proprio in fondo al file. Adesso il gioco comincia dal suo
titolo, e *Ricominciare* spiega perché quella riga conta.
`drawHud` fa una domanda tutta sua. Cambia il suo `if over then` in:
[source,lua]
----
if state == OVER then
----
La callback dei comandi ha bisogno dello stesso tipo di ragionamento. Eccola
intera:
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_LEFT then
goLeft = true
elseif what == SWITCH_RIGHT then
goRight = true
elseif what == SWITCH_BUTTON1 and state == PLAYING then
shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY }
soundPlay(shootClip, 0, 35)
elseif what == SWITCH_START1 and (state == TITLE or state == OVER) then
soundPlay(clickClip)
startGame()
end
end
----
Leggila dall'alto. I tasti freccia vengono registrati qualunque sia lo stato,
perché dicono soltanto quali tasti sono premuti, e niente muove la nave se lo
stato non è `PLAYING`. Il fuoco vuol dire qualcosa solo mentre si gioca. E `1`
comincia una partita dal titolo o dalla schermata di game over, e non mentre
l'ultima esplosione sta ancora suonando: la partita non è finita finché non lo
dice lei. Le parentesi funzionano come in aritmetica. Prima si calcola l’`or`
che sta dentro, e poi l’`and` lo unisce al tasto.
[#_score_and_lives]
=== Punteggio e vite
Li hai tutti e due dalla nona lezione, e cambiano poco. La novità è il
punteggio migliore, che nasce come una variabile qualunque. Mettila con le
altre, sotto `lives`:
[source,lua]
----
local best = 0
----
Mostralo nella riga in alto, tra gli altri due. In `drawHud`:
[source,lua]
----
overlayPrint(1, 1, "SCORE " .. score)
overlayPrint(25, 1, "BEST " .. best)
overlayPrint(50, 1, "LIVES " .. lives)
----
E dai a `endGame` il compito di accorgersi di un record:
[source,lua]
----
if score > best then
best = score
end
----
Eseguilo. Il titolo dice `BEST 0`. Premi `1`, spara a qualcosa, perdi, e la
schermata di game over mostra il tuo punteggio come il migliore. Premi di nuovo
`1` e il migliore resta mentre il punteggio riparte da zero. Ora esci, eseguilo
di nuovo e guarda il titolo: `BEST 0`. Il record è vissuto esattamente quanto
il programma. È la seconda metà di questa lezione.
Già che guardi le vite, nota che la nona lezione ha scritto il controllo come
`lives <= 0` e non `lives == 0`, e lascialo così. Oggi niente può toglierti due
vite in una volta, quindi i due controlli vanno sempre d'accordo. Il giorno in
cui aggiungi una roccia che ne costa due, `== 0` scavalca lo zero e il
giocatore continua a giocare con meno una vita, mentre `<= 0` continua a
funzionare. Chiedi quello che intendi.
[#_starting_again]
=== Ricominciare
Hai già cancellato lo `startGame()` in fondo al file. Nella decima lezione la
prima partita cominciava con quella riga e tutte le successive premendo `1`:
due porte d'ingresso. Adesso ce n'è una. Ogni partita, compresa la prima,
comincia quando qualcuno preme `1` e parte `startGame`, quindi la prima partita
e la quinta cominciano esattamente allo stesso modo.
`startGame` deve rimettere a posto tutto ciò che una partita cambia:
[source,lua]
----
function startGame()
shipX = (screenWidth - shipWidth) / 2
score = 0
lives = START_LIVES
state = PLAYING
deathChannel = -1
shots = {}
for i = 1, ROCK_COUNT do
rocks[i] = {}
newRock(rocks[i])
end
musicSetVolume(50)
musicPlay(theme, -1)
end
----
È altrettanto importante sapere che cosa lascia stare, e perché. `best`
sopravvive a ogni partita; è tutto il suo lavoro. Le stelle sono scenografia. E
`goLeft` e `goRight` non sono affatto della partita. Sono della tastiera, e solo
la tastiera dovrebbe cambiarli.
Quest'ultima cosa è un bug che hai appena corretto senza accorgertene. La
callback dei comandi della decima lezione cominciava con `if over then`, e
mentre la partita era finita buttava via ogni tasto tranne `1`. Premi la
freccia destra mentre si vede GAME OVER, tienila premuta e premi `1`. La nuova
nave resta ferma: la pressione della freccia destra è stata buttata via, il
rilascio non è ancora avvenuto, e quindi `goRight` è `false` mentre il tasto è
chiaramente giù. La nave resta dov'è finché non lasci il tasto e lo premi di
nuovo. La nuova callback registra i tasti freccia in ogni stato, quindi il
tasto tenuto premuto è ancora premuto quando la partita comincia.
I bug del ricominciare sono i bug più comuni in qualunque gioco, e quando giochi
sembrano tutti uguali: la seconda partita è sbagliata in un modo sottile e la
prima andava bene. Quando succede, leggi `startGame` e fatti due domande. Che
cosa ha cambiato l'ultima partita che questa non rimette a posto? E che cosa
sta rimettendo a posto questa che la partita non doveva mai toccare?
[#_a_number_that_outlives_the_program]
=== Un numero che sopravvive al programma
Tutto quello che il tuo gioco ha tenuto finora -- `score`, `lives`, l'elenco
delle rocce -- vive in memoria, e la memoria sparisce nell'istante in cui il
programma si ferma.
Singe dà a ogni gioco un salvataggio tutto suo: un insieme di nomi con dei
valori sotto, scritto su disco per te. È un'altra parte del kit di gioco
dell'undicesima lezione, quindi è lo stesso tipo di cosa di `collideRects`:
chiamate semplici, niente da caricare o liberare.
[source,lua]
----
saveSet("highScore", 4200)
----
`saveSet` prende un nome -- la *chiave* -- e un valore. La chiave la scegli tu
e non cambia mai; il valore è qualunque cosa tu voglia ricordare. Numeri,
stringhe, `true` e `false`, e intere tabelle di queste cose: ci va tutto. Non
ci vanno le cose che hanno senso solo mentre il programma è in esecuzione. Un
handle di sprite è solo un numero, quindi si salva, e domani è un numero che
non vuol dire niente. Una funzione non si può proprio scrivere, e provare a
salvarne una ferma il gioco con un errore invece di lasciartelo scoprire la
settimana dopo.
Rileggerlo è l'altra metà. Cambia la riga che hai aggiunto per `best`:
[source,lua]
----
local best = saveGet("highScore", 0)
----
Viene eseguita una volta, all'avvio del gioco. Il secondo argomento è il
*valore predefinito*: che cosa rispondere quando sotto quella chiave non è mai
stato salvato niente. Ne riparliamo tra un momento, perché è tutto l'errore di
questa lezione.
E fai in modo che `endGame` scriva il record quando viene battuto:
[source,lua]
----
function endGame()
deathChannel = -1
state = OVER
musicStop(theme, 1500)
if score > best then
best = score
saveSet("highScore", best)
saveFlush()
end
end
----
Non devi chiedere a Singe di scrivere il file. Lo scrive una volta alla fine di
ogni fotogramma in cui è cambiato qualcosa, per quante chiavi tu abbia
impostato, e di nuovo quando il gioco si chiude. `saveFlush` invece lo scrive
subito: un nuovo punteggio record merita una scrittura del file tutta sua,
perché un cabinato viene spento staccando la corrente e un fotogramma è tanto
tempo.
Eseguilo, fai un punteggio, esci ed eseguilo di nuovo. Il titolo se lo ricorda.
La sezione Salvataggio del Riferimento elenca il resto della famiglia.
`saveDelete` dimentica una chiave, `saveClear` dimentica tutto -- è il «reset
dei punteggi record» di un menu di servizio -- e `saveGetAll` ti dà una copia
di tutto quanto, che è utile quando vuoi stampare l'intero salvataggio mentre
stai dando la caccia a qualcosa.
[#_where_the_file_actually_goes]
=== Dove va a finire davvero il file
Non va accanto al tuo script, e non è un caso.
Un gioco finito può stare su un disco, su una cartella condivisa in rete o
dentro un unico file impacchettato (la trentesima lezione), e in nessuno di
questi si può scrivere. Un gioco che salva accanto al proprio script funziona
perfettamente sulla macchina su cui l'hai scritto e fallisce su metà delle
macchine su cui viene installato. Singe 2 faceva così, ed era sbagliato.
Quindi tutto ciò che Singe scrive per un gioco -- il salvataggio, gli
screenshot, il suo log -- va in un posto solo, e `singeGetDataPath` ti dice
dove:
[source,lua]
----
debugPrint(singeGetDataPath())
----
Mettilo per un momento subito sotto la riga del `dofile` ed eseguilo. Dice
`data/rocks/`: la cartella `data` comparsa nella tua cartella di lavoro la
prima volta che hai eseguito qualcosa, nella prima lezione, e dentro una
cartella con il nome della cartella del tuo gioco. Lì dentro troverai `save.json`. Aprilo nel
tuo editor di testo: è testo semplice, e il tuo punteggio record è lì dentro,
sotto il nome che gli hai dato.
È questa la risposta anche ogni volta che ti serve un file tutto tuo.
Costruisci il nome a partire da `singeGetDataPath()` e il file finisce in un
posto dove si può scrivere; scrivi su un nome inventato da te e prima o poi non
sarà così.
[#_the_first_time_there_is_nothing]
=== La prima volta non c'è niente
La primissima volta che qualcuno esegue il tuo gioco, non è mai stato salvato
nessun punteggio record. `saveGet` non ha niente da restituire, quindi
restituisce il valore predefinito che gli hai dato:
[source,lua]
----
local best = saveGet("highScore", 0)
----
Niente di salvato, quindi `best` è `0`, la schermata del titolo dice `BEST 0`,
e la prima partita che qualcuno gioca stabilisce un record. È esattamente
quello che vuoi, e costa un argomento in più.
Lascia fuori lo `0` e `saveGet` non ha un valore predefinito su cui ripiegare,
quindi risponde `nil` -- il valore che Lua usa per «qui non c'è niente», che
hai incontrato nella terza lezione. `nil` non è zero. Non ci puoi sommare
niente, non lo puoi confrontare con un numero e non lo puoi attaccare a una
stringa. Va storto al primo fotogramma, ogni volta, su ogni macchina dove il
gioco non è ancora stato giocato -- il che comprende ogni macchina su cui i
tuoi giocatori stanno per installarlo, e non quella su cui stai facendo le
prove.
Dai a ogni `saveGet` un valore predefinito. È l'abitudine più economica di
questo libro.
[#_what_just_happened_8]
=== Cos'è appena successo
[source,lua]
----
local TITLE = "title"
----
Una costante: un nome che imposti una volta e non cambi mai, in maiuscolo così
si vede con un'occhiata che è una costante, come nella settima lezione. Lua non
lo impone -- niente ti impedisce di assegnare un valore a `TITLE` -- quindi le
maiuscole sono un messaggio per la prossima persona che legge, cioè tu fra due
settimane.
[source,lua]
----
if state == PLAYING then
----
La domanda che decide se il mondo si muove. Quando più avanti aggiungerai un
quinto stato -- una pausa, o un round bonus -- la prima cosa da chiedersi è se
in quello stato il mondo si muove, ed è qui che va la risposta.
[source,lua]
----
elseif what == SWITCH_START1 and (state == TITLE or state == OVER) then
----
Senza le parentesi, Lua legherebbe per primo `and` e leggerebbe la riga come
«il tasto è `1` e lo stato è il titolo, oppure lo stato è la fine della
partita». Allora qualunque tasto che i rami qui sopra non hanno acchiappato
farebbe cominciare una partita dalla schermata di game over, compreso il fuoco,
e un giocatore che martella ancora il pulsante di fuoco quando arriva l'ultima
roccia verrebbe buttato in una nuova partita nell'istante in cui compare GAME
OVER. Le parentesi fanno venire prima `or`. Quando mescoli `and` e `or`, metti
le parentesi anche se pensi di conoscere l'ordine. La prossima persona che lo
legge non avrà bisogno di conoscerlo.
[source,lua]
----
saveSet("highScore", best)
saveFlush()
----
`saveSet` mette il numero nel salvataggio; `saveFlush` mette il salvataggio sul
disco adesso invece che alla fine del fotogramma. Stanno tutte e due dentro
l’`if`, quindi il file viene scritto solo quando il record viene battuto
davvero, cioè qualche volta per sessione invece che sessanta volte al secondo.
[source,lua]
----
local best = saveGet("highScore", 0)
----
Letto una volta, all'avvio, in una variabile qualunque. Il resto del gioco
legge `best` invece di chiamare di nuovo `saveGet`, perché il salvataggio è un
file e una variabile è una variabile. Quando il record cade, scrivi in tutti e
due.
[#_try_it_12]
=== Prova tu
. *Torna al titolo.* Un cabinato non resta su GAME OVER per sempre. Cerca
`timerAfter` nella sezione Timer del Riferimento, un'altra parte del kit di
gioco, e usalo in `endGame` per riportare lo stato a `TITLE` cinque secondi
dopo la fine della partita. Poi premi `1` subito dopo la fine di una partita
e controlla che il timer non rispedisca al titolo la tua nuova partita dopo
tre secondi.
. *Mostra le vite come navi.* Disegna `shipSprite` una volta per ogni vita
nell'angolo in alto a destra con `spriteDraw`, invece di stampare il numero.
Un ciclo `for` della quarta lezione, e `shipWidth` ti dice quanto
distanziarle.
. *Salva più di un numero.* Aggiungi `saveSet("gamesPlayed", saveGet("gamesPlayed",
0) + 1)` a `startGame`, e stampalo sulla schermata del titolo. Esci, eseguilo
di nuovo e guarda che se lo ricorda.
. *Trova il file e rompilo.* Apri `data/rocks/save.json` nel tuo editor, cambia
il punteggio record in qualcosa di assurdo e avvia il gioco. Poi cancella del
tutto il file e avvialo di nuovo. Ai tuoi giocatori succederanno tutte e due
le cose.
. *Azzeralo dall'interno del gioco.* Fai in modo che un tasto che non
premeresti mai per sbaglio chiami `saveClear()`, e poi controlla la schermata
del titolo. Attento: `best` è ancora in memoria, e `saveClear` non lo tocca.
Scopri che cos'altro deve succedere.
[#_break_it_on_purpose_11]
=== Rompilo apposta
Togli il valore predefinito, così la riga diventa:
[source,lua]
----
local best = saveGet("highScore")
----
Se hai già fatto un punteggio record non succede proprio niente, ed è questa la
trappola: il salvataggio ha la chiave, quindi `saveGet` ti restituisce il
valore. Prima cancella `data/rocks/save.json`, poi eseguilo. La schermata del
titolo non compare mai, e ottieni questo:
----
Error executing function 'onOverlayUpdate': rocks/rocks.singe:101: attempt to concatenate a nil value (upvalue 'best')
stack traceback:
rocks/rocks.singe:101: in function 'drawTitle'
rocks/rocks.singe:165: in function 'onOverlayUpdate'
----
Leggilo come ti ha insegnato l'ottava lezione. Il file e la riga ti portano a
`overlayPrint(25, 9, "BEST " .. best)` in `drawTitle`, e la lamentela dice che
qualcosa che era `nil` è stato attaccato a una stringa. `..` unisce due
stringhe, e `nil` non è una stringa. Se le tue righe sono disposte
diversamente, anche i tuoi numeri lo saranno.
La riga che nomina non è dove sta l'errore. La riga 101 è dove il gioco alla
fine ci è inciampato; l'errore è stato fatto vicino all'inizio del file, dove è
stato chiesto `best` e lì non c'era niente. Quella distanza tra il punto in cui
nasce un valore sbagliato e il punto in cui qualcuno se ne accorge è la cosa
più difficile di `nil`, ed è il motivo per cui un valore predefinito costa un
argomento e ti risparmia una serata.
Rimetti lo `0`.
[#_what_you_learned_12]
=== Cosa hai imparato
* Un gioco è in un solo stato alla volta, e i flag vero-o-falso che cercano di
dire quale generano combinazioni che non vogliono dire niente.
* Tieni lo stato in una variabile sola, con i suoi possibili valori nominati
una volta in cima al file.
* `onOverlayUpdate` decide che cosa si muove e che cosa viene detto, chiedendolo
allo stato.
* Fermare un gioco vuol dire non chiamare le cose che lo muovono.
* Fai cominciare ogni partita, compresa la prima, da una sola funzione, e fai
in modo che quella funzione rimetta a posto tutto ciò che una partita
cambia -- e nient'altro.
* I tasti appartengono alla tastiera. Registrali in ogni stato.
* `saveSet` conserva un valore sotto una chiave, e `saveGet` lo rilegge in un
avvio successivo.
* `saveFlush` scrive subito il file, per le cose che ti dispiacerebbe perdere.
* Il salvataggio va nella cartella dei dati del gioco, quella che nomina
`singeGetDataPath`. Non scrivere mai accanto al tuo script.
* `saveGet` senza valore predefinito risponde `nil` quando non è stato salvato
niente, e `nil` rompe il primo fotogramma su una macchina che non ha mai
eseguito il tuo gioco.
* Quando mescoli `and` e `or`, usa le parentesi.
[#_next_time_12]
=== La prossima volta
Adesso il tuo gioco è un gioco, ed è una cartella che solo tu sai come avviare.
La tredicesima lezione gli dà un `games.dat`, così il menu del motore stesso lo
elenca con un titolo e un'immagine, e un giocatore che non ha mai sentito
parlare di te può avviarlo senza scrivere niente.