Faites votre jeu Pop avec des effets de particules et Quadtrees

Vous voulez donc des explosions, des tirs, des balles ou des sorts magiques dans votre jeu? Les systèmes de particules font de grands effets graphiques simples pour pimenter un peu votre jeu. Vous pouvez épater encore plus le joueur en faisant en sorte que les particules interagissent avec votre monde, en rebondissant sur l'environnement et les autres joueurs. Dans ce didacticiel, nous allons implémenter quelques effets de particules simples, et à partir de là, nous allons faire en sorte que les particules rebondissent sur le monde qui les entoure..

Nous allons également optimiser les choses en implémentant une structure de données appelée quadtree. Les quad-arbres vous permettent de vérifier les collisions beaucoup plus rapidement que vous ne le pourriez sans, et ils sont simples à mettre en œuvre et à comprendre..

Remarque: Bien que ce tutoriel soit écrit en HTML5 et JavaScript, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..

Pour voir les démonstrations incluses dans l'article, veillez à lire cet article dans Chrome, Firefox, IE 9 ou tout autre navigateur prenant en charge HTML5 et Canvas..
Remarquez comment les particules changent de couleur à mesure qu'elles tombent et comment elles rebondissent sur les formes.

Qu'est-ce qu'un système de particules??

Un système de particules est un moyen simple de générer des effets tels que le feu, la fumée et les explosions.

Vous créez un émetteur de particules, et cela lance de petites "particules" que vous pouvez afficher sous forme de pixels, de zones ou de petites images bitmap. Ils suivent la physique newtonienne et changent de couleur au fur et à mesure qu'ils se déplacent, produisant des effets graphiques dynamiques et personnalisables..


Le début d'un système de particules

Notre système de particules aura quelques paramètres réglables:

  • Combien de particules il crache chaque seconde.
  • Combien de temps une particule peut "vivre".
  • Les couleurs de chaque particule passeront à travers.
  • La position et l'angle des particules vont apparaître.
  • À quelle vitesse les particules iront quand elles apparaissent.
  • Combien de gravité devrait affecter les particules.

Si chaque particule produisait exactement la même chose, nous aurions simplement un flux de particules, pas un effet de particule. Donc, autorisons également la variabilité configurable. Cela nous donne quelques paramètres supplémentaires pour notre système:

  • Combien leur angle de lancement peut varier.
  • Combien leur vitesse initiale peut varier.
  • Combien leur vie peut varier.

Nous nous retrouvons avec une classe de système de particules qui commence comme ceci:

 function ParticleSystem (params) // Paramètres par défaut this.params = // Où les particules apparaissent-elles à partir de pos: new Point (0, 0), // Combien de particules apparaissent toutes les secondes PartPreSecond: 100, // Durée de vie de chaque particule particleLife: 0.5, lifeVariation: 0.52, // Le dégradé de couleurs que la particule voyagera à travers les couleurs: new Gradient ([new Color (255, 255, 255, 1)), new Color (0, 0, 0, 0)]), // L'angle sous lequel la particule se déclenchera (et dans quelle mesure celui-ci peut varier) angle: 0, angleVariation: Math.PI * 2, // plage de vitesse dans laquelle la particule se déclenchera minVelocity: 20, maxVelocity: 50, // Le vecteur de gravité appliqué à chaque particule de gravité: new Point (0, 30.8), // Objet à tester en vue de collisions et facteur d'amortissement du rebond // pour ledit collisionneur collisions: null, bounceDamper: 0,5; // Remplace nos paramètres par défaut avec les paramètres fournis pour (var p dans params) this.params [p] = params [p];  this.particles = []; 

Faire circuler le système

Chaque image doit faire trois choses: créer de nouvelles particules, déplacer les particules existantes et dessiner les particules.

Création de particules

La création de particules est assez simple. Si nous créons 300 particules par seconde et que cela fait 0,05 seconde depuis la dernière image, nous créons 15 particules pour l’image (moyenne de 300 par seconde)..

