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.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.
Unser Partikelsystem wird einige einstellbare Parameter haben:
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:
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 = [];
Bei jedem Frame müssen wir drei Dinge tun: Neue Partikel erstellen, vorhandene Partikel verschieben und Partikel zeichnen.
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.
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; ;
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]); ;
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)..
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:
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:
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:
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!
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: