Machen Sie Ihr Spiel mit Partikeleffekten und Quadtrees zum Knall

Sie wollen also Explosionen, Feuer, Kugeln oder Zaubersprüche in Ihrem Spiel? Partikelsysteme sorgen für großartige einfache grafische Effekte, die Ihr Spiel ein wenig aufpeppen. Sie können den Spieler noch mehr begeistern, indem Sie die Partikel mit Ihrer Welt interagieren lassen und von der Umgebung und anderen Spielern abprallen. In diesem Tutorial werden wir einige einfache Partikeleffekte implementieren. Von hier aus werden die Partikel von der sie umgebenden Welt abprallen lassen.

Wir werden auch die Dinge optimieren, indem wir eine Datenstruktur namens Quadtree implementieren. Mit Quadtrees können Sie wesentlich schneller auf Kollisionen prüfen, als dies ohne Kollision möglich wäre. Sie sind einfach zu implementieren und zu verstehen.

Hinweis: Obwohl dieses Tutorial mit HTML5 und JavaScript geschrieben wurde, sollten Sie in der Lage sein, in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte zu verwenden.

Lesen Sie diesen Artikel in Chrome, Firefox, IE 9 oder einem anderen Browser, der HTML5 und Canvas unterstützt, um die In-Artikel-Demos zu sehen.
Beachten Sie, wie die Partikel die Farbe ändern, wenn sie fallen, und wie sie von den Formen abprallen.

Was ist ein Partikelsystem??

Ein Partikelsystem ist eine einfache Methode, um Effekte wie Feuer, Rauch und Explosionen zu erzeugen.

Sie erstellen eine Partikelemitter, Dadurch werden kleine "Partikel" gestartet, die Sie als Pixel, Boxen oder kleine Bitmaps anzeigen können. Sie folgen der einfachen Newtonschen Physik und ändern ihre Farbe, wenn sie sich bewegen, was zu dynamischen, anpassbaren grafischen Effekten führt.


Der Beginn eines Partikelsystems

Unser Partikelsystem wird einige einstellbare Parameter haben:

  • Wie viele Partikel spuckt es pro Sekunde aus?.
  • Wie lange kann ein Partikel "leben".
  • Die Farben, die jedes Partikel durchläuft.
  • Die Position und der Winkel, aus dem die Partikel austreten.
  • Wie schnell werden die Partikel abtauchen, wenn sie laichen.
  • Wie viel Schwerkraft sollte Partikel beeinflussen.

Wenn jedes Partikel genau gleich ist, haben wir nur einen Partikelstrom und keinen Partikeleffekt. Lassen Sie uns auch konfigurierbare Variabilität zulassen. Dies gibt uns einige weitere Parameter für unser System:

  • Wie weit ihr Startwinkel variieren kann.
  • Wie viel ihre Anfangsgeschwindigkeit kann variieren.
  • Wie viel kann ihre Lebensdauer variieren.

Wir erhalten eine Partikelsystemklasse, die wie folgt beginnt:

 function ParticleSystem (params) // Standardparameter this.params = // Wo Partikel aus pos: new Point (0, 0) erscheinen, // wie viele Partikel pro Sekunde Partikel erzeugenPerSecond: 100, // wie lange jedes Partikel lebt? (und wie viel davon variieren kann) ParticleLife: 0.5, lifeVariation: 0.52, // Der Farbverlauf der Partikel, den das Partikel durchläuft, ist der neue Farbverlauf (neue Farbe (255, 255, 255, 1), neue Farbe (0, 0, 0, 0)]), // Der Winkel, unter dem das Partikel ausgelöst wird (und wie viel davon variieren kann). Winkel: 0, angleVariation: Math.PI * 2, // Der Geschwindigkeitsbereich, mit dem das Partikel ausgelöst wird minVelocity: 20, maxVelocity: 50, // Der auf jedes Partikel angewendete Gravitationsvektor: Neuer Punkt (0, 30.8), // Ein auf Kollisionen zu prüfendes Objekt gegen den Dämpfungsfaktor // und für den Abprall-Dämpfungsfaktor. Kollider: null, bounceDamper: 0,5; // Überschreiben Sie unsere Standardparameter mit den angegebenen Parametern für (var p in params) this.params [p] = params [p];  this.particles = []; 

