Gli effetti particellari rendono molto più vivaci le immagini del gioco. Di solito non sono l'obiettivo principale di un gioco, ma molti giochi si basano su effetti particellari per aumentare la loro ricchezza visiva. Sono dappertutto: nuvole di polvere, fuoco, spruzzi d'acqua, lo chiami. Gli effetti particellari vengono solitamente implementati con discreto movimento di emettitore e discreto emissione "scoppia". Il più delle volte, tutto sembra a posto; tuttavia, le cose si guastano quando si ha a emettitore in rapido movimento e alto tasso di emissione. Questo è quando interpolazione del sub-frame entra in gioco.
Questa demo Flash mostra la differenza tra un'implementazione comune di un emettitore in rapido movimento e l'approccio di interpolazione della sottostruttura a velocità diverse.
Per prima cosa, diamo un'occhiata a un'implementazione comune degli effetti particellari. Presenterò un'implementazione molto minimalista di un emettitore di punti; su ogni fotogramma, crea nuove particelle nella sua posizione, integra le particelle esistenti, tiene traccia della vita di ogni particella e rimuove le particelle morte.
Per ragioni di semplicità, non userò i pool di oggetti per riutilizzare le particelle morte; inoltre, userò il Vector.splice
metodo per rimuovere le particelle morte (di solito non si vuole fare questo perché Vector.splice
è un'operazione lineare-temporale). L'obiettivo principale di questo tutorial non è l'efficienza, ma il modo in cui le particelle vengono inizializzate.
Ecco alcune funzioni di supporto che serviranno in seguito:
// funzione di interpolazione lineare public lerp (a: Number, b: Number, t: Number): Number return a + (b - a) * t; // restituisce una funzione pubblica di numeri casuali uniformi casuali (media: Numero, variazione: Numero): Number return average + 2.0 * (Math.random () - 0.5) * variazione;
E sotto è il particella
classe. Definisce alcune proprietà delle particelle comuni, tra cui durata, tempo di crescita e riduzione, posizione, rotazione, velocità lineare, velocità angolare e scala. Nel ciclo di aggiornamento principale, la posizione e la rotazione sono integrate e i dati delle particelle vengono infine scaricati nell'oggetto di visualizzazione rappresentato dalla particella. La scala viene aggiornata in base alla vita residua della particella, rispetto al tempo di crescita e restringimento.
public class Particle // display object rappresentato da questa particella public var display: DisplayObject; // vita attuale e iniziale, in secondi public var initLife: Number; public var life: Number; // tempo di crescita in secondi public var growTime: Number; // riduci il tempo in secondi public var shrinkTime: Number; // position public var x: Number; public var y: Number; // linear velocity public var vx: Number; public var vy: Number; // angolo di orientamento in gradi rotazione var pubblica: Number; // velocità angolare public var omega: Number; // scala iniziale e attuale public var initScale: Number; scala var pubblica: numero; // funzione public constructor Particle (display: DisplayObject) this.display = display; // aggiornamento della funzione pubblica del ciclo di aggiornamento principale (dt: Number): void // integrate posizione x + = vx * dt; y + = vy * dt; // integrare la rotazione dell'orientamento + = omega * dt; // decrementa life life - = dt; // calcola la scala se (life> initLife - growTime) scale = lerp (0.0, initScale, (initLife - life) / growTime); altrimenti se (vita < shrinkTime) scale = lerp(initScale, 0.0, (shrinkTime - life) / shrinkTime); else scale = initScale; // dump particle data into display object display.x = x; display.y = y; display.rotation = rotation; display.scaleX = display.scaleY = scale;
E infine, abbiamo il punto emettitore stesso. Nel ciclo di aggiornamento principale, vengono create nuove particelle, tutte le particelle vengono aggiornate e quindi le particelle morte vengono rimosse. Il resto di questo tutorial si concentrerà sull'inizializzazione delle particelle all'interno del createParticles ()
metodo.
public class PointEmitter // particles per secondo public var emissionRate: Number; // posizione dell'emettitore public var position: Point; // durata delle particelle e variazione in secondi public var particleLife: Number; public var particleLifeVar: Number; // scala delle particelle e variazione public var particleScale: Number; public var particleScaleVar: Number; // crescita della particella e tempo di restringimento in percentuale della vita (da 0.0 a 1.0) public var particleGrowRatio: Number; public var particleShrinkRatio: Number; // velocità delle particelle e variazione public var particleSpeed: Number; public var particleSpeedVar: Number; // variazione della velocità angolare delle particelle in gradi al secondo public var particleOmegaVar: Number; // il container nuove particelle vengono aggiunte al contenitore var privato: DisplayObjectContainer; // l'oggetto classe per l'istanziazione di nuove particelle private var displayClass: Class; // vettore che contiene particelle particle di particelle private: Vector.; // funzione public constructor PointEmitter (container: DisplayObjectContainer, displayClass: Class) this.container = container; this.displayClass = displayClass; this.position = new Point (); this.particles = new Vector. (); // crea una nuova funzione particella privata createParticles (numParticles: uint, dt: Number): void for (var i: uint = 0; i < numParticles; ++i) var p:Particle = new Particle(new displayClass()); container.addChild(p.display); particles.push(p); // initialize rotation & scale p.rotation = random(0.0, 180.0); p.initScale = p.scale = random(particleScale, particleScaleVar); // initialize life & grow & shrink time p.initLife = random(particleLife, particleLifeVar); p.growTime = particleGrowRatio * p.initLife; p.shrinkTime = particleShrinkRatio * p.initLife; // initialize linear & angular velocity var velocityDirectionAngle:Number = random(0.0, Math.PI); var speed:Number = random(particleSpeed, particleSpeedVar); p.vx = speed * Math.cos(velocityDirectionAngle); p.vy = speed * Math.sin(velocityDirectionAngle); p.omega = random(0.0, particleOmegaVar); // initialize position & current life p.x = position.x; p.y = position.y; p.life = p.initLife; // removes dead particles private function removeDeadParticles():void // It's easy to loop backwards with splicing going on. // Splicing is not efficient, // but I use it here for simplicity's sake. var i:int = particles.length; while (--i >= 0) var p: Particle = particles [i]; // controlla se la particella è morta se (p.life < 0.0) // remove from container container.removeChild(p.display); // splice it out particles.splice(i, 1); // main update loop public function update(dt:Number):void // calculate number of new particles per frame var newParticlesPerFrame:Number = emissionRate * dt; // extract integer part var numNewParticles:uint = uint(newParticlesPerFrame); // possibly add one based on fraction part if (Math.random() < newParticlesPerFrame - numNewParticles) ++numNewParticles; // first, create new particles createParticles(numNewParticles, dt); // next, update particles for each (var p:Particle in particles) p.update(dt); // finally, remove all dead particles removeDeadParticles();
Se usiamo questo emettitore di particelle e lo facciamo muovere in un movimento circolare, questo è ciò che otterremo:
Sembra buono, giusto? Vediamo cosa succede se aumentiamo la velocità di movimento dell'emettitore:
Vedi il punto discreto "raffiche"? Ciò è dovuto al modo in cui l'attuale implementazione presuppone che l'emettitore "teletrasporti" su punti discreti attraverso i frame. Inoltre, le nuove particelle all'interno di ciascun fotogramma vengono inizializzate come se fossero create contemporaneamente e interrotte in una sola volta.
Concentriamoci ora sulla parte specifica del codice che risulta in questo artefatto in PointEmitter.createParticles ()
metodo:
p.x = position.x; p.y = position.y; p.life = p.initLife;
Per compensare il movimento dell'emettitore discreto e far sembrare che il movimento dell'emettitore sia regolare, simulando anche l'emissione continua di particelle, applicheremo interpolazione del sub-frame.
Nel PointEmitter
classe, avremo bisogno di un flag booleano per attivare l'interpolazione del sub-frame e un extra Punto
per tenere traccia della posizione precedente:
public var useSubFrameInterpolation: Boolean; private var prevPosition: Point;
All'inizio del PointEmitter.update ()
metodo, abbiamo bisogno di una prima inizializzazione, che assegna la posizione corrente a prevPosition
. E alla fine del PointEmitter.update ()
metodo, registreremo la posizione corrente e la salveremo in prevPosition
.
Quindi questo è ciò che è nuovo PointEmitter.update ()
il metodo sembra (le linee evidenziate sono nuove):
aggiornamento della funzione pubblica (dt: Number): void // initialization per la prima volta if (! prevPosition) prevPosition = position.clone (); var newParticlesPerFrame: Number = emissionRate * dt; var numNewParticles: uint = uint (newParticlesPerFrame); if (Math.random () < newParticlesPerFrame - numNewParticles) ++numNewParticles; createParticles(numNewParticles, dt); for each (var p:Particle in particles) p.update(dt); removeDeadParticles(); // record previous position prevPosition = position.clone();
Infine, applicheremo l'interpolazione sub-frame all'inizializzazione delle particelle nel PointEmitter.createParticles ()
metodo. Per simulare l'emissione continua, l'inizializzazione per la posizione delle particelle ora interpola linearmente tra la posizione attuale e quella precedente dell'emettitore. L'inizializzazione della durata della particella simula anche il "tempo trascorso" dall'ultimo frame fino alla creazione della particella. Il "tempo trascorso" è una frazione di dt
ed è anche usato per integrare la posizione delle particelle.
Pertanto modificheremo il seguente codice all'interno di per
loop in PointEmitter.createParticles ()
metodo:
p.x = position.x; p.y = position.y; p.life = p.initLife;
... a questo (ricordalo io
è la variabile del ciclo):
if (useSubFrameInterpolation) // interpolation sub-frame var t: Number = Number (i) / Number (numParticles); var timeElapsed: Number = (1.0 - t) * dt; p.x = lerp (prevPosition.x, position.x, t); p.y = lerp (prevPosition.y, position.y, t); p.x + = p.vx * timeElapsed; p.y + = p.vy * timeElapsed; p.life = p.initLife - timeElapsed; else // inizializzazione regolare p.x = position.x; p.y = position.y; p.life = p.initLife;
Ora, questo è quello che sembra quando l'emettitore di particelle si muove ad alta velocità con l'interpolazione sotto-frame:
Molto meglio!
Sfortunatamente, l'interpolazione del sub-frame usando l'interpolazione lineare non è ancora perfetta. Se aumentiamo ulteriormente la velocità del movimento circolare dell'emettitore, questo è ciò che otterremo:
Questo artefatto è causato dal tentativo di far corrispondere la curva circolare con l'interpolazione lineare. Un modo per ovviare a questo non è solo tenere traccia della posizione dell'emettitore nel fotogramma precedente, ma invece di tenere traccia della posizione precedente all'interno multiplo fotogrammi e interpola tra questi punti usando curve morbide (come le curve di Bezier).
Secondo me, tuttavia, l'interpolazione lineare è più che sufficiente. La maggior parte delle volte, non si avranno emettitori di particelle che si muovono abbastanza velocemente da causare l'interpolazione del sub-frame con interpolazione lineare da abbattere.
Gli effetti particellari possono rompersi quando l'emettitore di particelle si muove ad alta velocità e ha un alto tasso di emissione. La natura discreta dell'emettitore diventa visibile. Per migliorare la qualità visiva, utilizzare l'interpolazione del sottotelaio per simulare il movimento dell'emettitore e l'emissione continua. Senza introdurre troppe spese generali, viene solitamente utilizzata l'interpolazione lineare.
Tuttavia, un diverso artefatto comincerebbe a comparire se l'emettitore si muove ancora più velocemente. È possibile utilizzare l'interpolazione curva lineare per risolvere questo problema, ma l'interpolazione lineare di solito funziona abbastanza bene ed è un buon equilibrio tra efficienza e qualità visiva.