Nous devrions avoir une boucle simple qui ressemble à ceci:

 var newParticlesThisFrame = this.params.particlesPerSecond * frameTime; pour (var i = 0; i < newParticlesThisFrame; i++)  this.spawnParticle((1.0 + i) / newParticlesThisFrame * frameTime); 

Notre spawnParticle () function crée une nouvelle particule basée sur les paramètres de notre système:

 ParticleSystem.prototype.spawnParticle = function (offset) // Nous voulons déclencher la particule selon un angle et une vitesse aléatoires // dans les paramètres dictés pour ce système var angle = randVariation (this.params.angle, this. params.angleVariation); var speed = randRange (this.params.minVelocity, this.params.maxVelocity); var life = randVariation (this.params.particleLife, this.params.particleLife * this.params.lifeVariation); // Notre vitesse initiale se déplace à la vitesse que nous avons choisie ci-dessus dans la direction // de l'angle que nous avons choisi var velocity = new Point (). FromPolar (angle, vitesse); // Si nous créions chaque particule à "pos", chaque particule // créée dans une image commencerait au même endroit. // Au lieu de cela, nous agissons comme si nous créions la particule continuellement entre // cette image et l'image précédente, en la démarrant à un certain décalage // sur son trajet. var pos = this.params.pos.clone (). add (velocity.times (offset)); // Contruct un nouvel objet particule à partir des paramètres choisis, this.particles.push (new Particle (this.params, pos, velocity, life)); ;

Nous choisissons notre vitesse initiale à partir d'un angle et d'une vitesse aléatoires. Nous utilisons ensuite le dePolar () méthode pour créer un vecteur vitesse cartésien à partir de la combinaison angle / vitesse.

La trigonométrie de base donne la dePolaire méthode:

 Point.prototype.fromPolar = fonction (ang, rad) this.x = Math.cos (ang) * rad; this.y = Math.sin (ang) * rad; retournez ceci; ;

Si vous avez besoin de rafraîchir un peu la trigonométrie, toute la trigonométrie que nous utilisons est dérivée du cercle unitaire..

Mouvement de particules

Le mouvement des particules suit les lois newtoniennes de base. Les particules ont toutes une vitesse et une position. La force de gravité agit sur notre vitesse et notre position change proportionnellement à la gravité. Enfin, nous devons suivre la vie de chaque particule, sans quoi les particules ne mourraient jamais, nous en aurions trop et le système serait paralysé. Toutes ces actions se produisent proportionnellement au temps entre les images.

 Particle.prototype.step = function (frameTime) this.velocity.add (this.params.gravity.times (frameTime)); this.pos.add (this.velocity.times (frameTime)); this.life - = frameTime; ;

Dessin de particules

Enfin, nous devons dessiner nos particules. La manière dont vous l'implémenterez dans votre jeu variera considérablement d'une plate-forme à l'autre et à quel point vous voulez que le rendu soit avancé. Cela peut être aussi simple que de placer un seul pixel coloré, de déplacer une paire de triangles pour chaque particule, dessinés par un shader GPU complexe.

Dans notre cas, nous allons tirer parti de l'API Canvas pour dessiner un petit rectangle pour la particule..

 Particle.prototype.draw = function (ctx, frameTime) // Il n'est pas nécessaire de dessiner la particule si elle est en dehors de la vie. if (this.isDead ()) renvoie; // Nous voulons parcourir notre dégradé de couleurs à mesure que la particule vieillit. Var lifePercent = 1.0 - this.life / this.maxLife; var color = this.params.colors.getColor (lifePercent); // Définir les couleurs ctx.globalAlpha = color.a; ctx.fillStyle = color.toCanvasColor (); // Complétez le rectangle à la position de la particule ctx.fillRect (this.pos.x - 1, this.pos.y - 1, 3, 3); ;