Das System fließen lassen

Bei jedem Frame müssen wir drei Dinge tun: Neue Partikel erstellen, vorhandene Partikel verschieben und Partikel zeichnen.

Partikel erzeugen

Das Erstellen von Partikeln ist ziemlich einfach. Wenn wir 300 Partikel pro Sekunde erstellen und seit dem letzten Frame 0,05 Sekunden vergangen sind, erstellen wir 15 Partikel für den Frame (durchschnittlich 300 pro Sekunde)..

Wir sollten eine einfache Schleife haben, die so aussieht:

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

Unsere spawnParticle () Funktion erstellt ein neues Partikel basierend auf den Parametern unseres Systems:

 ParticleSystem.prototype.spawnParticle = function (offset) // Wir möchten das Partikel mit einem zufälligen Winkel und einer zufälligen Geschwindigkeit // innerhalb der für dieses System vorgegebenen Parameter abfeuern. 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); // Unsere Anfangsgeschwindigkeit bewegt sich mit der Geschwindigkeit, die wir oben in // der Richtung des Winkels gewählt haben, die wir gewählt haben. Var Geschwindigkeit = neuer Punkt (). FromPolar (Winkel, Geschwindigkeit); // Wenn wir jedes einzelne Partikel bei "pos" erstellt hätten, würde jedes // innerhalb eines Rahmens erzeugte Partikel an derselben Stelle beginnen. // Stattdessen verhalten wir uns so, als würden wir das Teilchen kontinuierlich zwischen diesem // Frame und dem vorherigen Frame erzeugen, indem wir es mit einem bestimmten Offset // entlang seines Pfads beginnen. var pos = this.params.pos.clone (). add (Geschwindigkeit x (Offset)); // Konstruiere ein neues Partikelobjekt aus den Parametern, die wir gewählt haben this.particles.push (new Particle (this.params, pos, speed, life)); ;

Wir wählen unsere Anfangsgeschwindigkeit aus einem zufälligen Winkel und Geschwindigkeit. Wir benutzen dann die fromPolar () Methode zum Erstellen eines kartesischen Geschwindigkeitsvektors aus der Kombination aus Winkel und Geschwindigkeit.

Grundtrigonometrie ergibt die vonPolar Methode:

 Point.prototype.fromPolar = Funktion (ang, rad) this.x = Math.cos (ang) * rad; this.y = Math.sin (ang) * rad; kehre das zurück; ;

Wenn Sie die Trigonometrie ein wenig auffrischen müssen, wird die gesamte von uns verwendete Trigonometrie vom Einheitskreis abgeleitet.

Partikelbewegung

Die Bewegung von Partikeln folgt den grundlegenden Newtonschen Gesetzen. Partikel haben alle eine Geschwindigkeit und Position. Unsere Geschwindigkeit wird durch die Schwerkraft beeinflusst, und unsere Position ändert sich proportional zur Schwerkraft. Schließlich müssen wir das Leben jedes Teilchens im Auge behalten, sonst würden die Teilchen niemals sterben, wir würden am Ende zu viele haben und das System würde zum Erliegen kommen. Alle diese Aktionen finden proportional zur Zeit zwischen den Bildern statt.

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

Partikel zeichnen

Zum Schluss müssen wir unsere Partikel zeichnen. Wie Sie dies in Ihrem Spiel implementieren, hängt von Plattform zu Plattform stark ab und davon, wie fortschrittlich das Rendering sein soll. Dies kann so einfach sein wie das Platzieren eines einfarbigen Pixels oder das Bewegen eines Paares von Dreiecken für jedes Partikel, das von einem komplexen GPU-Shader gezeichnet wird.

