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