singe/docs/it/lessons/25-particles.adoc

551 lines
21 KiB
Text

[#_lesson_25_particles]
== Lezione 25: Particelle
image::learn/25-particles.png[La lezione finita, 480]
Alla fine di questa lezione una nave attraverserà la parte bassa dello schermo
con una scia di fuoco dal motore, e un pulsante la farà saltare in aria in
centoquaranta scintille rotanti che rimbalzano sul pavimento. Non scriverai una
sola riga che disegni una scintilla.
L'idea di questa lezione è tutta qui. Tu descrivi il fuoco; il motore lo fa.
[#_why_you_do_not_draw_the_sparks]
=== Perché le scintille non le disegni tu
Potresti farlo nell'altro modo. Ne sai abbastanza: una tabella di scintille
come nella sesta lezione, ognuna con una x, una y e una velocità; un ciclo in
`onOverlayUpdate` che le muove tutte, le fa sbiadire, le toglie quando muoiono
e le disegna. Funziona. Sono anche duecento righe di contabilità che ogni gioco
riscrive, e girano in Lua, una scintilla alla volta, sessanta volte al secondo.
Invece lo fa Singe per te. Gli consegni una *ricetta* -- quante scintille al
secondo, quanto vive ciascuna, quanto veloci e in che direzione partono, che
cosa le tira, che aspetto hanno e verso quale colore sbiadiscono -- e le
scintille le gestisce lui. La cosa che contiene la ricetta e la esegue si
chiama *emettitore*.
Un emettitore non è un'immagine e non è un oggetto del tuo gioco. È la
descrizione di un tipo di fuoco, tenuta in un posto che puoi indicare, e può
fare quel fuoco ovunque tu glielo chieda.
[#_start_with_something_that_moves]
=== Comincia da qualcosa che si muove
Le particelle sono più convincenti quando escono da qualcosa, quindi comincia
con una nave. Crea una cartella, copiaci dentro la cartella `art` del libro e
metti questo in `particles.singe`:
[source,lua]
----
dofile("Singe/Framework.singe")
local SHIP_WIDTH = 32
local SHIP_HEIGHT = 24
local SHIP_SPEED = 120
local shipSprite = spriteLoad(DIR .. "art/spaceship.png")
local shipX = (overlayGetWidth() - SHIP_WIDTH) / 2
local shipY = overlayGetHeight() - 56
local movingLeft = false
local movingRight = false
local lastTicks = singeGetTicks()
function onInputPressed(what)
if what == SWITCH_LEFT then
movingLeft = true
elseif what == SWITCH_RIGHT then
movingRight = true
end
end
function onInputReleased(what)
if what == SWITCH_LEFT then
movingLeft = false
elseif what == SWITCH_RIGHT then
movingRight = false
end
end
function onOverlayUpdate()
local now = singeGetTicks()
local seconds = (now - lastTicks) / 1000
lastTicks = now
if movingLeft then
shipX = shipX - SHIP_SPEED * seconds
end
if movingRight then
shipX = shipX + SHIP_SPEED * seconds
end
overlayClear()
spriteDraw(shipSprite, shipX, shipY)
return OVERLAY_UPDATED
end
----
Eseguilo con `Singe -R particles` e guida con i tasti freccia sinistra e
destra. Qui non c'è niente di nuovo: uno sprite dalla nona lezione, gli
interruttori dalla terza lezione e `singeGetTicks`, così la nave si muove alla
stessa velocità qualunque sia la frequenza dei fotogrammi.
L'unica riga che vale la pena indicare è `dofile("Singe/Framework.singe")`. È
quella che definisce `DIR`, la cartella in cui si trova il tuo script, così
`DIR .. "art/spaceship.png"` trova l'immagine da qualunque cartella tu abbia
avviato Singe.
[#_give_it_an_exhaust]
=== Dagli uno scarico
Ora il fuoco. Aggiungi questo sopra le callback, dopo le variabili della nave:
[source,lua]
----
local exhaust = emitterNew()
emitterSetBlend(exhaust, PARTICLE_ADD)
emitterSetRate(exhaust, 90)
emitterSetLife(exhaust, 0.15, 0.40)
emitterSetDirection(exhaust, 0, 1)
emitterSetSpread(exhaust, 12)
emitterSetSpeed(exhaust, 40, 90)
emitterSetSize(exhaust, 7, 1)
emitterSetColor(exhaust, 255, 240, 180, 255, 255, 60, 0, 0)
emitterSetMax(exhaust, 120)
emitterStart(exhaust)
----
E due righe dentro `onOverlayUpdate`. La prima va subito prima di
`overlayClear`, la seconda subito dopo `spriteDraw`:
[source,lua]
----
emitterSetPosition(exhaust, shipX + SHIP_WIDTH / 2, shipY + SHIP_HEIGHT - 2)
----
[source,lua]
----
emitterDraw(exhaust)
----
Salva. Adesso la nave sta sopra una corta lingua di fiamma gialla, e la fiamma
la segue quando sterzi.
[#_blow_it_up]
=== Falla esplodere
Uno scarico va avanti per sempre. Un'esplosione succede una volta sola, e il
motore la tratta come un tipo diverso di richiesta allo stesso emettitore.
Prima, altri due file e un altro numero. Questi vanno in cima, accanto allo
sprite della nave:
[source,lua]
----
local sparkSprite = spriteLoad(DIR .. "art/star.png")
local boomSound = soundLoad(DIR .. "art/boom.wav")
----
e questo va accanto a `shipX` e `shipY`, venti unità sopra il fondo dello
schermo:
[source,lua]
----
local ground = overlayGetHeight() - 20
----
Ora la seconda ricetta, sotto la prima:
[source,lua]
----
local boom = emitterNew()
emitterSetTexture(boom, sparkSprite)
emitterSetBlend(boom, PARTICLE_ADD)
emitterSetLife(boom, 0.4, 1.1)
emitterSetSpeed(boom, 40, 220)
emitterSetSpread(boom, 180)
emitterSetGravity(boom, 0, 160)
emitterSetDrag(boom, 0.9)
emitterSetSize(boom, 10, 2, 0.4)
emitterSetSpin(boom, -240, 240)
emitterSetColor(boom, 255, 255, 220, 255, 255, 70, 20, 0)
emitterSetCollide(boom, COLLIDE_FLOOR, 0.35, 0.3, ground)
emitterSetTrail(boom, 4, 2)
emitterSetMax(boom, 400)
----
Nota che manca `emitterStart`. Questo non emetterà mai un flusso.
Aggiungi una funzione per farlo scattare, sopra `onInputPressed`:
[source,lua]
----
function explode()
if not flying then
return
end
flying = false
emitterStop(exhaust)
emitterSetPosition(boom, shipX + SHIP_WIDTH / 2, shipY + SHIP_HEIGHT / 2)
emitterBurst(boom, 140)
soundPlay(boomSound)
timerAfter(RESPAWN_MS, function()
flying = true
emitterStart(exhaust)
end)
end
----
Servono altre tre cose: `local RESPAWN_MS = 1400` con le altre costanti,
`local flying = true` con le altre variabili e un ramo in `onInputPressed`:
[source,lua]
----
elseif what == SWITCH_BUTTON1 then
explode()
----
Infine, la nave non dovrebbe esserci mentre è a pezzi. Racchiudi la guida e il
disegno di `onOverlayUpdate` dentro `if flying then ... end`, e aggiungi
`emitterDraw(boom)` accanto all'altra chiamata di disegno. Lo script finito è
`learn/25-particles.singe`.
Premi la *barra spaziatrice* -- il pulsante uno, se non hai cambiato niente --
e la nave va in pezzi in una nube di stelle che girano su se stesse, scendono
ad arco, colpiscono il pavimento e rimbalzano.
[#_what_just_happened_21]
=== Cos'è appena successo
In questa lezione ci sono moltissime chiamate nuove, e hanno tutte la stessa
forma: prima l'emettitore, poi la cosa che stai impostando. Prendile a gruppi.
[source,lua]
----
local exhaust = emitterNew()
----
Crea un emettitore e ti restituisce un *handle*: un numero che il motore usa
per ritrovarlo, esattamente come il numero che ti dà `spriteLoad`. Senza
argomenti ottieni un emettitore bidimensionale, che vive nelle coordinate
dell'overlay e si disegna da `onOverlayUpdate`. Se invece dai a `emitterNew` un
nodo, ne ottieni uno tridimensionale, che vive nella scena e si disegna da
solo; tutto il resto di questa lezione vale per entrambi.
Un emettitore nuovo ha già una ricetta completa -- cinquanta particelle al
secondo, bianche, un secondo o due di vita, che salgono piano lungo lo schermo
-- quindi ogni chiamata dopo questa sei tu che non sei d'accordo con un valore
predefinito. La voce del Riferimento per `emitterNew` li elenca tutti.
[source,lua]
----
emitterSetRate(exhaust, 90)
emitterSetLife(exhaust, 0.15, 0.40)
----
Novanta particelle al secondo, ognuna che vive tra quindici centesimi e
quattro decimi di secondo. Questi due numeri insieme decidono quanto fuoco c'è:
novanta al secondo che vivono un terzo di secondo vuol dire circa trenta sullo
schermo in ogni momento.
Ovunque una chiamata di un emettitore prenda un minimo e un massimo, ogni
particella riceve un proprio valore scelto a caso tra i due. È da lì che viene
l'irregolarità. Un fuoco in cui ogni fiamma vivesse esattamente quanto tutte le
altre sembrerebbe una macchina.
[source,lua]
----
emitterSetDirection(exhaust, 0, 1)
emitterSetSpread(exhaust, 12)
emitterSetSpeed(exhaust, 40, 90)
----
In che direzione partono, quanto è largo il ventaglio e quanto vanno veloci. La
Y dell'overlay cresce *verso il basso*, come dalla prima lezione, quindi
`(0, 1)` è verso il basso dello schermo -- fuori dal retro di una nave che
punta in alto. `emitterSetSpread` è metà dell'angolo del ventaglio: dodici
gradi sono un getto stretto, novanta lo aprono a mezzo cerchio e centottanta
sono tutte le direzioni insieme.
[source,lua]
----
emitterSetSize(exhaust, 7, 1)
emitterSetColor(exhaust, 255, 240, 180, 255, 255, 60, 0, 0)
----
Sono queste due a far sembrare la fiamma una fiamma. Una particella non ha una
dimensione e un colore; ha una dimensione e un colore *alla nascita* e altri
*alla morte*, e scivola in modo uniforme dagli uni agli altri lungo la vita che
le è toccata. Così ogni scintilla comincia larga sette unità dell'overlay e
giallo pallido, e finisce larga un'unità e rosso arancio con un alfa di zero,
cioè invisibile. Si rimpicciolisce e svanisce perché hai detto tu dove finisce,
non perché qualcosa l'abbia fatta svanire.
`emitterSetColor` prende otto numeri, che sono tanti da leggere: rosso, verde,
blu e alfa all'inizio, poi rosso, verde, blu e alfa alla fine.
[source,lua]
----
emitterSetBlend(exhaust, PARTICLE_ADD)
----
Come una particella si combina con ciò che ha dietro. `PARTICLE_ALPHA` è la
pittura normale ed è il predefinito: usalo per fumo, polvere, pioggia e
macerie. `PARTICLE_ADD` invece aggiunge la sua luce all'immagine, quindi due
scintille una sopra l'altra sono più luminose di una e niente diventa mai più
scuro. Fuoco, scintille, laser e magia sono additivi. Il fumo no, e il fumo
disegnato in modo additivo sembra vapore illuminato da dentro.
[source,lua]
----
emitterSetMax(exhaust, 120)
emitterStart(exhaust)
----
`emitterSetMax` è la dimensione della riserva: quante particelle di questo
emettitore possono esistere insieme. Quando la riserva è piena non nasce
niente di nuovo finché qualcosa non muore. Novanta al secondo che vivono fino a
quattro decimi di secondo ne richiedono circa trentasei, quindi centoventi
bastano e avanzano. Impostalo prima di avviare, perché cambiarlo dopo butta via
ogni particella viva.
`emitterStart` apre il rubinetto. Da lì in poi l'emettitore crea particelle a
ogni fotogramma, che tu lo disegni o no, finché `emitterStop` non lo richiude.
Nota che cosa `emitterStop` *non* fa: ferma le particelle nuove, e quelle già
vive finiscono la loro vita e svaniscono normalmente. Ecco perché lo scarico si
dissolve a poco a poco invece di sparire quando la nave esplode. Se le vuoi via
all'istante, `emitterClear` le uccide.
[source,lua]
----
emitterSetPosition(exhaust, shipX + SHIP_WIDTH / 2, shipY + SHIP_HEIGHT - 2)
----
È così che un emettitore segue qualcosa. Un emettitore bidimensionale fa
nascere le particelle in un solo punto, e quel punto è dove l'hai messo
l'ultima volta, quindi lo imposti a ogni fotogramma da qualunque cosa si stia
muovendo: la posizione di uno sprite, il nodo di un corpo fisico, il mouse. Le
particelle nuove compaiono nel nuovo posto; quelle già in aria restano dove
sono nate, ed è proprio quello che serve per uno scarico, perché lo scarico
resta indietro.
Un emettitore tridimensionale viene creato su un nodo e segue quel nodo da
solo, senza nessuna chiamata nel tuo aggiornamento. E quando *vuoi* che
l'intera nube viaggi con la cosa -- uno scudo, un'aura, il pennacchio di un
propulsore visto da fuori -- `emitterSetLocal` lo attiva.
[source,lua]
----
emitterDraw(exhaust)
----
Gli emettitori bidimensionali si disegnano quando lo chiedi tu, una volta per
fotogramma, da `onOverlayUpdate`. L'ordine delle tue chiamate a `emitterDraw` è
l'ordine in cui si sovrappongono. Dimenticare questa chiamata è l'errore che
tutti fanno una volta: l'emettitore continua a funzionare, continua a consumare
la sua riserva e non mostra niente.
Ora le chiamate tutte dell'esplosione.
[source,lua]
----
emitterSetTexture(boom, sparkSprite)
----
Senza una texture ogni particella è un morbido disco rotondo che il motore
disegna da sé, ed è quello che usa lo scarico, giusto per una fiamma. Con una
texture, ogni particella indossa quell'immagine -- qui `star.png`, otto pixel
per otto. L'emettitore si tiene una sua copia dei pixel, quindi dopo puoi
scaricare lo sprite e le particelle continuano a funzionare.
[source,lua]
----
emitterSetGravity(boom, 0, 160)
emitterSetDrag(boom, 0.9)
----
La gravità è un'attrazione costante, in unità dell'overlay al secondo al
secondo, ed è positiva verso il basso per lo stesso motivo per cui lo scarico
spara con una Y positiva. La resistenza è il contrario: toglie velocità, un po'
a ogni fotogramma, così le scintille partono forte e poi restano sospese.
Scintille con la gravità e senza resistenza piovono dritte giù come un fuoco
d'artificio; è la resistenza a farle sembrare in movimento nell'aria.
[source,lua]
----
emitterSetSpin(boom, -240, 240)
emitterSetSize(boom, 10, 2, 0.4)
----
La rotazione è in gradi al secondo, e un intervallo a cavallo dello zero ti dà
particelle che girano in tutti e due i versi. Si vede su una texture e non fa
proprio niente al disco incorporato, che è rotondo.
Il terzo numero di `emitterSetSize` è la variazione. Scala entrambe le
dimensioni di ogni particella per uno stesso fattore casuale -- qui da sei
decimi a uno e quattro decimi -- così le scintille non sono tutte la stessa
stella della stessa grandezza.
[source,lua]
----
emitterSetCollide(boom, COLLIDE_FLOOR, 0.35, 0.3, ground)
emitterSetTrail(boom, 4, 2)
----
`COLLIDE_FLOOR` stende attraverso il mondo un pavimento piatto e invisibile
all'altezza che indichi, e le particelle ci rimbalzano sopra. I due numeri che
la precedono sono quanta velocità sopravvive a un rimbalzo e quanta velocità
laterale viene strofinata via. Non costa niente, perché è un solo confronto per
particella, e fa sembrare che i detriti siano atterrati da qualche parte invece
di cadere attraverso il mondo. In un gioco tridimensionale `COLLIDE_SCENE` fa
invece rimbalzare le particelle sulla geometria vera, e costa molto di più.
Una scia ricorda gli ultimi posti in cui è passata una particella e ci disegna
attraverso un nastro che sbiadisce. Quattro posizioni e due unità di larghezza
trasformano ogni stella in una breve striscia.
[source,lua]
----
emitterBurst(boom, 140)
----
E questo è l'altro modo di creare particelle. `emitterStart` è un rubinetto;
`emitterBurst` è una manciata lanciata tutta in una volta. L'emettitore non deve
per forza emettere un flusso, e questo non lo fa mai. Tutto il resto -- la
ricetta, la riserva, la posizione -- funziona allo stesso modo.
Vale la pena tenere a mente questa distinzione. Tutto ciò che è continuo è un
flusso che avvii e fermi: uno scarico, il fumo, la pioggia, una torcia, una
cascata. Tutto ciò che succede in un istante è una raffica: un'esplosione, uno
spruzzo, un passo nella polvere, un proiettile che colpisce, una vampata alla
bocca di un'arma. Molti effetti sono tutte e due le cose, con uno o due
emettitori: una granata scoppia e poi fuma.
[#_how_many_is_too_many]
=== Quante sono troppe
La risposta onesta è che le particelle costano poco da muovere e tanto da
guardare.
Muovere diecimila particelle è aritmetica, e un computer che riesce a far
girare un gioco lo fa senza accorgersene. Quello che costa è il disegno, e il
disegno costa più o meno quanti pixel dello schermo coprono le particelle,
sovrapposizioni comprese. Duecento scintille larghe otto pixel coprono meno
schermo di uno sbuffo di fumo che lo riempie. La regola è questa: piccole e
tante è quasi gratis, grandi e sovrapposte no.
La fusione additiva è il tipo costoso, perché niente di ciò che disegna si può
saltare -- ogni strato si aggiunge a ciò che c'è sotto, quindi la macchina li
dipinge tutti. Una dozzina di lampi additivi a tutto schermo uno sopra l'altro
riducono un Raspberry Pi 4 a passo di lumaca, mentre diecimila minuscole
scintille che rimbalzano non gli danno nessun fastidio.
Quindi: qualche migliaio di particelle piccole non è niente di cui
preoccuparsi. Prima di andare oltre, metti il conteggio sullo schermo e tienilo
d'occhio.
[source,lua]
----
overlayPrint(2, 4, "Particles alive: " .. (emitterGetCount(exhaust) + emitterGetCount(boom)))
----
`emitterGetCount` è il modo onesto di dimensionare una riserva, invece di
tirare a indovinare. Se il numero sta fermo esattamente sul tuo
`emitterSetMax`, la riserva è piena e il motore si sta rifiutando in silenzio
di creare particelle che hai chiesto. Se non ci arriva mai vicino, stai
riservando memoria per niente.
[#_try_it_25]
=== Prova tu
. *Trasforma lo scarico in fumo.* Cambia `emitterSetBlend(exhaust,
PARTICLE_ADD)` in `PARTICLE_ALPHA`, imposta il colore su un grigio che sfuma
in un grigio trasparente, e fallo crescere invece di rimpicciolire con
`emitterSetSize(exhaust, 3, 14)`. Tre righe hanno fatto una macchina diversa.
. *Togli la resistenza all'esplosione.* Cancella la riga di `emitterSetDrag` e
confronta. Poi rimetti la resistenza e togli invece la gravità. Una delle due
sembra un fuoco d'artificio e l'altra sembra un incidente.
. *Allarga lo scarico.* Imposta `emitterSetSpread(exhaust, 180)`. La nave
smette di sembrare dotata di un motore. L'apertura lavora più di quanto
pensi.
. *Una raffica da mille.* Cambia `emitterBurst(boom, 140)` in
`emitterBurst(boom, 1000)` e guarda il conteggio sullo schermo. Non arriverà
a mille. Capisci da `emitterSetMax` perché no, poi alza la riserva e fai le
scintille quattro volte più grandi, e guarda che cosa succede alla frequenza
dei fotogrammi.
. *Fai esplodere dove la nave non c'è.* Metti `emitterSetPosition(boom,
40, 60)` in `explode` al posto della posizione della nave. Un emettitore è la
descrizione di un fuoco, non una cosa che vive da qualche parte, e puoi far
scoppiare lo stesso in dieci posti nello stesso fotogramma.
[#_break_it_on_purpose_24]
=== Rompilo apposta
Le costanti -- `PARTICLE_ADD`, `COLLIDE_FLOOR`, `OVERLAY_UPDATED` -- sono nomi
che Singe ha dato a dei numeri. Sbagliane una di poco e Lua non si lamenta,
perché in Lua un nome a cui non è mai stato dato un valore non è un errore: è
`nil`, il valore che vuol dire «qui non c'è niente».
Cambia la riga della fusione dello scarico con un nome che sembra altrettanto
plausibile:
[source,lua]
----
emitterSetBlend(exhaust, PARTICLE_ADDITIVE)
----
Singe si ferma prima che il gioco parta, indicando la riga del tuo file in
cui si trova quella chiamata:
----
22:emitterSetBlend: Argument 2 must be a number.
----
Questo errore ha una forma diversa dagli errori di Lua dell'ottava lezione,
perché viene dal motore e non da Lua. C'è il numero di riga, poi la chiamata
che ha protestato, poi la lamentela. L'argomento due di `emitterSetBlend` è il
modo di fusione, e quello che è arrivato è `nil`, perché `PARTICLE_ADDITIVE`
non è un nome che qualcosa abbia mai definito.
«Must be a number» vuol dire quasi sempre questo: una costante scritta male, o
una variabile che pensavi di aver impostato. La soluzione è controllare il nome
nel Riferimento, che lo scrive `PARTICLE_ADD`.
[#_what_you_learned_25]
=== Cosa hai imparato
* Un emettitore contiene la ricetta di un tipo di particella e gestisce da sé
le particelle, così il tuo script non ne disegna mai una.
* `emitterNew` senza argomenti crea un emettitore bidimensionale nelle
coordinate dell'overlay; con un nodo ne crea uno tridimensionale nella scena.
* Ogni emettitore parte con una ricetta completa, e ogni chiamata
`emitterSet...` ne cambia una parte.
* Ovunque una chiamata prenda un minimo e un massimo, ogni particella estrae a
caso un suo valore tra i due, ed è da lì che viene l'irregolarità.
* Dimensione e colore si impostano alla nascita e alla morte, e la particella
scivola dagli uni agli altri lungo la sua vita.
* `emitterStart` emette un flusso fino a `emitterStop`; `emitterBurst` lancia
una manciata tutta in una volta. Gli effetti continui sono flussi, quelli
momentanei sono raffiche.
* `emitterStop` lascia finire le particelle vive; `emitterClear` le uccide
subito.
* Un emettitore bidimensionale si sposta con `emitterSetPosition` a ogni
fotogramma e si disegna con `emitterDraw` a ogni fotogramma. Uno
tridimensionale segue il suo nodo e si disegna da solo.
* Le particelle costano quello che coprono sullo schermo, non quello che costa
muoverle, e la fusione additiva è il tipo costoso.
* `emitterSetMax` fissa il limite della riserva, e `emitterGetCount` ti dice se
ne hai indovinato la dimensione.
[#_next_time_25]
=== La prossima volta
L'esplosione ha mosso centoquaranta cose senza che a nessuna venisse detto dove
andare, ma erano scintille, e alle scintille non importa contro che cosa vanno
a sbattere. La ventiseiesima lezione parla di qualcosa a cui importa: un
personaggio che deve andare da qui a lì, con un muro in mezzo.