In unserem Fall nutzen wir die Canvas-API, um ein kleines Rechteck für das Partikel zu zeichnen.

 Particle.prototype.draw = function (ctx, frameTime) // Die Partikel müssen nicht gezeichnet werden, wenn sie nicht mehr verwendet werden. if (this.isDead ()) return; // Wir möchten unseren Farbverlauf durchlaufen, wenn das Teilchen altern var lifePercent = 1.0 - this.life / this.maxLife; var color = this.params.colors.getColor (lifePercent); // Richten Sie die Farben ein ctx.globalAlpha = color.a; ctx.fillStyle = color.toCanvasColor (); // Fülle das Rechteck an der Partikelposition aus ctx.fillRect (this.pos.x - 1, this.pos.y - 1, 3, 3); ;

Die Farbinterpolation hängt davon ab, ob die von Ihnen verwendete Plattform eine Farbklasse (oder ein Darstellungsformat) bereitstellt, ob sie einen Interpolator bereitstellt und wie Sie das gesamte Problem angehen möchten. Ich habe eine kleine Gradientenklasse geschrieben, die eine einfache Interpolation zwischen mehreren Farben ermöglicht, und eine kleine Farbklasse, die die Funktionalität zur Interpolation zwischen zwei beliebigen Farben bietet.

 Color.prototype.interpolate = Funktion (Prozent, andere) neue Farbe zurückgeben (this.r + (other.r - this.r) * Prozent, this.g + (other.g - this.g) * Prozent, this .b + (andere.b - this.b) * Prozent, this.a + (andere.a - this.a) * Prozent); ; Gradient.prototype.getColor = Funktion (Prozent) // Farbe des Gleitpunkts innerhalb des Arrays var colorF = percent * (this.colors.length - 1); //Abrunden; Dies ist die angegebene Farbe im Array // unterhalb unserer aktuellen Farbe var color1 = parseInt (colorF); //Zusammenfassen; Dies ist die angegebene Farbe im Array // oberhalb unserer aktuellen Farbe var color2 = parseInt (colorF + 1); // Interpoliere zwischen den beiden nächstgelegenen Farben (mit obiger Methode) gib this.colors [color1] .interpolate ((colorF - color1) / (color2 - color1), this.colors [color2]); ;

Hier ist unser Partikelsystem in Aktion!

Springenden Teilchen

Wie Sie in der obigen Demo sehen können, haben wir nun einige grundlegende Partikeleffekte. Ihnen fehlt jedoch die Interaktion mit der Umgebung. Damit diese Effekte Teil unserer Spielwelt werden, müssen wir sie von den Wänden um sie herum abprallen lassen.

Zum Starten benötigt das Partikelsystem nun eine Collider als Parameter. Es ist die Aufgabe des Kolliders, einem Partikel mitzuteilen, ob er in irgendetwas eingestürzt ist. Das Schritt() Die Methode eines Teilchens sieht jetzt so aus:

 Particle.prototype.step = function (frameTime) // Speichern Sie unsere letzte Position var lastPos = this.pos.clone (); // Move this.velocity.add (this.params.gravity.times (frameTime)); this.pos.add (this.velocity.times (frameTime))); // Kann dieses Teilchen springen? if (this.params.collider) // Prüfen Sie, ob wir irgendetwas treffen. var intersect = this.params.collider.getIntersection (neue Zeile (lastPos, this.pos)); if (intersect! = null) // Wenn ja, setzen wir unsere Position zurück und aktualisieren // unsere Geschwindigkeit, um die Kollision wiederzugeben. this.pos = lastPos; this.velocity = intersect.seg.reflect (this.velocity) .times (this.params.bounceDamper);  this.life - = frameTime; ;

Jedes Mal, wenn sich das Teilchen bewegt, fragen wir den Collider, ob seine Bewegungsbahn über die "Kollision" erfolgt ist getIntersection () Methode. Wenn dies der Fall ist, setzen wir seine Position zurück (damit sie sich nicht innerhalb des Schnittpunkts befindet) und reflektieren die Geschwindigkeit.

Eine grundlegende "Collider" -Implementierung könnte folgendermaßen aussehen:

 // Nimmt eine Sammlung von Liniensegmenten auf, die die Spielweltfunktion darstellen. Collider (Linien) this.lines = lines;  // Gibt ein beliebiges Liniensegment zurück, das von "Pfad" geschnitten wird, andernfalls Null Collider.prototype.getIntersection = Funktion (Pfad) for (var i = 0; i < this.lines.length; i++)  var intersection = this.lines[i].getIntersection(path); if (intersection) return intersection;  return null; ;

