Effecten van deeltjes zorgen voor een strikter beeld van het spel. Ze zijn meestal niet het hoofddoel van een game, maar veel games vertrouwen op deeltjeseffecten om hun visuele rijkdom te vergroten. Ze zijn overal: stofwolken, vuur, waterspatten, noem maar op. Deeltjeseffecten worden meestal geïmplementeerd met discreet emitter beweging en discreet emissie "bursts". Meestal ziet alles er prima uit; echter, dingen gaan kapot als je een hebt snel bewegende emitter en hoge uitstoot. Dit is wanneer subframe interpolatie komt in het spel.
Deze Flash-demo toont het verschil tussen een gemeenschappelijke implementatie van een snel bewegende emitter en de subframe-interpolatiebenadering op verschillende snelheden.
Laten we eerst eens kijken naar een gemeenschappelijke implementatie van deeltjeseffecten. Ik zal een zeer minimalistische implementatie van een puntzender presenteren; op elk frame creëert het nieuwe deeltjes op zijn positie, integreert het bestaande deeltjes, houdt het de levensduur van elk deeltje in de gaten en verwijdert dode deeltjes.
Omwille van de eenvoud zal ik geen objectpools gebruiken om dode deeltjes opnieuw te gebruiken; ook, ik zal de gebruiken Vector.splice
methode om dode deeltjes te verwijderen (meestal wil je dit niet doen omdat Vector.splice
is een lineaire-tijdbewerking). De belangrijkste focus van deze zelfstudie is niet efficiëntie, maar hoe de deeltjes worden geïnitialiseerd.
Hier zijn enkele hulpfuncties die we later nodig hebben:
// lineaire interpolatie openbare functie lerp (a: Number, b: Number, t: Number): Number return a + (b - a) * t; // geeft een uniform willekeurig getal terug publieke functie willekeurig (gemiddelde: Getal, variatie: Getal): Getal return average + 2.0 * (Math.random () - 0.5) * variation;
En hieronder is het Deeltje
klasse. Het definieert enkele algemene deeltjeseigenschappen, waaronder levensduur, groei- en krimptijd, positie, rotatie, lineaire snelheid, hoeksnelheid en schaal. In de hoofdupdatalkring zijn positie en rotatie geïntegreerd en worden de deeltjegegevens uiteindelijk in het weergaveobject dat door het deeltje wordt weergegeven, gedumpt. De schaal wordt bijgewerkt op basis van de resterende levensduur van het deeltje, vergeleken met de groei- en krimptijd.
public class Particle // weergaveobject vertegenwoordigd door deze partikel public var display: DisplayObject; // huidige en initiële levensduur, in seconden public var initLife: Number; public var life: Number; // groeitijd in seconden public var growTime: Number; // tijd inkorten in seconden public var shrinkTime: Number; // position public var x: Number; public var y: Number; // linear velocity public var vx: Number; public var vy: Number; // oriëntatiehoek in graden public var rotation: Number; // Angular velocity public var omega: Number; // initiële & huidige schaal public var initScale: Number; public var scale: Number; // constructor public function Particle (display: DisplayObject) this.display = display; // hoofd update update publieke functie update (dt: Number): void // integrate position x + = vx * dt; y + = vy * dt; // integratie oriëntatierotatie + = omega * dt; // decrement life life - = dt; // schaal berekenen if (life> initLife - growTime) scale = lerp (0.0, initScale, (initLife - life) / growTime); anders als (leven < 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;
En tot slot hebben we de puntzender zelf. In de hoofdupdatalkring worden nieuwe deeltjes gemaakt, alle deeltjes worden bijgewerkt en vervolgens worden dode deeltjes verwijderd. De rest van deze tutorial zal zich richten op de deeltjesinitialisatie in de createParticles ()
methode.
public class PointEmitter // deeltjes per seconde public var emissionRate: Number; // positie van zender publieke positie: positie; // deeltje leven & variatie in seconden public var particleLife: Number; public var particleLifeVar: Number; // particle scale & variation public var particleScale: Number; public var particleScaleVar: Number; // deeltje groeit en krimpt tijd in levensduurpercentage (0,0 tot 1,0) public var particleGrowRatio: Number; public var particleShrinkRatio: Number; // particle speed & variation public var particleSpeed: Number; public var particleSpeedVar: Number; // deeltje hoeksnelheidsvariatie in graden per seconde public var particleOmegaVar: Number; // de container nieuwe deeltjes worden toegevoegd aan de private var-container: DisplayObjectContainer; // het klasseobject voor het instantiëren van nieuwe partikels private var displayClass: Class; // vector die deeltjesobjecten private var particles bevat: Vector.; // constructor public function PointEmitter (container: DisplayObjectContainer, displayClass: Class) this.container = container; this.displayClass = displayClass; this.position = new Point (); this.particles = new Vector. (); // maakt een nieuwe particle private function 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]; // controleer of deeltje dood is als (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();
Als we deze deeltjes-emitter gebruiken en deze in een cirkelvormige beweging laten bewegen, dan is dit wat we krijgen:
Ziet er goed uit, toch? Laten we eens kijken wat er gebeurt als we de bewegingssnelheid van de zender verhogen:
Zie het discrete punt "bursts"? Dit komt door de manier waarop de huidige implementatie veronderstelt dat de zender "teleporteert" naar discrete punten over frames. Ook worden nieuwe deeltjes in elk frame geïnitialiseerd alsof ze tegelijkertijd worden gemaakt en tegelijk worden uitgeblust.
Laten we ons nu richten op het specifieke deel van de code dat resulteert in dit artefact in de PointEmitter.createParticles ()
methode:
p.x = position.x; p.y = position.y; p.life = p.initLife;
Om de discrete emitterbeweging te compenseren en het te laten lijken alsof de emitterbeweging soepel is, en ook continue emissie van deeltjes simuleert, gaan we van toepassing zijn subframe interpolatie.
In de PointEmitter
klasse, hebben we een Booleaanse markering nodig voor het inschakelen van subframe-interpolatie en een extra Punt
voor het bijhouden van de vorige positie:
public var useSubFrameInterpolation: Boolean; privé var prevosition: punt;
Aan het begin van de PointEmitter.update ()
methode, hebben we een eerste initialisatie nodig, waaraan de huidige positie wordt toegewezen prevPosition
. En aan het einde van de PointEmitter.update ()
methode, zullen we de huidige positie registreren en opslaan prevPosition
.
Dus dit is wat het nieuwe is PointEmitter.update ()
methode ziet eruit (de gemarkeerde lijnen zijn nieuw):
update van public function (dt: Number): void // first-time initialization 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();
Ten slotte zullen we subframe-interpolatie toepassen op deeltjesinitialisatie in de PointEmitter.createParticles ()
methode. Om continue emissie te simuleren interpoleert de initialisatie voor deeltjespositie nu lineair tussen de huidige en vorige positie van de zender. De initialisatie van de levensduur van de deeltjes simuleert ook de "verstreken tijd" sinds het laatste frame tot de creatie van het deeltje. De "verstreken tijd" is een fractie van dt
en wordt ook gebruikt om de deeltjespositie te integreren.
We zullen daarom de volgende code binnen de voor
loop in de PointEmitter.createParticles ()
methode:
p.x = position.x; p.y = position.y; p.life = p.initLife;
... hieraan (onthoud dat ik
is de lusvariabele):
if (useSubFrameInterpolation) // subframe-interpolatie 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 - timeElpsed; else // reguliere initialisatie p.x = position.x; p.y = position.y; p.life = p.initLife;
Dit is hoe het eruitziet wanneer de deeltjesemitter met hoge snelheid beweegt met subframe-interpolatie:
Veel beter!
Helaas is subframe-interpolatie met behulp van lineaire interpolatie nog steeds niet perfect. Als we de snelheid van de cirkelvormige beweging van de zender verder verhogen, is dit wat we krijgen:
Dit artefact wordt veroorzaakt door te proberen de cirkelcurve aan te passen aan lineaire interpolatie. Een manier om dit te verhelpen is niet alleen om de positie van de emittent in het vorige frame te volgen, maar om eerdere positie binnen het vorige frame te volgen. meerdere frames en interpoleer tussen deze punten met behulp van vloeiende curven (zoals Bezier-curven).
Naar mijn mening is lineaire interpolatie echter meer dan voldoende. Meestal zal het niet zo zijn dat deeltjesuitstralers snel genoeg bewegen om subframe-interpolatie met lineaire interpolatie te veroorzaken.
Effecten van deeltjes kunnen afbreken wanneer de deeltjesemitter met een hoge snelheid beweegt en een hoge emissiegraad heeft. De discrete aard van de zender wordt zichtbaar. Om de visuele kwaliteit te verbeteren, gebruikt u subframe-interpolatie om een soepele emitterbeweging en continue emissie te simuleren. Zonder teveel overhead te introduceren, wordt meestal lineaire interpolatie gebruikt.
Er zou echter een ander artefact verschijnen als de zender nog sneller beweegt. Gladde curve-interpolatie kan worden gebruikt om dit probleem op te lossen, maar lineaire interpolatie werkt meestal goed genoeg en is een goede balans tussen efficiëntie en visuele kwaliteit.