L'interpolation des couleurs dépend de si la plate-forme que vous utilisez fournit une classe de couleurs (ou un format de représentation), d'un interpolateur pour vous et de la manière dont vous souhaitez aborder l'ensemble du problème. J'ai écrit une petite classe de dégradé qui permet une interpolation facile entre plusieurs couleurs, et une petite classe de couleur offrant la fonctionnalité permettant d'interpoler entre deux couleurs quelconques..

 Color.prototype.interpolate = function (pourcent, autre) retourne une nouvelle couleur (this.r + (other.r - this.r) * pourcent, this.g + (other.g - this.g) * pourcent, this .b + (other.b - this.b) * pour cent, this.a + (autre.a - this.a) * pour cent); ; Gradient.prototype.getColor = fonction (pourcentage) // Emplacement de la couleur en virgule flottante dans le tableau var colorF = pourcentage * (this.colors.length - 1); //Arrondir vers le bas; c'est la couleur spécifiée dans le tableau // en dessous de notre couleur actuelle var color1 = parseInt (colorF); //Rassembler; c'est la couleur spécifiée dans le tableau // au-dessus de notre couleur actuelle var color2 = parseInt (colorF + 1); // Interpole entre les deux couleurs les plus proches (en utilisant la méthode ci-dessus) renvoie this.colors [color1] .interpolate ((colorF - color1) / (color2 - color1), this.colors [color2]); ;

Voici notre système de particules en action!

Particules rebondissantes

Comme vous pouvez le voir dans la démo ci-dessus, nous avons maintenant quelques effets de base sur les particules. Ils n'ont cependant aucune interaction avec l'environnement qui les entoure. Pour que ces effets fassent partie de notre monde du jeu, nous allons les faire rebondir sur les murs qui les entourent..

Pour commencer, le système de particules prendra maintenant une collisionneur en paramètre. Ce sera le travail du collisionneur de dire à une particule si elle s'est écrasée dans quoi que ce soit. le étape() méthode d'une particule ressemble maintenant à ceci:

 Particle.prototype.step = function (frameTime) // Enregistrer notre dernière position var lastPos = this.pos.clone (); // Move this.velocity.add (this.params.gravity.times (frameTime)); this.pos.add (this.velocity.times (frameTime)); // Cette particule peut-elle rebondir? if (this.params.collider) // Vérifie si nous avons touché quelque chose var intersect = this.params.collider.getIntersection (new Line (lastPos, this.pos)); if (intersect! = null) // Si c'est le cas, nous réinitialisons notre position et mettons à jour notre vélocité // pour refléter la collision this.pos = lastPos; this.velocity = intersect.seg.reflect (this.velocity) .times (this.params.bounceDamper);  this.life - = frameTime; ;

Maintenant, chaque fois que la particule bouge, nous demandons au collisionneur si sa trajectoire de mouvement est "entrée en collision" via le getIntersection () méthode. Si tel est le cas, nous réinitialisons sa position (afin qu’elle ne soit pas à l’intérieur de ce qu’elle a intersecté) et reflétons la vitesse..

Une implémentation "collisionneur" de base pourrait ressembler à ceci:

 // Prend une collection de segments de ligne représentant la fonction du monde du jeu Collider (lines) this.lines = lines;  // Retourne tout segment de ligne coupé par "chemin", sinon null Collider.prototype.getIntersection = function (chemin) pour (var i = 0; i < this.lines.length; i++)  var intersection = this.lines[i].getIntersection(path); if (intersection) return intersection;  return null; ;

Notez un problème? Chaque particule a besoin d'appeler collider.getIntersection () et puis chaque getIntersection call doit vérifier tous les "murs" du monde. Si vous avez 300 particules (un nombre peu élevé) et 200 murs dans votre monde (pas déraisonnables non plus), vous effectuez des tests d'intersection de 60 000 lignes! Cela pourrait bloquer votre jeu, en particulier avec plus de particules (ou des mondes plus complexes).


Détection de collisions plus rapide avec les arbres quadruples

Le problème avec notre simple collisionneur est qu’il vérifie chaque mur pour chaque particule. Si notre particule se trouve dans le quadrant supérieur droit de l'écran, nous ne devrions pas perdre de temps à vérifier si elle s'est écrasée contre des murs situés en bas ou à gauche de l'écran. Donc, idéalement, nous voulons supprimer toutes les vérifications pour les intersections en dehors du quadrant supérieur droit:


Nous vérifions simplement les collisions entre le point bleu et les lignes rouges.