Problem feststellen? Jedes Teilchen muss anrufen collider.getIntersection () und dann jeder getIntersection Anruf muss gegen jede "Mauer" der Welt prüfen. Wenn Sie 300 Partikel (eine Art niedriger Anzahl) und 200 Wände in Ihrer Welt haben (auch nicht unvernünftig), führen Sie 60.000 Schnittpunkttests durch! Dies könnte Ihr Spiel zum Stillstand bringen, insbesondere mit mehr Partikeln (oder komplexeren Welten)..


Schnellere Kollisionserkennung mit Quadtrees

Das Problem bei unserem einfachen Collider ist, dass er jede Wand auf jedes Partikel überprüft. Wenn sich unser Partikel im oberen rechten Quadranten des Bildschirms befindet, sollten Sie keine Zeit damit verschwenden, zu prüfen, ob es in Wände gefallen ist, die sich nur im unteren oder linken Bereich des Bildschirms befinden. Im Idealfall möchten wir die Prüfung auf Kreuzungen außerhalb des rechten oberen Quadranten ausschließen:


Wir prüfen nur auf Kollisionen zwischen dem blauen Punkt und den roten Linien.

Das ist nur ein Viertel der Schecks! Nun geht es noch weiter: Wenn sich das Teilchen im oberen linken Quadranten des oberen rechten Quadranten des Bildschirms befindet, müssen wir nur diese Wände im selben Quadranten überprüfen:

Quadtrees erlauben genau das zu tun! Anstatt es zu testen alles Mauern teilen Sie Wände in die Quadranten und Sub-Quadranten, die sie einnehmen, so dass Sie nur einige Quadranten überprüfen müssen. Sie können leicht von 200 Prüfungen pro Partikel bis zu 5 oder 6 gehen.

Um einen Quadtree zu erstellen, gehen Sie folgendermaßen vor:

  1. Beginnen Sie mit einem Rechteck, das den gesamten Bildschirm ausfüllt.
  2. Nehmen Sie das aktuelle Rechteck und zählen Sie, wie viele "Wände" in das Rechteck fallen.
  3. Wenn Sie mehr als drei Zeilen haben (Sie können eine andere Anzahl wählen), teilen Sie das Rechteck in vier gleiche Quadranten auf. Wiederholen Sie Schritt 2 mit jedem Quadranten.
  4. Nachdem Sie die Schritte 2 und 3 wiederholt haben, erhalten Sie einen "Baum" von Rechtecken, wobei keines der kleinsten Rechtecke mehr als drei Zeilen enthält (oder was auch immer Sie gewählt haben)..

Einen Quadtree bauen. Die Zahlen stellen die Anzahl der Zeilen innerhalb des Quadranten dar, wobei Rot zu hoch ist und unterteilt werden muss.

Um unseren Quadtree zu erstellen, verwenden wir eine Reihe von "Wänden" (Liniensegmenten) als Parameter. Wenn zu viele in unserem Rechteck enthalten sind, unterteilen wir uns in kleinere Rechtecke, und der Vorgang wiederholt sich.

 QuadTree.prototype.addSegments = Funktion (segs) für (var i = 0; i < segs.length; i++)  if (this.rect.overlapsWithLine(segs[i]))  this.segs.push(segs[i]);   if (this.segs.length > 3) diese Unterteilung (); ; 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 (neuer QuadTree (x, y, w2, h2)); this.quads.push (neuer QuadTree (x + w2, y, w2, h2)); this.quads.push (neuer QuadTree (x + w2, y + h2, w2, h2)); this.quads.push (neuer QuadTree (x, y + h2, w2, h2)); für (var i = 0; i < this.quads.length; i++)  this.quads[i].addSegments(this.segs);  this.segs = []; ;

