[#_lesson_7_a_game] == Lezione 7: Un gioco image::learn/07-a-game.png[La lezione finita, 480] Sei lezioni fa hai messo una parola su uno schermo nero. Da allora hai imparato tutte le idee di cui è fatto un piccolo gioco: variabili, decisioni, cicli, funzioni tutte tue ed elenchi di cose. Questa lezione non insegna un'idea nuova. Spende quelle che hai. Costruirai un gioco chiamato Dodge. Dei blocchi cadono dal cielo, tu fai scorrere una barra lungo il fondo per scansarli, e ogni blocco che ti manca è un punto. Fatti colpire da tre ed è finita. Lo costruirai in sei fasi, e ognuna di esse funziona: non dovrai mai scrivere cento righe e sperare. [#_make_the_folder] === Crea la cartella Crea una cartella chiamata `dodge`, e dentro un file chiamato `dodge.singe`. Avvialo come avvii tutto dalla prima lezione: ---- Singe -R dodge ---- Lascialo in esecuzione in un terminale per tutta la lezione. Ogni volta che salvi, il gioco riparte con dentro la tua modifica. [#_stage_one_something_you_can_steer] === Prima fase: qualcosa da guidare Scrivi questo, salva e guidalo con i tasti freccia sinistra e destra. [source,lua] ---- dofile("Singe/Framework.singe") local PLAYER_WIDTH = 40 local PLAYER_HEIGHT = 8 local PLAYER_SPEED = 4 local PLAYER_MARGIN = 4 local screenWidth = overlayGetWidth() local screenHeight = overlayGetHeight() local playerY = screenHeight - PLAYER_HEIGHT - PLAYER_MARGIN local playerX = (screenWidth - PLAYER_WIDTH) / 2 local goingLeft = false local goingRight = false function onInputPressed(what) if what == SWITCH_LEFT then goingLeft = true elseif what == SWITCH_RIGHT then goingRight = true end end function onInputReleased(what) if what == SWITCH_LEFT then goingLeft = false elseif what == SWITCH_RIGHT then goingRight = false end end function onOverlayUpdate() if goingLeft then playerX = playerX - PLAYER_SPEED end if goingRight then playerX = playerX + PLAYER_SPEED end if playerX < 0 then playerX = 0 elseif playerX > screenWidth - PLAYER_WIDTH then playerX = screenWidth - PLAYER_WIDTH end overlayClear() colorForeground(80, 255, 160) overlayBox(playerX, playerY, playerX + PLAYER_WIDTH - 1, playerY + PLAYER_HEIGHT - 1) return OVERLAY_UPDATED end ---- Cinquanta righe, e quasi niente è nuovo. Due cose lo sono, quindi parti dalla prima riga. [source,lua] ---- dofile("Singe/Framework.singe") ---- `dofile` appartiene a Lua più che a Singe, e vuol dire «vai a eseguire quel file, poi continua qui». Il file che esegue è `Singe/Framework.singe`, uno dei file che il motore ha estratto nella tua cartella di lavoro la prima volta che hai eseguito qualcosa, nella prima lezione. Aprilo nel tuo editor di testo e guarda: è Lua normale, non diverso per natura da quello che scrivi dalla seconda lezione, e non c'è niente di magico. Eseguirlo consegna al tuo script una serie di nomi che poi può usare. Quelli a cui ricorre questo libro sono questi: * `DIR`, che contiene la cartella in cui si trova il tuo script. La nona lezione ne ha bisogno appena un gioco carica un'immagine, ed è il vero motivo per cui questa riga è qui. * `SCANCODE` e `MODIFIER`, nomi per ogni tasto di una tastiera, per un gioco che legge quello che si digita invece di un joystick. * Da `GAMEPAD_0` a `GAMEPAD_3` e da `MOUSE_0` a `MOUSE_3`, per un cabinato con più di un esemplare di qualcosa collegato. * Alcune funzioni di aiuto il cui nome comincia con `util`, e le sei chiamate che inviano un punteggio a una classifica, che sono la ventottesima lezione. * Il secondo modo di scrivere un gioco, del tutto diverso, che è la quindicesima lezione. Una cosa che *non* ti dà, perché è la cosa naturale da supporre. `SWITCH_LEFT`, e ogni altro nome che comincia con `SWITCH_`, viene dal motore stesso, prima ancora che venga eseguito qualsiasi script. È per questo che dalla terza alla sesta lezione hai potuto leggere i tasti freccia senza caricare niente. Il framework aggiunge a ciò che c'è già; non lo fornisce. Questo gioco in particolare non usa niente del framework. La riga è qui perché ogni gioco dalla nona lezione alla fine del libro ne ha bisogno, e perché è così che cominciano quasi tutti i veri giochi Singe. Viene letta una volta, all'avvio, e poi dimenticata. La seconda cosa nuova è il modo in cui vengono gestiti i tasti freccia, perché è la prima cosa che i principianti sbagliano. `onInputPressed` non muove il giocatore. Imposta `goingLeft` a `true`, e `onInputReleased` lo rimette a `false`. Il movimento avviene in `onOverlayUpdate`, che sessanta volte al secondo chiede «il giocatore sta tenendo premuto sinistra?». Se invece muovessi il giocatore dentro `onInputPressed`, otterresti un passo per pressione: Singe invia una pressione esattamente una volta, quando il tasto va giù, e non la ripete mai, come dice chiaramente la voce di `onInputPressed` nel Riferimento. Tenere premuto il tasto non farebbe proprio niente. Quindi il gestore della pressione registra ciò che è vero nel mondo, e l'aggiornamento decide che cosa farne. Mantieni questa divisione e il tuo codice per i comandi resterà lungo cinque righe per sempre. I quattro nomi in maiuscolo sono variabili normali. Scrivere `PLAYER_SPEED` in maiuscolo è un messaggio a te stesso e a chiunque legga: questo viene impostato una volta in cima e non cambia mai. Niente in Lua lo impone. Conta perché fra un minuto ce ne saranno dieci, e quando il gioco è troppo difficile vuoi un posto ovvio dove andare ad abbassare un numero. `screenWidth` e `screenHeight` vengono dal motore invece di essere scritti a mano, così il gioco si dispone da sé secondo la dimensione che l'overlay si trova ad avere. `playerY` viene calcolato una volta sola dall'altezza: la barra sta a `PLAYER_MARGIN` pixel dal fondo e non si muove mai né su né giù, quindi non c'è motivo di ricalcolarlo a ogni fotogramma. Il `- 1` nella riga di `overlayBox` non è un errore di battitura. `overlayBox` disegna fino a entrambi gli angoli che gli dai, compresi, quindi un riquadro da `playerX` a `playerX + 40` è largo quarantuno pixel. Togliere uno rende la barra che vedi larga esattamente quaranta, e conterà nella quarta fase, quando qualcosa dovrà decidere se ti ha toccato. [#_stage_two_something_to_dodge] === Seconda fase: qualcosa da schivare Un blocco che cade. Aggiungi due costanti all'elenco in cima: [source,lua] ---- local BLOCK_SIZE = 12 local BLOCK_SPEED = 1.5 ---- Altre due variabili sotto `goingRight`: [source,lua] ---- local blockX = math.random(0, screenWidth - BLOCK_SIZE) local blockY = -BLOCK_SIZE ---- In `onOverlayUpdate`, subito sopra la riga `overlayClear()`, fallo cadere: [source,lua] ---- blockY = blockY + BLOCK_SPEED if blockY > screenHeight then blockX = math.random(0, screenWidth - BLOCK_SIZE) blockY = -BLOCK_SIZE end ---- E sotto la riga che disegna il giocatore, disegnalo: [source,lua] ---- colorForeground(255, 90, 90) overlayBox(blockX, blockY, blockX + BLOCK_SIZE - 1, blockY + BLOCK_SIZE - 1) ---- Salva. Un quadrato rosso cade, raggiunge il fondo e ricompare in cima in un punto nuovo. Parte da `-BLOCK_SIZE`, che è sopra il bordo superiore dello schermo. Non c'è niente di sbagliato in una posizione negativa: il motore disegna la parte del riquadro che sta sull'overlay e butta via in silenzio quella che non c'è. Il blocco scivola nella visuale invece di spuntare dal nulla. `BLOCK_SPEED` vale `1.5`, non un numero intero, quindi `blockY` passa gran parte della sua vita a valere qualcosa come `83.5`. Va bene anche questo. Il disegno taglia via la parte decimale, e tenerla nella variabile è ciò che permette al blocco di muoversi più lentamente di un pixel a fotogramma. [#_stage_three_a_sky_full_of_them] === Terza fase: un cielo pieno di blocchi Un blocco non è un gioco. Nella sesta lezione hai imparato a tenere tanti esemplari di qualcosa in un elenco, e questo è ciò a cui serviva. Cancella `blockX` e `blockY`, e al loro posto metti questi: [source,lua] ---- local blocks = {} local spawnTimer = 0 ---- Aggiungi un'altra costante in cima: [source,lua] ---- local SPAWN_FRAMES = 40 ---- Ora una funzione che crea un blocco e lo mette nell'elenco. Mettila sopra `onInputPressed`: [source,lua] ---- function spawnBlock() local block = {} block.x = math.random(0, screenWidth - BLOCK_SIZE) block.y = -BLOCK_SIZE table.insert(blocks, block) end ---- Sostituisci il codice della caduta della seconda fase con questo: [source,lua] ---- spawnTimer = spawnTimer - 1 if spawnTimer <= 0 then spawnBlock() spawnTimer = SPAWN_FRAMES end for i = #blocks, 1, -1 do local block = blocks[i] block.y = block.y + BLOCK_SPEED if block.y > screenHeight then table.remove(blocks, i) end end ---- E sostituisci l'unica riga che disegnava il blocco con un ciclo su tutti quanti: [source,lua] ---- for i, block in ipairs(blocks) do overlayBox(block.x, block.y, block.x + BLOCK_SIZE - 1, block.y + BLOCK_SIZE - 1) end ---- Salva. Piovono blocchi. Un blocco è un record: una piccola tabella con dentro una `x` e una `y`, che è il modo di evitare che due numeri che stanno insieme vadano ognuno per conto suo. `blocks` è un elenco di questi record, che è la forma che ha il mondo di quasi ogni gioco. `spawnTimer` conta all'indietro di uno a ogni fotogramma, e quando arriva a zero nasce un blocco e il timer viene ricaricato. Quaranta fotogrammi sono circa due terzi di secondo. In questo gioco non c'è un orologio e non ce n'è bisogno: `onOverlayUpdate` è il battito del cuore, e contare i battiti è un modo perfettamente valido di misurare il tempo. Il ciclo della caduta va *all'indietro* -- da `#blocks` giù fino a `1`, un passo alla volta -- perché toglie cose mentre procede. È la regola della sesta lezione, ed è la prima volta che hai un vero motivo per rispettarla. Togli l'elemento tre mentre vai avanti e tutto scivola giù di uno, così il blocco che era l'elemento quattro ora è l'elemento tre e il ciclo non lo guarda mai. Andando all'indietro, tutto ciò che si sposta è già stato sistemato. Il ciclo del disegno invece usa `ipairs`, perché non toglie niente e vuole solo ogni blocco a turno. [#_stage_four_getting_hit] === Quarta fase: farsi colpire I blocchi ti passano attraverso. È ora di accorgersene. Il motore ha una funzione proprio per questa domanda. Cambia il controllo dentro il ciclo della caduta da così: [source,lua] ---- if block.y > screenHeight then table.remove(blocks, i) end ---- a così: [source,lua] ---- if collideRects(block.x, block.y, BLOCK_SIZE, BLOCK_SIZE, playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT) or block.y > screenHeight then table.remove(blocks, i) end ---- Salva, e porta la barra contro un blocco che cade. Sparisce. `collideRects` prende due rettangoli e risponde `true` se si sovrappongono. Ogni rettangolo è fatto di quattro numeri: un angolo, poi una larghezza e un'altezza -- *non* due angoli, che è l'altro modo ragionevole di descrivere un rettangolo e quello usato da `overlayBox`. La voce di `collideRects` nel Riferimento dà l'ordine, e vale la pena guardarla adesso, perché mettere gli otto argomenti nell'ordine sbagliato è il tipo di errore che non produce nessun messaggio, solo un gioco in cui niente colpisce mai niente. Potresti scriverla da te. Sono quattro confronti, e non è davvero difficile. Usa comunque quella del motore, per tre motivi. È corretta, anche nei casi scomodi: un rettangolo senza larghezza, due rettangoli che si toccano solo lungo un bordo. Dice quello che intende, così la riga si legge «questi due si sono sovrapposti?» invece di quattro confronti uniti da `and` da decifrare ogni volta che ci torni. E quando arriverai all'undicesima lezione, dove le cose si colpiscono di continuo, conoscerai già la famiglia: `collidePointRect` per un clic del mouse, `collideCircles` per due cose rotonde e `collideSegments` per un colpo contro un muro. Il Riferimento le elenca insieme. L'unica cosa che il motore non può fare per te è decidere che cosa *significa* un rettangolo. Qui il rettangolo del blocco è il blocco intero. In un vero gioco spesso vuoi che il rettangolo di collisione sia un po' più piccolo dell'immagine, così un colpo mancato di poco sembra proprio un colpo mancato di poco. È una decisione di design, e l'undicesima lezione è dove la prendi. [#_stage_five_score_and_lives] === Quinta fase: punteggio e vite Essere colpiti dovrebbe costare qualcosa, e sopravvivere dovrebbe valere qualcosa. Due costanti in più in cima: [source,lua] ---- local BLOCK_FASTER = 0.05 local START_LIVES = 3 ---- Tre variabili in più, accanto a `spawnTimer`: [source,lua] ---- local blockSpeed = BLOCK_SPEED local score = 0 local lives = START_LIVES ---- `BLOCK_SPEED` ora è la velocità a cui il gioco *comincia*, e `blockSpeed` è la velocità a cui sta andando in questo momento. Cambia la riga della caduta perché usi la variabile: [source,lua] ---- block.y = block.y + blockSpeed ---- Poi separa i due casi, perché ora fanno cose diverse: [source,lua] ---- if collideRects(block.x, block.y, BLOCK_SIZE, BLOCK_SIZE, playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT) then table.remove(blocks, i) lives = lives - 1 elseif block.y > screenHeight then table.remove(blocks, i) score = score + 1 blockSpeed = blockSpeed + BLOCK_FASTER end ---- E mostra i numeri. Metti questo subito prima del `return`: [source,lua] ---- overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives) ---- Salva, e giocaci per un minuto. Diventa più difficile, ed è tutto lì il trucco: ogni blocco che schivi fa cadere i successivi più veloci di un ventesimo di pixel a fotogramma. Dopo venti punti sono visibilmente più rapidi. Non hai dovuto scrivere una sola parola sui livelli di difficoltà. Vedrai anche `LIVES` arrivare a `-4`, perché ancora niente ferma il gioco. È la prossima fase, ed è quella che conta di più. [#_stage_six_game_over_and_going_again] === Sesta fase: game over e una nuova partita Ecco la trappola. Il modo ovvio per aggiungere una schermata di game over è una variabile chiamata `gameOver`, impostata a `true` quando finiscono le vite. Poi vuoi una schermata del titolo prima della prima partita, quindi aggiungi `started`. Poi vuoi un tasto di pausa, quindi aggiungi `paused`. Ora hai tre variabili vero-o-falso, otto combinazioni tra loro, e cinque di quelle combinazioni sono assurdità che il tuo codice deve stare attento a non produrre mai. Ogni principiante scrive questo gioco almeno una volta, ed è il posto dove i piccoli giochi vanno a morire. Fai invece così. Una variabile contiene *che cosa sta facendo il gioco*, sotto forma di parola: [source,lua] ---- local state = "waiting" ---- Vale `"waiting"` prima della prima partita, `"playing"` durante una partita e `"over"` quando le vite sono finite. Tre stati, una variabile, e nessuna combinazione impossibile. Le stringhe non hanno niente di magico; sono parole che hai scelto tu, e devi scriverle sempre allo stesso modo. Ora la funzione che comincia una partita. Ogni variabile che appartiene a una partita torna qui al suo valore iniziale, in un posto solo: [source,lua] ---- function startGame() playerX = (screenWidth - PLAYER_WIDTH) / 2 blocks = {} blockSpeed = BLOCK_SPEED spawnTimer = 0 score = 0 lives = START_LIVES state = "playing" end ---- Perdere l'ultima vita la fa finire. Dentro il ramo della collisione, sotto `lives = lives - 1`: [source,lua] ---- if lives == 0 then state = "over" end ---- La barra spaziatrice comincia una partita, ma solo quando non ce n'è già una in corso. Aggiungi un ramo a `onInputPressed`: [source,lua] ---- elseif what == SWITCH_BUTTON1 and state ~= "playing" then startGame() ---- `~=` vuol dire «è diverso da». `state ~= "playing"` è vero durante l'attesa e vero a partita finita, che sono esattamente i due momenti in cui la barra spaziatrice deve cominciare una partita. `SWITCH_BUTTON1` è la barra spaziatrice per impostazione predefinita, e anche il pulsante A di un controller. Due piccole funzioni mettono le parole sullo schermo. `printCentered` fa il calcolo una volta sola, così che la schermata del titolo e quella di game over non debbano farlo male ciascuna per conto suo: [source,lua] ---- function drawOverText() printCentered(7, "GAME OVER") printCentered(9, "PRESS SPACE TO PLAY AGAIN") end function drawWaitingText() printCentered(6, "DODGE THE BLOCKS") printCentered(8, "ARROW KEYS TO MOVE") printCentered(10, "PRESS SPACE TO START") end function printCentered(row, text) overlayPrint(math.floor((TEXT_COLUMNS - #text) / 2), row, text) end ---- con un'altra costante in cima: [source,lua] ---- local TEXT_COLUMNS = overlayGetWidth() // overlayGetFontWidth() ---- Infine, la parte importante. Sposta tutto ciò che disegna fuori da `onOverlayUpdate`, in una funzione chiamata `drawGame`, sposta tutto ciò che muove in una funzione chiamata `updatePlaying` -- sono tutte e due nel listato completo qui sotto -- e lascia `onOverlayUpdate` così: [source,lua] ---- function onOverlayUpdate() if state == "playing" then updatePlaying() end overlayClear() drawGame() if state == "waiting" then drawWaitingText() elseif state == "over" then drawOverText() end return OVERLAY_UPDATED end ---- Salva. Hai un gioco: una schermata del titolo, una partita, un game over e un modo per ricominciare. Rileggi quelle tredici righe, perché sono la forma di ogni gioco che scriverai. Il mondo si muove solo mentre lo stato è `"playing"`, quindi la schermata di game over si congela con gli ultimi blocchi sospesi esattamente dov'erano. Il mondo viene disegnato a ogni fotogramma qualunque sia lo stato, perché l'immagine è la stessa in tutti e tre; lo stato decide solo quali parole ci vanno sopra. E c'è esattamente un posto che prende questa decisione. Ecco che cosa ti sei comprato con la variabile `state`. Quando aggiungerai una pausa, aggiungerai un ramo a quell'istruzione `if` e una riga a `onInputPressed`, e nient'altro nel gioco avrà bisogno di saperne qualcosa. [#_the_whole_thing_6] === Tutto insieme Eccolo, finito. Se il tuo non corrisponde, vale la pena trovare la differenza. [source,lua] ---- dofile("Singe/Framework.singe") local PLAYER_WIDTH = 40 local PLAYER_HEIGHT = 8 local PLAYER_SPEED = 4 local PLAYER_MARGIN = 4 local BLOCK_SIZE = 12 local BLOCK_SPEED = 1.5 local BLOCK_FASTER = 0.05 local SPAWN_FRAMES = 40 local START_LIVES = 3 local TEXT_COLUMNS = overlayGetWidth() // overlayGetFontWidth() local screenWidth = overlayGetWidth() local screenHeight = overlayGetHeight() local playerY = screenHeight - PLAYER_HEIGHT - PLAYER_MARGIN local playerX = (screenWidth - PLAYER_WIDTH) / 2 local goingLeft = false local goingRight = false local blocks = {} local blockSpeed = BLOCK_SPEED local spawnTimer = 0 local score = 0 local lives = START_LIVES local state = "waiting" function drawGame() colorForeground(80, 255, 160) overlayBox(playerX, playerY, playerX + PLAYER_WIDTH - 1, playerY + PLAYER_HEIGHT - 1) colorForeground(255, 90, 90) for i, block in ipairs(blocks) do overlayBox(block.x, block.y, block.x + BLOCK_SIZE - 1, block.y + BLOCK_SIZE - 1) end overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives) end function drawOverText() printCentered(7, "GAME OVER") printCentered(9, "PRESS SPACE TO PLAY AGAIN") end function drawWaitingText() printCentered(6, "DODGE THE BLOCKS") printCentered(8, "ARROW KEYS TO MOVE") printCentered(10, "PRESS SPACE TO START") end function printCentered(row, text) overlayPrint(math.floor((TEXT_COLUMNS - #text) / 2), row, text) end function spawnBlock() local block = {} block.x = math.random(0, screenWidth - BLOCK_SIZE) block.y = -BLOCK_SIZE table.insert(blocks, block) end function startGame() playerX = (screenWidth - PLAYER_WIDTH) / 2 blocks = {} blockSpeed = BLOCK_SPEED spawnTimer = 0 score = 0 lives = START_LIVES state = "playing" end function updatePlaying() if goingLeft then playerX = playerX - PLAYER_SPEED end if goingRight then playerX = playerX + PLAYER_SPEED end if playerX < 0 then playerX = 0 elseif playerX > screenWidth - PLAYER_WIDTH then playerX = screenWidth - PLAYER_WIDTH end spawnTimer = spawnTimer - 1 if spawnTimer <= 0 then spawnBlock() spawnTimer = SPAWN_FRAMES end for i = #blocks, 1, -1 do local block = blocks[i] block.y = block.y + blockSpeed if collideRects(block.x, block.y, BLOCK_SIZE, BLOCK_SIZE, playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT) then table.remove(blocks, i) lives = lives - 1 if lives == 0 then state = "over" end elseif block.y > screenHeight then table.remove(blocks, i) score = score + 1 blockSpeed = blockSpeed + BLOCK_FASTER end end end function onInputPressed(what) if what == SWITCH_LEFT then goingLeft = true elseif what == SWITCH_RIGHT then goingRight = true elseif what == SWITCH_BUTTON1 and state ~= "playing" then startGame() end end function onInputReleased(what) if what == SWITCH_LEFT then goingLeft = false elseif what == SWITCH_RIGHT then goingRight = false end end function onOverlayUpdate() if state == "playing" then updatePlaying() end overlayClear() drawGame() if state == "waiting" then drawWaitingText() elseif state == "over" then drawOverText() end return OVERLAY_UPDATED end ---- Circa centoventi righe di codice, e le hai scritte tutte poche alla volta. [#_what_just_happened_4] === Cos'è appena successo Alcuni pezzi meritano una seconda occhiata. [source,lua] ---- local blocks = {} local score = 0 function startGame() score = 0 end ---- I dati sono `local` e le funzioni no. Sono due scelte volute, e opposte. I dati sono `local` perché appartengano a questo file e niente da fuori possa entrare a cambiarli. Le funzioni sono globali perché possano chiamarsi a vicenda in qualsiasi ordine: `drawOverText` è definita prima di `printCentered` e la chiama lo stesso, perché un nome globale viene cercato nel momento della chiamata, e a quel punto l'intero file è già stato letto. Rendi `local` quelle funzioni e il file dovrà essere ordinato in modo che niente venga usato prima di comparire, una regola su cui inciamperesti per tutto il resto del libro. Tre di questi nomi non hanno proprio scelta: `onOverlayUpdate` e le due callback dei comandi devono essere globali, perché è così che Singe le trova. Dentro il file l'ordine è alfabetico, con le tre callback per ultime. Non è una regola di Lua. Serve a farti trovare `spawnBlock` fra sei mesi senza leggere niente. [source,lua] ---- overlayPrint(math.floor((TEXT_COLUMNS - #text) / 2), row, text) ---- `+#text+` è la lunghezza di una stringa, in caratteri. Hai incontrato `+#+` nella sesta lezione, mentre contava gli elementi di un elenco, e qui fa lo stesso lavoro. `"GAME OVER"` è lungo nove caratteri, quindi su uno schermo di sessanta colonne comincia alla colonna venticinque, il che lascia venticinque celle di spazio alla sua sinistra e ventisei alla sua destra. `math.floor` butta via la parte decimale, perché non esiste una colonna venticinque e mezzo. `TEXT_COLUMNS` calcola quante celle di carattere stanno in larghezza sullo schermo. `overlayPrint` conta in celle mentre tutto il resto conta in pixel, quindi la larghezza dell'overlay viene divisa per la larghezza di una cella, ed è `overlayGetFontWidth` a dirtela. Due barre invece di una, perché una frazione di colonna non è un posto dove stampare. Calcolalo così e la centratura resta giusta con qualsiasi dimensione dell'overlay, che è la quarta cosa di *Prova tu*. [source,lua] ---- colorForeground(255, 90, 90) ---- `colorForeground` imposta un colore che resta impostato. Non è un argomento di `overlayBox`; è un interruttore che fai scattare, e tutto ciò che disegni dopo esce in quel colore finché non lo fai scattare di nuovo. È per questo che `drawGame` imposta il verde, disegna il giocatore, imposta il rosso e poi disegna tutti i blocchi senza più nominare il colore. Non influisce su `overlayPrint`. Il font della console viene copiato sull'overlay esattamente com'è, nei suoi colori, come dice testualmente la voce di `overlayPrint` nel Riferimento. Il testo colorato ha bisogno di un font caricato, e questa è la diciannovesima lezione. [#_try_it_7] === Prova tu . *Rendilo più facile, poi ingiusto.* Cambia `PLAYER_WIDTH` in `80` e gioca una partita. Poi cambialo in `12`, la stessa dimensione di un blocco. Nota che per fare l'una o l'altra cosa non hai dovuto toccare né il disegno né la collisione. . *Accelera più in fretta.* Cambia `BLOCK_FASTER` da `0.05` a `0.5` e guarda quanti punti riesci a fare. Poi prova `0`, e chiediti se vale ancora la pena di giocarci. . *Una vita in più.* Restituisci una vita al giocatore ogni dieci punti. Tutto ciò che ti serve è già in `updatePlaying`, nel ramo che aumenta il punteggio. . *Giocalo più grande.* Avvialo con `Singe -R --canvas=1280x720 dodge`. Il gioco si dispone correttamente, testo centrato compreso, perché ha chiesto al motore quanto è grande lo schermo. Anche `TEXT_COLUMNS` l'ha chiesto. . *Mettilo in pausa.* Aggiungi un quarto stato, `"paused"`. `SWITCH_BUTTON2` dovrebbe passare da `"playing"` a `"paused"` e viceversa, e durante la pausa non dovrebbe muoversi niente. Fatto bene, sono tre righe e non devi toccare `updatePlaying` per niente. [#_break_it_on_purpose_7] === Rompilo apposta Vai in `spawnBlock` e cancella la riga che imposta `block.y`, così che diventi: [source,lua] ---- function spawnBlock() local block = {} block.x = math.random(0, screenWidth - BLOCK_SIZE) table.insert(blocks, block) end ---- Salva, premi la barra spaziatrice, e Singe si ferma con questo: ---- Error executing function 'onOverlayUpdate': dodge/dodge.singe:97: attempt to perform arithmetic on a nil value (field 'y') stack traceback: dodge/dodge.singe:97: in function 'updatePlaying' dodge/dodge.singe:135: in function 'onOverlayUpdate' ---- La riga 97 è `block.y = block.y + blockSpeed`. La lamentela è che qualcosa in quella somma non è un numero, e la parte tra parentesi ti dice che cosa: `field 'y'`, cioè la `y` dentro una tabella. Non ha un valore, perché non gliel'hai mai dato, e una cosa senza valore in Lua è `nil`. Nota dov'è l'errore e dov'è lo sbaglio. L'errore è in `updatePlaying`, a quaranta righe da `spawnBlock`, dove non c'è proprio niente che non va. `updatePlaying` è solo il primo pezzo di codice abbastanza sfortunato da toccare il danno. Questo è il caso normale, non l'eccezione, e seguire quella pista a ritroso è un'abilità. Rimetti la riga. Poi gira pagina, perché la prossima lezione parla solo di questo. [#_what_you_learned_7] === Cosa hai imparato * Un gioco è un mondo che cambia, un mondo che viene disegnato e una decisione su quale delle due cose fare -- e sono tre pezzi di codice separati. * I gestori dei comandi dovrebbero registrare che cosa è tenuto premuto; l'aggiornamento dovrebbe decidere che cosa significa. * Un elenco di record è la forma di quasi ogni mondo di gioco. * Togli le cose da un elenco percorrendolo all'indietro. * `collideRects` risponde se due rettangoli si sovrappongono, e la versione del motore è migliore della tua perché è corretta, si legge bene e ha una famiglia che incontrerai di nuovo. * Una variabile `state` che contiene una parola batte tre indicatori vero-o-falso, e continua a batterli man mano che il gioco cresce. * Comincia una partita da una funzione che azzera tutto, così la seconda partita è esattamente come la prima. * I numeri che potresti voler regolare vanno in costanti con un nome, in cima al file. * Costruisci un gioco per fasi che funzionano ciascuna. Un programma che non ha mai funzionato è molto più difficile da correggere di uno che funzionava cinque minuti fa. [#_next_time_7] === La prossima volta Ora hai scritto abbastanza codice da fare errori interessanti. La prossima lezione è quella di cui nessun libro per principianti si preoccupa: che cosa dicono davvero i messaggi di errore di Singe, come scoprire che cosa contiene davvero una variabile invece di tirare a indovinare, e come tagliare un problema a metà finché non gli resta nessun posto dove nascondersi.