C'est juste un quart des chèques! Maintenant, allons encore plus loin: si la particule se trouve dans le quadrant supérieur gauche du quadrant supérieur droit de l'écran, il suffit de vérifier ces murs dans le même quadrant:

Les quadruples vous permettent de faire exactement cela! Plutôt que de tester contre tout murs, vous divisez les murs en quadrants et sous-quadrants qu’ils occupent, il vous suffit donc de vérifier quelques quadrants. Vous pouvez facilement passer de 200 contrôles par particule à seulement 5 ou 6.

Les étapes pour créer un quadtree sont les suivantes:

  1. Commencez avec un rectangle qui remplit tout l'écran.
  2. Prenez le rectangle actuel, comptez combien de "murs" sont dedans.
  3. Si vous avez plus de trois lignes (vous pouvez choisir un nombre différent), divisez le rectangle en quatre quadrants égaux. Répétez l'étape 2 avec chaque quadrant.
  4. Après avoir répété les étapes 2 et 3, vous vous retrouvez avec un "arbre" de rectangles, aucun des plus petits rectangles ne contenant plus de trois lignes (ou ce que vous avez choisi)..

Construire un quadtree. Les nombres représentent le nombre de lignes dans le quadrant, le rouge étant trop élevé et devant être subdivisé.

Pour construire notre quadtree, nous prenons un ensemble de "murs" (segments de ligne) en tant que paramètre, et si trop de personnes sont contenues dans notre rectangle, nous les subdivisons en rectangles plus petits et le processus se répète..

 QuadTree.prototype.addSegments = function (segs) pour (var i = 0; i < segs.length; i++)  if (this.rect.overlapsWithLine(segs[i]))  this.segs.push(segs[i]);   if (this.segs.length > 3) thisSubdivide (); ; QuadTree.prototype.subdivide = function () var w2 = this.rect.w / 2, h2 = this.rect.h / 2, x = this.rect.x, y = this.rect.y; this.quads.push (nouveau QuadTree (x, y, w2, h2)); this.quads.push (nouveau QuadTree (x + w2, y, w2, h2)); this.quads.push (nouveau QuadTree (x + w2, y + h2, w2, h2)); this.quads.push (nouveau QuadTree (x, y + h2, w2, h2)); pour (var i = 0; i < this.quads.length; i++)  this.quads[i].addSegments(this.segs);  this.segs = []; ;

Vous pouvez voir la classe complète QuadTree ici:

 / ** * @constructor * / function QuadTree (x, y, w, h) this.thresh = 4; this.segs = []; this.quads = []; this.rect = new Rect2D (x, y, w, h);  QuadTree.prototype.addSegments = fonction (segs) pour (var i = 0; i < segs.length; i++)  if (this.rect.overlapsWithLine(segs[i]))  this.segs.push(segs[i]);   if (this.segs.length > this.thresh) this.subdivide (); ; QuadTree.prototype.getIntersection = function (seg) if (! This.rect.overlapsWithLine (seg)) return null; pour (var i = 0; i < this.segs.length; i++)  var s = this.segs[i]; var inter = s.getIntersection(seg); if (inter)  var o = ; return s;   for (var i = 0; i < this.quads.length; i++)  var inter = this.quads[i].getIntersection(seg); if (inter) return inter;  return null; ; QuadTree.prototype.subdivide = function()  var w2 = this.rect.w / 2, h2 = this.rect.h / 2, x = this.rect.x, y = this.rect.y; this.quads.push(new QuadTree(x, y, w2, h2)); this.quads.push(new QuadTree(x + w2, y, w2, h2)); this.quads.push(new QuadTree(x + w2, y + h2, w2, h2)); this.quads.push(new QuadTree(x, y + h2, w2, h2)); for (var i = 0; i < this.quads.length; i++)  this.quads[i].addSegments(this.segs);  this.segs = []; ; QuadTree.prototype.display = function(ctx, mx, my, ibOnly)  var inBox = this.rect.containsPoint(new Point(mx, my)); ctx.strokeStyle = inBox ? '#FF44CC' : '#000000'; if (inBox || !ibOnly)  ctx.strokeRect(this.rect.x, this.rect.y, this.rect.w, this.rect.h); for (var i = 0; i < this.quads.length; i++)  this.quads[i].display(ctx, mx, my, ibOnly);   if (inBox)  ctx.strokeStyle = '#FF0000'; for (var i = 0 ; i < this.segs.length; i++)  var s = this.segs[i]; ctx.beginPath(); ctx.moveTo(s.a.x, s.a.y); ctx.lineTo(s.b.x, s.b.y); ctx.stroke();   ;

