861 lines
29 KiB
Text
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.
|