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