Le test d'intersection avec un segment de droite est effectué de manière similaire. Pour chaque rectangle, nous procédons comme suit:

  1. Commencez par le plus grand rectangle du quadtree.
  2. Vérifiez si le segment de ligne coupe ou se trouve dans le rectangle actuel. Si ce n'est pas le cas, ne prenez pas la peine de faire d'autres tests dans cette voie..
  3. Si le segment de ligne tombe dans le rectangle actuel ou l'intersecte, vérifiez si le rectangle actuel a des rectangles enfants. Si c'est le cas, retournez à l'étape 2, mais utilisez chacun des rectangles enfants..
  4. Si le rectangle en cours n'a pas de rectangles enfants mais que c'est un noeud feuille (c’est-à-dire qu’il n’a que des segments de ligne sous forme d’enfants), testez le segment de ligne cible par rapport à ces segments. S'il s'agit d'une intersection, renvoyez l'intersection. Nous avons fini!

Recherche d'un quadtree. Nous commençons par le plus grand rectangle et cherchons des plus petits et des plus petits, jusqu'à ce que nous testions enfin des segments individuels. Avec le quadtree, nous effectuons seulement quatre tests de rectangle et deux tests de ligne, au lieu de tester sur les 21 segments de ligne. La différence ne fait que s'aggraver avec des ensembles de données plus volumineux.
 QuadTree.prototype.getIntersection = function (seg) if (! This.rect.overlapsWithLine (seg)) return null; pour (var i = 0; i < this.segs.length; i++)  var s = this.segs[i]; var inter = s.getIntersection(seg); if (inter)  var o = ; return s;   for (var i = 0; i < this.quads.length; i++)  var inter = this.quads[i].getIntersection(seg); if (inter) return inter;  return null; ;

Une fois que nous passons un QuadTree En nous opposant à notre système de particules en tant que "collisionneur", nous obtenons des recherches rapides comme l'éclair. Découvrez la démo interactive ci-dessous - utilisez votre souris pour voir les segments de ligne sur lesquels l’arbre quadripartite doit être testé.!


Survolez un (sous) quadrant pour voir les segments de ligne qu'il contient.

Nourriture pour la pensée

Le système de particules et le quadtree présentés dans cet article sont des systèmes d'enseignement rudimentaires. Quelques autres idées que vous voudrez peut-être prendre en compte lorsque vous les implémenterez vous-même

  • Vous souhaiterez peut-être placer des objets en plus des segments de ligne dans l’arbre en quatre. Comment voudriez-vous l'élargir pour inclure les cercles? Des carrés?
  • Vous voudrez peut-être un moyen de récupérer des objets individuels (pour les informer qu'ils ont été touchés par une particule), tout en récupérant des segments pouvant être réfléchis.
  • Les équations de la physique souffrent des incohérences que les équations d'Euler créent avec le temps avec des taux de trame instables. Bien que cela ne compte généralement pas pour un système de particules, pourquoi ne pas lire sur des équations de mouvement plus avancées? (Jetez un coup d’œil à ce tutoriel, par exemple.)
  • Vous pouvez stocker la liste des particules de différentes manières dans la mémoire. Un tableau est plus simple mais peut ne pas être le meilleur choix étant donné que les particules sont souvent retirées du système et que de nouvelles sont souvent insérées. Une liste chaînée peut convenir mieux mais avoir une localisation de cache médiocre. La meilleure représentation des particules peut dépendre du cadre ou du langage que vous utilisez..
Articles Similaires
  • Utilisation de quadruples pour détecter les collisions probables dans un espace 2D