Les effets de particules agrémentent grandement les visuels du jeu. Ils ne sont généralement pas l’objet principal d’un jeu, mais de nombreux jeux s’appuient sur des effets de particules pour augmenter leur richesse visuelle. Ils sont partout: nuages de poussière, feu, éclaboussures d’eau, etc. Les effets de particules sont généralement mis en œuvre avec discret mouvement d'émetteur et discret l'émission "éclate". La plupart du temps, tout va bien; cependant, les choses se détériorent lorsque vous avez un émetteur rapide et taux d'émission élevé. Lorsque cela est interpolation de sous-trame entre en jeu.
Cette démo Flash montre la différence entre une implémentation commune d'un émetteur en mouvement rapide et l'approche d'interpolation de sous-trames à différentes vitesses..
Tout d’abord, examinons une implémentation courante des effets de particules. Je présenterai une implémentation très minimaliste d’un émetteur ponctuel; sur chaque image, il crée de nouvelles particules à sa position, intègre les particules existantes, suit la vie de chaque particule et supprime les particules mortes.
Par souci de simplicité, je n’utiliserai pas les pools d’objets pour réutiliser des particules mortes; aussi, je vais utiliser le Vector.splice
méthode pour éliminer les particules mortes (vous ne voulez généralement pas le faire parce que Vector.splice
est une opération à temps linéaire). L'objectif principal de ce tutoriel n'est pas l'efficacité, mais la manière dont les particules sont initialisées.
Voici quelques fonctions d'aide dont nous aurons besoin plus tard:
// interpolation linéaire fonction publique lerp (a: nombre, b: nombre, t: nombre): nombre retour a + (b - a) * t; // retourne un nombre aléatoire uniforme fonction publique random (moyenne: nombre, variation: nombre): nombre retour moyen + 2.0 * (Math.random () - 0,5) * variation;
Et ci-dessous est le Particule
classe. Il définit certaines propriétés communes des particules, notamment la durée de vie, le temps de croissance et de contraction, la position, la rotation, la vitesse linéaire, la vitesse angulaire et l'échelle. Dans la boucle de mise à jour principale, la position et la rotation sont intégrées et les données de la particule sont finalement transférées dans l'objet d'affichage représenté par la particule. L'échelle est mise à jour en fonction de la durée de vie restante de la particule, comparée à son temps de croissance et de contraction.
public class Particle // objet d'affichage représenté par cette particule public var display: DisplayObject; // durée de vie actuelle et initiale, en secondes public var initLife: Number; public var life: Number; // temps de croissance en secondes public var growTime: Number; // durée de rétraction en secondes public var shrinkTime: Number; // position public var x: Number; public var y: Number; // vitesse linéaire public var vx: Number; var vy public: nombre; // angle d'orientation en degrés public var rotation: Number; // vitesse angulaire public var omega: Number; // échelle initiale et courante public var initScale: Number; échelle var publique: Nombre; // constructeur public function Particle (display: DisplayObject) this.display = display; // mise à jour de la fonction publique de la boucle de mise à jour principale (dt: Number): void // position d'intégration x + = vx * dt; y + = vy * dt; // intégrer la rotation d'orientation + = omega * dt; // décrémente la vie la vie - = dt; // calcule l'échelle si (vie> initLife - growTime) scale = lerp (0.0, initScale, (initLife - life) / growTime); sinon si (vie < 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;
Et enfin, nous avons l’émetteur de point lui-même. Dans la boucle de mise à jour principale, de nouvelles particules sont créées, toutes les particules sont mises à jour, puis les particules mortes sont éliminées. Le reste de ce didacticiel se concentrera sur l’initialisation des particules dans le createParticles ()
méthode.
public class PointEmitter // particules par seconde public var emissionRate: Number; // position de l'émetteur public var position: Point; // durée de vie et variation en secondes de public var var particleLife: Number; public var particleLifeVar: Number; // échelle et variation des particules public var particleScale: Number; public var particleScaleVar: Number; // temps de croissance et de contraction des particules en pourcentage dans la durée de vie (0,0 à 1,0) public var particleGrowRatio: Number; public var particleShrinkRatio: Number; // vitesse et variation des particules public var particleSpeed: Number; public var particleSpeedVar: Number; // variation de la vitesse angulaire d'une particule en degrés par seconde public var particleOmegaVar: Number; // les nouvelles particules du conteneur sont ajoutées au conteneur var privé: DisplayObjectContainer; // l'objet de classe pour l'instanciation de nouvelles particules private var displayClass: Class; // vecteur contenant des objets de particules private var particules: Vector.; // constructeur public function PointEmitter (conteneur: DisplayObjectContainer, displayClass: Class) this.container = conteneur; this.displayClass = displayClass; this.position = new Point (); this.particles = nouveau vecteur. (); // crée une nouvelle fonction privée de particule createParticles (numParticles: uint, dt: Number): void pour (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: Particule = particules [i]; // vérifie si la particule est morte si (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();
Si nous utilisons cet émetteur de particules et le déplaçons dans un mouvement circulaire, voici ce que nous obtiendrons:
Ça a l'air bien, non? Voyons ce qui se passe si nous augmentons la vitesse de déplacement de l'émetteur:
Voir le point discret "éclats"? Celles-ci sont dues à la manière dont l'implémentation actuelle suppose que l'émetteur se "téléporte" vers des points discrets d'une trame à l'autre. De plus, les nouvelles particules dans chaque image sont initialisées comme si elles étaient créées en même temps et éclatées en même temps..
Concentrons-nous maintenant sur la partie spécifique du code qui aboutit à cet artefact dans le PointEmitter.createParticles ()
méthode:
p.x = position.x; p.y = position.y; p.life = p.initLife;
Pour compenser le mouvement discret de l'émetteur et lui donner l'impression que le mouvement de l'émetteur est lisse, simulant également l'émission continue de particules, nous allons appliquer interpolation de sous-trame.
dans le Point Emetteur
classe, nous aurons besoin d’un drapeau booléen pour activer l’interpolation des sous-images, et un extra Point
pour garder la trace de la position précédente:
public var useSubFrameInterpolation: Boolean; private var prevPosition: Point;
Au début de la PointEmitter.update ()
méthode, nous avons besoin d’une première initialisation, qui assigne la position actuelle à prevPosition
. Et à la fin du PointEmitter.update ()
méthode, nous allons enregistrer la position actuelle et l'enregistrer dans prevPosition
.
C'est donc ce que le nouveau PointEmitter.update ()
méthode ressemble à (les lignes surlignées sont nouvelles):
mise à jour de la fonction publique (dt: Number): void // première initialisation if (! prevPosition) prevPosition = position.clone (); var newParticlesPerFrame: Number = emissionRate * dt; var numNewParticles: uint = uint (newParticlesPerFrame); si (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();
Enfin, nous appliquerons l’interpolation de sous-images à l’initialisation de particules dans le PointEmitter.createParticles ()
méthode. Pour simuler une émission continue, l'initialisation de la position de la particule effectue maintenant une interpolation linéaire entre la position actuelle et la position précédente de l'émetteur. L'initialisation de la durée de vie des particules simule également le "temps écoulé" depuis la dernière image jusqu'à la création de la particule. Le "temps écoulé" est une fraction de dt
et est également utilisé pour intégrer la position des particules.
Nous allons donc changer le code suivant à l'intérieur du pour
boucle dans le PointEmitter.createParticles ()
méthode:
p.x = position.x; p.y = position.y; p.life = p.initLife;
… À ceci (rappelez-vous que je
est la variable de boucle):
if (useSubFrameInterpolation) // interpolation de sous-trame 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 // initialisation normale p.x = position.x; p.y = position.y; p.life = p.initLife;
Voici à quoi cela ressemble quand l’émetteur de particules se déplace à grande vitesse avec interpolation de sous-trame:
Beaucoup mieux!
Malheureusement, l'interpolation des sous-images à l'aide d'une interpolation linéaire n'est toujours pas parfaite. Si nous augmentons encore la vitesse du mouvement circulaire de l'émetteur, voici ce que nous obtiendrons:
Cet artefact est provoqué en essayant de faire correspondre la courbe circulaire avec une interpolation linéaire. Une façon de remédier à cela est de ne pas simplement garder une trace de la position de l'émetteur dans la trame précédente, mais plutôt de garder une trace de la position précédente dans plusieurs cadres et interpoler entre ces points en utilisant des courbes lisses (comme les courbes de Bézier).
À mon avis, cependant, l'interpolation linéaire est plus que suffisant. La plupart du temps, les émetteurs de particules ne bougent pas assez rapidement pour provoquer la rupture de l'interpolation de sous-images avec interpolation linéaire..
Les effets de particules peuvent s’effondrer lorsque l’émetteur de particules se déplace à grande vitesse et a un taux d’émission élevé. La nature discrète de l'émetteur devient visible. Pour améliorer la qualité visuelle, utilisez une interpolation de sous-trame pour simuler un mouvement régulier de l'émetteur et une émission continue. Sans introduire trop de temps, une interpolation linéaire est généralement utilisée.
Cependant, un artefact différent commencerait à apparaître si l'émetteur se déplaçait encore plus rapidement. Une interpolation à courbe lisse peut être utilisée pour résoudre ce problème, mais l’interpolation linéaire fonctionne généralement assez bien et constitue un bon équilibre entre efficacité et qualité visuelle..