552 lines
21 KiB
Text
552 lines
21 KiB
Text
[#_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.
|