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..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..
Notre système de particules aura quelques paramètres réglables:
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:
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 = [];
Chaque image doit faire trois choses: créer de nouvelles particules, déplacer les particules existantes et dessiner les 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..
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; ;
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]); ;
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).
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:
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:
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:
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é.!
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