551 lines
21 KiB
Text
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.
|