Sie können die vollständige QuadTree-Klasse hier sehen:

 / ** * @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 = function (segs) für (var i = 0; i < segs.length; i++)  if (this.rect.overlapsWithLine(segs[i]))  this.segs.push(segs[i]);   if (this.segs.length > this.thresh) thisSubdivide (); ; QuadTree.prototype.getIntersection = function (seg) if (! This.rect.overlapsWithLine (seg)) gibt null zurück; für (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();   ;

Auf ähnliche Weise wird der Schnittpunkt mit einem Liniensegment getestet. Für jedes Rechteck machen wir folgendes:

  1. Beginnen Sie mit dem größten Rechteck im Quadtree.
  2. Prüfen Sie, ob sich das Liniensegment schneidet oder sich innerhalb des aktuellen Rechtecks ​​befindet. Wenn dies nicht der Fall ist, sollten Sie diesen Pfad nicht weiter testen.
  3. Wenn das Liniensegment in das aktuelle Rechteck fällt oder es schneidet, prüfen Sie, ob das aktuelle Rechteck über untergeordnete Rechtecke verfügt. Wenn dies der Fall ist, kehren Sie zu Schritt 2 zurück, verwenden Sie jedoch alle untergeordneten Rechtecke.
  4. Wenn das aktuelle Rechteck keine untergeordneten Rechtecke hat, ist es jedoch ein Blattknoten (das heißt, es hat nur Liniensegmente als untergeordnete Elemente), testen Sie das Ziel-Liniensegment gegen diese Liniensegmente. Wenn es sich um eine Kreuzung handelt, geben Sie die Kreuzung zurück. Wir sind fertig!

Quadtree suchen. Wir beginnen am größten Rechteck und suchen nach kleineren und kleineren, bis schließlich einzelne Liniensegmente getestet werden. Mit dem Quadtree führen wir nur vier Rechtecktests und zwei Linientests durch, anstatt alle 21 Liniensegmente zu testen. Der Unterschied wird nur bei größeren Datensätzen dramatischer.
 QuadTree.prototype.getIntersection = function (seg) if (! This.rect.overlapsWithLine (seg)) gibt null zurück; für (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; ;

Sobald wir eine QuadTree Wenn Sie gegen unser Partikelsystem Einspruch erheben, erhalten wir als "Collider" blitzschnelle Nachschläge. Sehen Sie sich unten die interaktive Demo an. Verwenden Sie Ihre Maus, um zu sehen, mit welchen Liniensegmenten der Quadtree getestet werden muss!


Bewegen Sie den Mauszeiger über einen (Unter-) Quadranten, um zu sehen, welche Liniensegmente er enthält.

Denkanstöße

Das in diesem Artikel vorgestellte Partikelsystem und Quadtree sind rudimentäre Lehrsysteme. Einige andere Ideen, die Sie möglicherweise bei der Implementierung dieser Ideen berücksichtigen sollten:

  • Möglicherweise möchten Sie Objekte neben Liniensegmenten im Quadtree halten. Wie würden Sie es erweitern, um Kreise einzubeziehen? Quadrate?
  • Sie möchten möglicherweise eine Möglichkeit, einzelne Objekte abzurufen (um sie darüber zu informieren, dass sie von einem Partikel getroffen wurden), während sie weiterhin reflektierbare Segmente abrufen.
  • Die Physikgleichungen leiden unter Diskrepanzen, die Euler-Gleichungen im Laufe der Zeit mit instabilen Frameraten aufbauen. Während dies für ein Partikelsystem im Allgemeinen keine Rolle spielt, sollten Sie sich die fortgeschritteneren Bewegungsgleichungen ansehen. (Sehen Sie sich zum Beispiel dieses Tutorial an.)
  • Es gibt viele Möglichkeiten, die Liste der Partikel im Speicher abzulegen. Ein Array ist am einfachsten, ist jedoch möglicherweise nicht die beste Wahl, da häufig Partikel aus dem System entfernt und häufig neue eingefügt werden. Eine verknüpfte Liste passt möglicherweise besser, hat jedoch eine schlechte Cache-Lokalität. Die beste Darstellung für Partikel kann vom verwendeten Rahmen oder der verwendeten Sprache abhängen.
zusammenhängende Posts
  • Verwenden Sie Quadtrees, um mögliche Kollisionen im 2D-Raum zu erkennen