singe/docs/it/lessons/07-a-game.adoc

861 lines
29 KiB
Text

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