789 lines
28 KiB
Text
789 lines
28 KiB
Text
[#_lesson_9_pictures]
|
|
== Lezione 9: Immagini
|
|
|
|
image::learn/09-pictures.png[La lezione finita, 480]
|
|
|
|
Tutto quello che hai disegnato finora l'hai disegnato con riquadri, linee e il
|
|
testo semplice del motore. Era voluto: otto lezioni senza grafica volevano dire
|
|
otto lezioni in cui niente poteva andare storto tranne il tuo programma. Ora i
|
|
riquadri diventano un'astronave e qualche roccia.
|
|
|
|
Un'immagine che il motore disegna sull'overlay si chiama *sprite*. Questa
|
|
lezione li carica, li disegna, li misura, li mette nell'ordine giusto e scorre
|
|
un'immagine che contiene diversi fotogrammi di un'animazione. Alla fine, il
|
|
gioco della settima lezione sembrerà un gioco.
|
|
|
|
[#_the_art_kit]
|
|
=== Il kit di grafica
|
|
|
|
Non devi disegnare niente. Questo libro viene con una piccola cartella di
|
|
grafica, e tutto ciò che c'è in questa lezione e nella prossima la usa.
|
|
|
|
Trova la cartella `art` che è arrivata con il libro e copia l'intera cartella
|
|
nella cartella di questa lezione, accanto al tuo script. Dovresti ottenere
|
|
questo:
|
|
|
|
----
|
|
singe/ <- la tua cartella di lavoro
|
|
Singe/
|
|
data/
|
|
rocks/
|
|
rocks.singe
|
|
art/
|
|
spaceship.png
|
|
asteroid.png
|
|
bullet.png
|
|
star.png
|
|
walk.png
|
|
shoot.wav
|
|
boom.wav
|
|
----
|
|
|
|
La tua grafica va dentro la cartella del tuo gioco, e la cartella `Singe` del
|
|
motore resta dov'è, fuori nella cartella di lavoro. Questo conta tra un momento,
|
|
perché decide come indichi un file quando lo carichi.
|
|
|
|
La cartella `Singe` è quella che il motore ha estratto per te al tuo primissimo
|
|
avvio, nella prima lezione. Vale la pena sapere che cosa contiene, perché anche
|
|
quei file li puoi usare, e ogni lettore di questo libro li ha:
|
|
|
|
* `Singe/click.wav` -- un breve clic, che il menu del motore usa per la sua
|
|
prova del suono.
|
|
* `Singe/FreeSansBold.ttf` -- un font TrueType, per la diciannovesima lezione.
|
|
* `Singe/menuIntro.flac` -- la musica che suona il menu del motore. La userai
|
|
nella decima lezione.
|
|
* `Singe/menuBackground.mkv` -- un video, per la quattordicesima lezione.
|
|
* `Singe/DragonModel.glb` e `Singe/SingeText.glb` -- due modelli 3D, per la
|
|
ventitreesima lezione.
|
|
* `Singe/missing.png` -- l'immagine che il motore disegna quando un gioco ne
|
|
chiede una che non riesce a trovare.
|
|
|
|
Le immagini del kit sono piccole apposta. L'astronave è larga 32 pixel e alta
|
|
24, una roccia è 24 per 24, un colpo è 4 per 10 e una stella è 8 per 8. Sembra
|
|
minuscolo finché non ti ricordi quanto è grande davvero lo schermo, che è la
|
|
prossima cosa da chiarire.
|
|
|
|
[#_how_big_the_screen_is]
|
|
=== Quanto è grande lo schermo
|
|
|
|
Senza un video da riprodurre, Singe dà al tuo gioco un canvas largo 720 pixel e
|
|
alto 480, e la superficie su cui disegni -- l'*overlay* -- è la metà in ciascuna
|
|
direzione: *360 per 240*. Singe lo stira per riempire la finestra al posto tuo,
|
|
quindi un'astronave di 32 pixel occupa circa un decimo della larghezza dello
|
|
schermo, che è più o meno l'aspetto di un'astronave in un gioco da sala giochi.
|
|
|
|
Non scrivere mai 360 e 240 nel tuo programma. Chiedi:
|
|
|
|
[source,lua]
|
|
----
|
|
local screenWidth = overlayGetWidth()
|
|
local screenHeight = overlayGetHeight()
|
|
----
|
|
|
|
Quelle due riportano la dimensione dell'overlay, e un gioco che chiede è un
|
|
gioco che funziona ancora quando qualcuno lo avvia a una dimensione diversa.
|
|
|
|
C'è una cosa che non conta in quei pixel: `overlayPrint`, della prima lezione,
|
|
conta in celle di carattere. Ogni cella è larga 6 pixel e alta 13, quindi un
|
|
overlay di 360 per 240 fa 60 colonne per 18 righe di testo. Gli sprite sono in
|
|
pixel, `overlayPrint` è in celle, e confondere le due cose vale un pomeriggio di
|
|
confusione, se glielo permetti.
|
|
|
|
[#_one_picture_on_screen]
|
|
=== Un'immagine sullo schermo
|
|
|
|
Comincia un nuovo file accanto alla tua cartella `art` e scrivici questo.
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
|
|
local ship = spriteLoad(DIR .. "art/spaceship.png")
|
|
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
spriteDraw(ship, 160, 100)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
function onShutdown()
|
|
spriteUnload(ship)
|
|
end
|
|
----
|
|
|
|
Avvialo. Una piccola astronave blu sta vicino al centro di uno schermo nero, con
|
|
la punta verso l'alto.
|
|
|
|
[#_what_just_happened_5]
|
|
=== Cos'è appena successo
|
|
|
|
Quattro cose nuove, una alla volta.
|
|
|
|
[source,lua]
|
|
----
|
|
local ship = spriteLoad(DIR .. "art/spaceship.png")
|
|
----
|
|
|
|
`spriteLoad` legge un file di immagine dal disco, lo decodifica e tiene
|
|
l'immagine in memoria. Quello che ti restituisce non è l'immagine. È un
|
|
*handle*: un semplice numero, un biglietto d'ingresso, il modo del motore per
|
|
dire «quella lì». Ridai quel numero al motore ogni volta che vuoi fare qualcosa
|
|
con l'immagine, e non ti serve mai sapere che numero sia, né preoccupartene.
|
|
|
|
Stampalo, se vuoi -- `debugPrint(ship)` dell'ottava lezione ti mostrerà un
|
|
numero piccolo -- ma il numero in sé non è affar tuo, e niente nel tuo programma
|
|
dovrebbe mai dipendere da quale numero risulti. L'unica regola è che lo tieni da
|
|
qualche parte dove puoi riprenderlo, che è esattamente ciò a cui serve una
|
|
variabile.
|
|
|
|
`DIR` è nuovo anche lui, e risolve un problema creato dalla struttura che usi
|
|
dalla prima lezione. Singe cerca i file a partire dalla cartella in cui ti
|
|
trovavi quando l'hai avviato, *non* a partire dal tuo script. Tu lo avvii dalla
|
|
cartella di lavoro, quindi un semplice `art/spaceship.png` manda il motore a
|
|
cercare `singe/art/spaceship.png`, là fuori accanto alla cartella `Singe`. La
|
|
tua grafica non è lì. Sta un livello più giù, in `rocks/art/spaceship.png`,
|
|
con il tuo script.
|
|
|
|
`DIR` colma quel divario. È un pezzo di testo che contiene la cartella in cui si
|
|
trova il tuo script, con la barra già in fondo, quindi
|
|
`DIR .. "art/spaceship.png"` -- che unisce due pezzi di testo con `..`, come fai
|
|
dalla prima parte -- indica l'immagine correttamente comunque sia stato avviato
|
|
il gioco, e continua a indicarla correttamente quando sposti l'intera cartella
|
|
da un'altra parte.
|
|
|
|
`DIR` è uno dei nomi che definisce `Singe/Framework.singe`, ed è a questo che
|
|
serve la riga `dofile` in cima al tuo script. Hai incontrato quella riga, e ciò
|
|
che contiene, nella settima lezione, e questo è il momento in cui si guadagna il
|
|
posto: senza di lei non c'è `DIR`, e senza `DIR` il motore non trova la tua
|
|
astronave.
|
|
|
|
Usa `DIR` per ogni file che il tuo gioco carica, da qui alla fine del libro.
|
|
|
|
[source,lua]
|
|
----
|
|
spriteDraw(ship, 160, 100)
|
|
----
|
|
|
|
Disegna lo sprite con quell'handle, in quella posizione. Come ogni chiamata di
|
|
disegno, funziona solo dentro `onOverlayUpdate`.
|
|
|
|
La posizione è l'*angolo in alto a sinistra* dell'immagine, non il suo centro.
|
|
Chiedi `(160, 100)` e il pixel più in alto e più a sinistra dell'astronave
|
|
atterra lì, e il resto pende verso il basso e verso destra. Questo frega tutti
|
|
una volta, di solito quando provano a centrare qualcosa e viene fuori spostato
|
|
in basso a destra di metà della sua dimensione.
|
|
|
|
Se preferisci indicare il centro, dillo:
|
|
|
|
[source,lua]
|
|
----
|
|
spriteDraw(ship, 160, 100, true)
|
|
----
|
|
|
|
Un quarto argomento `true` vuol dire «tratta quel punto come il centro». Tutte e
|
|
due le forme sono nella voce di `spriteDraw` nel Riferimento, insieme ad altre
|
|
due che stirano l'immagine dentro un rettangolo.
|
|
|
|
[source,lua]
|
|
----
|
|
function onShutdown()
|
|
spriteUnload(ship)
|
|
end
|
|
----
|
|
|
|
`onShutdown` è una callback, come `onOverlayUpdate` e le due callback dei
|
|
comandi della terza lezione. Il motore la chiama una volta, quando il tuo gioco
|
|
sta finendo. È lì che restituisci quello che hai preso: `spriteUnload` libera
|
|
l'immagine e butta via l'handle.
|
|
|
|
A rigore, potresti ometterla. Il motore libera tutto ciò che il tuo script ha
|
|
caricato quando il gioco finisce, quindi uno `spriteUnload` dimenticato non è
|
|
una perdita di memoria. Scrivila comunque. Costa una riga, dice che cosa possiede
|
|
il tuo programma, e fra un minuto vedrai il caso in cui dimenticarsi di scaricare
|
|
conta davvero.
|
|
|
|
Un avvertimento su quell'handle: una volta scaricato uno sprite, il numero è
|
|
morto. Disegnare con quel numero non si limita a non disegnare niente: termina
|
|
il tuo gioco con un errore.
|
|
|
|
[#_transparency_and_why_it_is_a_png]
|
|
=== La trasparenza, e perché è un PNG
|
|
|
|
L'astronave è un triangolo con due alette. Il file in cui vive è un rettangolo,
|
|
32 per 24, perché ogni file di immagine è un rettangolo. Quindi che fine hanno
|
|
fatto gli angoli?
|
|
|
|
Sono *trasparenti*. Un PNG può dire, per ogni singolo pixel, quanto è solido, e
|
|
le immagini del kit dicono «proprio non c'è» per ogni pixel fuori dalla forma.
|
|
Singe lo rispetta, quindi lo sfondo nero si vede attraverso gli angoli e
|
|
l'astronave ha un contorno invece di un riquadro intorno.
|
|
|
|
È per questo che il kit è in PNG e non in JPEG. Un JPEG non può proprio
|
|
memorizzare la trasparenza -- ti darebbe l'astronave in un riquadro grigio -- e
|
|
in più sbava i bordi netti, che è l'ultima cosa di cui ha bisogno la grafica
|
|
piccola. Per la grafica di un gioco, PNG. Per una fotografia che riempie tutto
|
|
lo schermo e non ha parti trasparenti, JPEG va bene ed è molto più piccolo. La
|
|
voce di `spriteLoad` nel Riferimento elenca tutti i formati che Singe sa leggere, e
|
|
sono parecchi.
|
|
|
|
Provalo tu stesso: cambia il nero con qualcos'altro disegnando un riquadro
|
|
dietro l'astronave prima di disegnare l'astronave. Gli angoli mostreranno
|
|
qualunque cosa ci sia dietro, che è proprio il punto.
|
|
|
|
[#_do_not_load_while_you_draw]
|
|
=== Non caricare mentre disegni
|
|
|
|
Ecco l'errore che questa sezione esiste per impedire. Sembra del tutto
|
|
ragionevole:
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
local ship = spriteLoad(DIR .. "art/spaceship.png")
|
|
spriteDraw(ship, 160, 100)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Eseguilo e funziona. Disegna l'astronave. Niente si lamenta. Non avresti nessun
|
|
motivo di pensare che qualcosa non vada.
|
|
|
|
`onOverlayUpdate` gira circa sessanta volte al secondo. Sono sessanta copie nuove
|
|
dell'astronave caricate in memoria ogni secondo, tremilaseicento al minuto, e
|
|
nessuna viene mai scaricata. Il gioco gira benissimo per due minuti, poi
|
|
rallenta, poi la macchina resta senza memoria e il gioco muore. Peggio ancora,
|
|
muore durante la demo e non mentre lo stavi provando.
|
|
|
|
La regola: *carica una volta, disegna molte volte*. Caricare è lento e occupa
|
|
memoria. Disegnare è veloce e non ne occupa. Quindi `spriteLoad` va in cima al
|
|
tuo file, fuori da ogni funzione, dove viene eseguito una volta sola all'avvio
|
|
dello script.
|
|
|
|
Non riguarda solo gli sprite. Ogni `somethingLoad` del motore funziona così --
|
|
i suoni nella decima lezione, i font nella diciannovesima, i modelli nella
|
|
ventitreesima. Se ti ritrovi a scrivere `Load` dentro una funzione che gira a
|
|
ogni fotogramma, fermati e spostalo fuori.
|
|
|
|
[#_how_big_is_it]
|
|
=== Quanto è grande?
|
|
|
|
Sai che l'astronave è 32 per 24 perché te l'ha detto questo libro. Il tuo
|
|
programma non lo sa, e non dovrebbe averne bisogno. Chiedi:
|
|
|
|
[source,lua]
|
|
----
|
|
local shipWidth = spriteGetWidth(ship)
|
|
local shipHeight = spriteGetHeight(ship)
|
|
----
|
|
|
|
Ora puoi tenere l'astronave sullo schermo, perché la posizione più a destra in
|
|
cui può stare è `screenWidth - shipWidth`, e metterla al centro, perché il
|
|
centro è `(screenWidth - shipWidth) / 2`. Nessuna delle due righe contiene un
|
|
numero che dovrebbe cambiare se qualcuno ridisegnasse l'astronave un po' più
|
|
grande.
|
|
|
|
Entrambe riportano la dimensione *con cui lo sprite verrebbe disegnato in questo
|
|
momento*. Se mai scali o ruoti uno sprite, con `spriteScale` o `spriteRotate`,
|
|
la risposta cambia di conseguenza. Di solito è quello che vuoi, e ti sorprenderà
|
|
esattamente una volta.
|
|
|
|
[#_drawing_order]
|
|
=== L'ordine di disegno
|
|
|
|
Gli sprite non si fondono. Il successivo copre il precedente, pixel per pixel,
|
|
esattamente come attaccare ritagli di carta su un muro. Quindi l'ordine delle
|
|
tue chiamate `spriteDraw` è l'ordine dal fondo verso il davanti:
|
|
|
|
[source,lua]
|
|
----
|
|
drawStars()
|
|
drawRocks()
|
|
drawShots()
|
|
spriteDraw(shipSprite, shipX, shipY)
|
|
drawHud()
|
|
----
|
|
|
|
Le stelle sono le più lontane, quindi vanno giù per prime. L'astronave sta
|
|
davanti a tutto nell'area di gioco. Il punteggio sta sopra tutto quanto.
|
|
Inverti l'ordine e la tua astronave disegnata con tanta cura passa la partita a
|
|
nascondersi dietro il campo stellare, e non c'è nessun errore a dirtelo -- solo
|
|
un'astronave che non vedi.
|
|
|
|
`overlayClear` viene ancora per primo, prima di tutto questo. Disegnare non
|
|
sostituisce mai, copre soltanto, quindi il fotogramma comincia vuoto ogni volta.
|
|
|
|
[#_frames]
|
|
=== Fotogrammi
|
|
|
|
Apri `art/walk.png` con un qualsiasi visualizzatore di immagini. Non è
|
|
l'immagine di una persona. È l'immagine di quattro persone, una accanto
|
|
all'altra: la stessa figura in quattro momenti di una camminata, ognuna larga 24
|
|
pixel, in una striscia larga 96.
|
|
|
|
Un'immagine che contiene diversi fotogrammi di un'animazione è uno *sprite
|
|
sheet*, ed è così che viene conservata quasi tutta l'animazione 2D. Un file, un
|
|
caricamento, un handle, e il gioco sceglie quale fetta disegnare.
|
|
|
|
Crea un secondo file, `walk.singe`, accanto al primo:
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
|
|
local walk = spriteLoadFrames(4, DIR .. "art/walk.png")
|
|
local walkFrame = 1
|
|
local walkTick = 0
|
|
local walkX = 0
|
|
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
walkTick = walkTick + 1
|
|
if walkTick >= 8 then
|
|
walkTick = 0
|
|
walkFrame = walkFrame + 1
|
|
if walkFrame > 4 then
|
|
walkFrame = 1
|
|
end
|
|
end
|
|
walkX = walkX + 1
|
|
if walkX > overlayGetWidth() then
|
|
walkX = -spriteFrameWidth(walk)
|
|
end
|
|
spriteDrawFrame(walk, walkX, 100, walkFrame)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
function onShutdown()
|
|
spriteUnload(walk)
|
|
end
|
|
----
|
|
|
|
Avvialo con `Singe rocks/walk.singe`. Una piccola figura attraversa lo schermo camminando
|
|
con passo regolare, sparisce oltre il bordo destro e rientra da sinistra.
|
|
|
|
`spriteLoadFrames` è `spriteLoad` con una cosa in più da dire: quanti fotogrammi
|
|
ci sono nella striscia. *Il conteggio viene per primo*, prima del nome del file,
|
|
che è il contrario di quello che pensano quasi tutti. Divide la larghezza
|
|
dell'immagine per il conteggio e si ricorda le fette. Quattro fotogrammi in una
|
|
striscia di 96 pixel vuol dire quattro fotogrammi larghi 24 pixel. Sbaglia il
|
|
conteggio -- diciamo 3 -- e ottieni tre fette da 32 pixel, ognuna con dei pezzi
|
|
di due figure, che è un modo memorabilmente buffo di scoprire che hai scritto il
|
|
numero sbagliato.
|
|
|
|
`spriteDrawFrame` disegna una fetta. Prende l'handle, dove metterla e quale
|
|
fotogramma, *in quest'ordine*, e i fotogrammi sono numerati *a partire da 1*: il
|
|
fotogramma 1 è la figura più a sinistra e il fotogramma 4 quella più a destra.
|
|
|
|
Attento a quell'1. Altrove nella famiglia degli sprite -- `spriteSetFrame` e
|
|
`spriteGetFrame` -- i fotogrammi sono numerati da 0. È un incidente della
|
|
storia, sono tutte e due nel Riferimento, e questo libro usa `spriteDrawFrame` e
|
|
conta da 1.
|
|
|
|
`spriteFrameWidth` riporta la larghezza di un fotogramma, qui 24, invece dei 96
|
|
dell'intera striscia. `spriteFrameHeight` è il suo compagno. Quando vuoi sapere
|
|
quanto è larga una fetta di uno sprite sheet, sono queste le due a cui chiedere,
|
|
e non `spriteGetWidth`.
|
|
|
|
Il numero del fotogramma è un numero in una variabile come qualsiasi altro,
|
|
quindi la camminata è aritmetica:
|
|
|
|
[source,lua]
|
|
----
|
|
walkTick = walkTick + 1
|
|
if walkTick >= 8 then
|
|
walkTick = 0
|
|
walkFrame = walkFrame + 1
|
|
if walkFrame > 4 then
|
|
walkFrame = 1
|
|
end
|
|
end
|
|
----
|
|
|
|
Conta i fotogrammi che passano. Ogni otto, passa all'immagine successiva, e dopo
|
|
la quarta immagine torna alla prima. Sessanta fotogrammi al secondo diviso otto
|
|
fa tra i sette e gli otto passi al secondo, che per un ciclo di quattro
|
|
fotogrammi è una camminata svelta ma credibile. Cambia l'8 e cambi la velocità:
|
|
più grande è, più è lenta.
|
|
|
|
Ti chiederai perché stai contando, quando il motore ha `spritePlay` per far
|
|
girare un'animazione al posto tuo. Ce l'ha, e per una GIF animata è la risposta
|
|
giusta, perché una GIF porta dentro il file la tempistica di ciascuno dei suoi
|
|
fotogrammi. Una semplice striscia come `walk.png` non porta nessuna tempistica,
|
|
quindi il motore la fa girare alla massima velocità consentita -- cento
|
|
fotogrammi al secondo, venticinque cicli di camminata completi ogni secondo, una
|
|
macchia indistinta. Quando i fotogrammi vengono da una striscia, conta tu.
|
|
|
|
[#_the_game_gets_its_artwork]
|
|
=== Il gioco riceve la sua grafica
|
|
|
|
Ora metti tutto insieme. Il gioco della settima lezione guidava un riquadro
|
|
lungo il fondo dello schermo e gli faceva cadere addosso altri riquadri. Ogni
|
|
riquadro diventa un'immagine, e già che ci sei l'astronave riceve qualcosa con
|
|
cui sparare.
|
|
|
|
Le modifiche sono tutte collegate fra loro, quindi ecco lo script intero. È più
|
|
lungo di qualsiasi cosa tu abbia scritto, e non c'è niente dentro che tu non
|
|
abbia già incontrato.
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
|
|
local SHIP_SPEED = 3
|
|
local SHOT_SPEED = 6
|
|
local ROCK_COUNT = 6
|
|
local STAR_COUNT = 40
|
|
local START_LIVES = 3
|
|
local HIT_SCORE = 10
|
|
|
|
local shipSprite = spriteLoad(DIR .. "art/spaceship.png")
|
|
local rockSprite = spriteLoad(DIR .. "art/asteroid.png")
|
|
local shotSprite = spriteLoad(DIR .. "art/bullet.png")
|
|
local starSprite = spriteLoad(DIR .. "art/star.png")
|
|
|
|
local screenWidth = overlayGetWidth()
|
|
local screenHeight = overlayGetHeight()
|
|
local shipWidth = spriteGetWidth(shipSprite)
|
|
local shipHeight = spriteGetHeight(shipSprite)
|
|
local rockWidth = spriteGetWidth(rockSprite)
|
|
local rockHeight = spriteGetHeight(rockSprite)
|
|
local shotWidth = spriteGetWidth(shotSprite)
|
|
local shotHeight = spriteGetHeight(shotSprite)
|
|
|
|
local shipX = (screenWidth - shipWidth) / 2
|
|
local shipY = screenHeight - shipHeight - 4
|
|
local goLeft = false
|
|
local goRight = false
|
|
local rocks = {}
|
|
local shots = {}
|
|
local stars = {}
|
|
local score = 0
|
|
local lives = START_LIVES
|
|
local over = false
|
|
|
|
|
|
function drawHud()
|
|
overlayPrint(1, 1, "SCORE " .. score)
|
|
overlayPrint(50, 1, "LIVES " .. lives)
|
|
if over then
|
|
overlayPrint(25, 8, "GAME OVER")
|
|
overlayPrint(17, 10, "PRESS 1 TO PLAY AGAIN")
|
|
end
|
|
end
|
|
|
|
|
|
function drawRocks()
|
|
for _, rock in ipairs(rocks) do
|
|
spriteDraw(rockSprite, rock.x, rock.y)
|
|
end
|
|
end
|
|
|
|
|
|
function drawShots()
|
|
for _, shot in ipairs(shots) do
|
|
spriteDraw(shotSprite, shot.x, shot.y)
|
|
end
|
|
end
|
|
|
|
|
|
function drawStars()
|
|
for _, star in ipairs(stars) do
|
|
spriteDraw(starSprite, star.x, star.y)
|
|
end
|
|
end
|
|
|
|
|
|
function newRock(rock)
|
|
rock.x = math.random(0, screenWidth - rockWidth)
|
|
rock.y = -rockHeight - math.random(0, 160)
|
|
rock.speed = math.random(8, 20) / 10
|
|
end
|
|
|
|
|
|
function onInputPressed(what)
|
|
if over then
|
|
if what == SWITCH_START1 then
|
|
startGame()
|
|
end
|
|
return
|
|
end
|
|
if what == SWITCH_LEFT then
|
|
goLeft = true
|
|
elseif what == SWITCH_RIGHT then
|
|
goRight = true
|
|
elseif what == SWITCH_BUTTON1 then
|
|
shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY }
|
|
end
|
|
end
|
|
|
|
|
|
function onInputReleased(what)
|
|
if what == SWITCH_LEFT then
|
|
goLeft = false
|
|
elseif what == SWITCH_RIGHT then
|
|
goRight = false
|
|
end
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
updateStars()
|
|
if not over then
|
|
updateShip()
|
|
updateShots()
|
|
updateRocks()
|
|
end
|
|
drawStars()
|
|
drawRocks()
|
|
drawShots()
|
|
spriteDraw(shipSprite, shipX, shipY)
|
|
drawHud()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
function onShutdown()
|
|
spriteUnload(shipSprite)
|
|
spriteUnload(rockSprite)
|
|
spriteUnload(shotSprite)
|
|
spriteUnload(starSprite)
|
|
end
|
|
|
|
|
|
function overlapping(ax, ay, aw, ah, bx, by, bw, bh)
|
|
if ax + aw <= bx then
|
|
return false
|
|
end
|
|
if bx + bw <= ax then
|
|
return false
|
|
end
|
|
if ay + ah <= by then
|
|
return false
|
|
end
|
|
if by + bh <= ay then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
|
|
function startGame()
|
|
shipX = (screenWidth - shipWidth) / 2
|
|
score = 0
|
|
lives = START_LIVES
|
|
over = false
|
|
shots = {}
|
|
for i = 1, ROCK_COUNT do
|
|
rocks[i] = {}
|
|
newRock(rocks[i])
|
|
end
|
|
end
|
|
|
|
|
|
function updateRocks()
|
|
for _, rock in ipairs(rocks) do
|
|
rock.y = rock.y + rock.speed
|
|
if rock.y > screenHeight then
|
|
newRock(rock)
|
|
end
|
|
if overlapping(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then
|
|
newRock(rock)
|
|
lives = lives - 1
|
|
if lives <= 0 then
|
|
over = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function updateShip()
|
|
if goLeft then
|
|
shipX = shipX - SHIP_SPEED
|
|
end
|
|
if goRight then
|
|
shipX = shipX + SHIP_SPEED
|
|
end
|
|
if shipX < 0 then
|
|
shipX = 0
|
|
end
|
|
if shipX > screenWidth - shipWidth then
|
|
shipX = screenWidth - shipWidth
|
|
end
|
|
end
|
|
|
|
|
|
function updateShots()
|
|
for i = #shots, 1, -1 do
|
|
local shot = shots[i]
|
|
local gone = false
|
|
|
|
shot.y = shot.y - SHOT_SPEED
|
|
if shot.y + shotHeight < 0 then
|
|
gone = true
|
|
end
|
|
for _, rock in ipairs(rocks) do
|
|
if not gone and overlapping(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then
|
|
newRock(rock)
|
|
score = score + HIT_SCORE
|
|
gone = true
|
|
end
|
|
end
|
|
if gone then
|
|
table.remove(shots, i)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function updateStars()
|
|
for _, star in ipairs(stars) do
|
|
star.y = star.y + star.speed
|
|
if star.y > screenHeight then
|
|
star.y = 0
|
|
star.x = math.random(0, screenWidth - 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
for i = 1, STAR_COUNT do
|
|
stars[i] = { x = math.random(0, screenWidth - 1), y = math.random(0, screenHeight - 1), speed = math.random(1, 3) / 4 }
|
|
end
|
|
|
|
startGame()
|
|
----
|
|
|
|
Le frecce sinistra e destra guidano. La barra spaziatrice spara. Quando le rocce
|
|
ti hanno tolto l'ultima vita, `1` comincia una nuova partita.
|
|
|
|
Alcune cose lì dentro meritano di essere indicate.
|
|
|
|
Le quattro chiamate `spriteLoad` e le otto misure stanno in cima, fuori da ogni
|
|
funzione, quindi avvengono una volta sola. Tutto ciò che viene dopo è scritto in
|
|
termini di `screenWidth`, `shipWidth` e compagnia, e in tutto il codice di gioco
|
|
non c'è un solo numero di pixel scritto a mano.
|
|
|
|
Le funzioni sono in ordine alfabetico, un'abitudine che vale la pena prendere
|
|
adesso. A Lua non importa minimamente -- è per te, fra sei mesi, quando
|
|
cercherai `updateShots` in un file con dentro quaranta funzioni.
|
|
|
|
Le ultime due cose nel file non sono affatto funzioni. Sono semplici istruzioni
|
|
in fondo allo script: costruisci il campo stellare, poi chiama `startGame` per
|
|
preparare le rocce e azzerare il punteggio. Vengono eseguite una volta, in
|
|
ordine, mentre lo script si carica, e devono venire dopo le funzioni che usano.
|
|
|
|
`overlapping` sono i quattro confronti che, come diceva la settima lezione,
|
|
potevi scrivere da te invece di chiamare `collideRects`, messi per esteso in una
|
|
funzione con gli stessi otto argomenti. Due rettangoli non si toccano se uno dei due sta completamente a sinistra, a destra, sopra o sotto
|
|
l'altro; se nessuna di queste quattro cose è vera, si sovrappongono.
|
|
L'undicesima lezione la sostituisce con qualcosa che fornisce il motore, e
|
|
spiega perché il rettangolo è la forma sbagliata per un'astronave.
|
|
|
|
Un'ultima nota per quando leggerai il codice Singe di altre persone. Ogni
|
|
chiamata agli sprite in questo libro prende l'handle *per primo*. Singe 2.10 lo
|
|
metteva per ultimo, e un gioco può chiedere al motore di continuare a fare così,
|
|
quindi il codice che trovi online potrebbe essere `spriteDraw(x, y, ship)`. Non
|
|
copiarlo. L'handle per primo è l'ordine attuale, e quello documentato dal
|
|
Riferimento.
|
|
|
|
[#_try_it_9]
|
|
=== Prova tu
|
|
|
|
. *Centra l'astronave sulla sua posizione.* Aggiungi `true` come quarto
|
|
argomento allo `spriteDraw` dell'astronave. Guarda dove salta l'astronave, e
|
|
dalla distanza di cui si è spostata deduci che cosa ha fatto davvero il quarto
|
|
argomento.
|
|
. *Più rocce.* Cambia `ROCK_COUNT` in 20. Poi in 200. Da qualche parte tra quei
|
|
due valori il gioco smette di essere giocabile, e da qualche parte molto più in
|
|
là il gioco comincia a rallentare. Trovali tutti e due.
|
|
. *Fai crescere l'astronave.* Aggiungi `spriteScale(shipSprite, 2)` proprio in
|
|
fondo al file, sulla riga sopra `startGame()`. Avvialo e guida contro il bordo
|
|
destro: l'astronave è grande il doppio ma si ferma ancora dove si fermava
|
|
quella piccola, e sporge oltre il bordo destro e oltre il fondo. Scopri
|
|
perché, poi sposta quell'unica riga sopra le chiamate `spriteGetWidth` e
|
|
guarda le cose sistemarsi. La voce di `spriteScale` nel Riferimento spiega che cosa
|
|
fa la scalatura alle misure.
|
|
. *Metti dentro chi cammina.* Carica `walk.png` nel gioco, e disegna la figura
|
|
che cammina lungo la parte alta dello schermo come scenografia. Ti servirà il
|
|
conteggio dei fotogrammi dello script della camminata, e dovrai decidere in
|
|
che punto di `onOverlayUpdate` va il disegno -- davanti alle stelle, dietro le
|
|
rocce.
|
|
. *Scarica qualcosa che stai ancora usando.* Aggiungi `spriteUnload(starSprite)`
|
|
in fondo al file, dopo `startGame()`, ed eseguilo. Leggi che cosa ottieni.
|
|
|
|
[#_break_it_on_purpose_8]
|
|
=== Rompilo apposta
|
|
|
|
Le maiuscole contano, ed è con la grafica che mordono più forte. Cambia il
|
|
caricamento dell'astronave perché usi una S maiuscola:
|
|
|
|
[source,lua]
|
|
----
|
|
local shipSprite = spriteLoad(DIR .. "art/Spaceship.png")
|
|
----
|
|
|
|
Il gioco si rifiuta di partire:
|
|
|
|
----
|
|
10:spriteLoad: Couldn't open rocks/art/Spaceship.png: No such file or directory
|
|
----
|
|
|
|
Leggilo come l'errore della prima lezione, ma nota che la forma è un po'
|
|
diversa. `10` è la riga. `spriteLoad` è la funzione del motore che si è arresa,
|
|
invece del nome di un file -- quando la lamentela viene da Singe invece che da
|
|
Lua, ottieni questo. Poi il motivo, che qui è formulato dal tuo sistema
|
|
operativo e cambierà un po' da macchina a macchina.
|
|
|
|
`No such file or directory` è il computer che è preciso, non scortese. Non
|
|
esiste davvero nessun file chiamato `Spaceship.png`; ce n'è uno chiamato
|
|
`spaceship.png`, e per una macchina Linux sono due nomi diversi. Su Windows, e
|
|
sulla maggior parte dei Mac, avrebbe funzionato, il che è peggio, perché vuol dire che il bug viaggia
|
|
fino alla macchina di qualcun altro e compare lì per la prima volta.
|
|
|
|
L'altra metà di questo errore che incontrerai è la cartella. Se hai messo il kit
|
|
di grafica nel posto sbagliato, o hai avviato Singe dalla cartella sbagliata, il
|
|
messaggio è esattamente lo stesso e la correzione no. Il percorso nel messaggio
|
|
aiuta: è il nome completo che Singe ha provato, contato dalla cartella da cui
|
|
l'hai avviato, e il `rocks/` davanti è la parte che ha aggiunto `DIR`. Controlla
|
|
tre cose, in quest'ordine: se il file si chiama esattamente come l'hai scritto, se sta nella
|
|
cartella `art` e se quella cartella `art` sta accanto al tuo script.
|
|
|
|
[#_what_you_learned_9]
|
|
=== Cosa hai imparato
|
|
|
|
* Un'immagine disegnata sull'overlay è uno sprite, e si carica con
|
|
`spriteLoad`.
|
|
* `spriteLoad` restituisce un handle: un numero che per il motore vuol dire
|
|
«quell'immagine», e che tieni in una variabile.
|
|
* `DIR` è la cartella in cui si trova il tuo script, e ogni file che il tuo
|
|
gioco carica andrebbe indicato a partire da lì.
|
|
* `spriteDraw(handle, x, y)` lo disegna, dal suo angolo in alto a sinistra, e
|
|
solo dentro `onOverlayUpdate`. Un quarto argomento `true` indica invece il
|
|
centro.
|
|
* Carica una volta, in cima al file. Caricare dentro `onOverlayUpdate` si mangia
|
|
la memoria finché il gioco muore.
|
|
* `spriteUnload` restituisce un'immagine, di solito da `onShutdown`, e dopo
|
|
l'handle è morto.
|
|
* Il PNG porta con sé la trasparenza, ed è per questo che gli angoli
|
|
dell'astronave non sono riquadri neri.
|
|
* `spriteGetWidth` e `spriteGetHeight` misurano uno sprite come verrebbe
|
|
disegnato ora, così non devi mai scrivere la sua dimensione nel programma.
|
|
* Gli sprite si coprono a vicenda. L'ordine in cui disegni va dal fondo verso il
|
|
davanti.
|
|
* `spriteLoadFrames(count, file)` taglia una striscia in fotogrammi, e
|
|
`spriteDrawFrame(handle, x, y, frame)` ne disegna uno, contando da 1.
|
|
|
|
[#_next_time_9]
|
|
=== La prossima volta
|
|
|
|
Il gioco ha l'aspetto giusto e non ha nessun suono. Nella decima lezione i colpi
|
|
ricevono un rumore, le rocce un'esplosione, e sotto a tutto suona qualcosa -- e
|
|
incontri la tua quinta callback, quella che il motore usa per dirti che un suono
|
|
è finito.
|