Erstellung von einem Neon-Vektor-Shooter in XNA: Partikeleffekte
German (Deutsch) translation by Alex Grigorovich (you can also view the original English article)
In dieser Reihe von Tutorials zeige ich Ihnen, wie Sie in XNA einen Neon-Twin-Stick-Shooter wie Geometry Wars erstellen. Das Ziel dieser Tutorials ist es nicht, Ihnen eine exakte Nachbildung von Geometry Wars zu hinterlassen, sondern die erforderlichen Elemente zu erläutern, mit denen Sie Ihre eigene hochwertige Variante erstellen können.
Überblick
In der bisherigen Serie haben wir das Gameplay eingerichtet und Bloom hinzugefügt. Dann werden wir Partikeleffekte hinzufügen.
Partikeleffekte entstehen durch die Herstellung einer großen Anzahl kleiner Partikel. Sie sind sehr vielseitig und können verwendet werden, um fast jedem Spiel Flair zu verleihen. In Shape Blaster werden wir Explosionen mit Partikeleffekten machen. Wir werden auch Partikeleffekte verwenden, um Abgasfeuer für das Schiff des Spielers zu erzeugen und den Schwarzen Löchern visuelles Flair zu verleihen. Außerdem schauen wir uns an, wie Partikel mit der Schwerkraft der Schwarzen Löcher interagieren.
Die ParticleManager
klasse
Zuerst erstellen wir eine ParticleManager
-Klasse, in der alle Partikel gespeichert, aktualisiert und gezeichnet werden. Wir werden diese Klasse so allgemein gestalten, dass sie problemlos in anderen Projekten wiederverwendet werden kann. Um den ParticleManager
allgemein zu halten, ist er nicht dafür verantwortlich, wie die Partikel aussehen oder sich bewegen. Wir werden das woanders erledigen.
Partikel neigen dazu, schnell und in großer Zahl erzeugt und zerstört zu werden. Wir werden einen Objektpool verwenden, um zu vermeiden, dass große Mengen Müll entstehen. Dies bedeutet, dass wir eine große Anzahl von Partikeln im Voraus zuweisen und diese Partikel dann weiterhin wiederverwenden. Wir werden auch dafür sorgen, dass der ParticleManager
eine feste Kapazität hat. Dies vereinfacht dies und stellt sicher, dass wir unsere Leistungs- oder Speicherbeschränkungen nicht überschreiten, indem wir zu viele Partikel erzeugen. Wenn die maximale Anzahl von Partikeln überschritten wird, werden die ältesten Partikel durch neue ersetzt.
Wir machen den ParticleManager
zu einer generischen Klasse. Auf diese Weise können wir benutzerdefinierte Statusinformationen für die Partikel speichern, ohne sie fest im ParticleManager
selbst zu codieren. Wir werden auch eine verschachtelte Particle
klasse erstellen.
public class ParticleManager<T> { public class Particle { public Texture2D Texture; public Vector2 Position; public float Orientation; public Vector2 Scale = Vector2.One; public Color Color; public float Duration; public float PercentLife = 1f; public T State; } }
Die Particle
klasse verfügt über alle Informationen, die zum Anzeigen eines Partikels und zum Verwalten seiner Lebensdauer erforderlich sind. Der generische Parameter T State
enthält zusätzliche Daten, die wir möglicherweise für unsere Partikel benötigen. Welche Daten benötigt werden, hängt von den gewünschten Partikeleffekten ab. Es kann verwendet werden, um Geschwindigkeit, Beschleunigung, Rotationsgeschwindigkeit oder alles andere zu speichern, was Sie benötigen.
Um die Partikel besser verwalten zu können, benötigen wir eine Klasse, die als kreisförmiges Array fungiert. Dies bedeutet, dass Indizes, die normalerweise außerhalb der Grenzen liegen, stattdessen am Anfang des Arrays umbrochen werden. Dies macht es einfach, die ältesten Partikel zuerst zu ersetzen, wenn wir keinen Platz mehr für neue Partikel in unserem Array haben. Wir fügen Folgendes als verschachtelte Klasse in ParticleManager
hinzu.
private class CircularParticleArray { private int start; public int Start { get { return start; } set { start = value % list.Length; } } public int Count { get; set; } public int Capacity { get { return list.Length; } } private Particle[] list; public CircularParticleArray(int capacity) { list = new Particle[capacity]; } public Particle this[int i] { get { return list[(start + i) % list.Length]; } set { list[(start + i) % list.Length] = value; } } }
Wir können die Start
-Eigenschaft so einstellen, dass angepasst wird, wo der Index Null in unserem CircularParticleArray
im zugrunde liegenden Array entspricht, und Count
wird verwendet, um zu verfolgen, wie viele aktive Partikel in der Liste enthalten sind. Wir werden sicherstellen, dass das Partikel am Index Null immer das älteste Partikel ist. Wenn wir das älteste Teilchen durch ein neues ersetzen, erhöhen wir einfach Start
, wodurch das kreisförmige Array im Wesentlichen gedreht wird.
Nachdem wir unsere Hilfsklassen haben, können wir die ParticleManager
-Klasse ausfüllen. Wir benötigen einige Mitgliedsvariablen und einen Konstruktor.
// This delegate will be called for each particle. private Action<Particle> updateParticle; private CircularParticleArray particleList; public ParticleManager(int capacity, Action<Particle> updateParticle) { this.updateParticle = updateParticle; particleList = new CircularParticleArray(capacity); // Populate the list with empty particle objects, for reuse. for (int i = 0; i < capacity; i++) particleList[i] = new Particle(); }
Die erste deklarierte Variable, updateParticle
, ist eine benutzerdefinierte Methode, mit der die Partikel entsprechend dem gewünschten Effekt aktualisiert werden. Ein Spiel kann mehrere ParticleManagers
haben, die bei Bedarf unterschiedlich aktualisiert werden. Wir erstellen auch eine CircularParticleList
und füllen sie mit leeren Partikeln. Der Konstruktor ist der einzige Ort, an dem der ParticleManager
Speicher zuweist.
Dann fügen wir die CreateParticle()
-Methode hinzu, mit der ein neues Partikel mit dem nächsten nicht verwendeten Partikel im Pool oder dem ältesten Partikel erstellt wird, wenn keine nicht verwendeten Partikel vorhanden sind.
public void CreateParticle(Texture2D texture, Vector2 position, Color tint, float duration, Vector2 scale, T state, float theta = 0) { Particle particle; if (particleList.Count == particleList.Capacity) { // if the list is full, overwrite the oldest particle, and rotate the circular list particle = particleList[0]; particleList.Start++; } else { particle = particleList[particleList.Count]; particleList.Count++; } // Create the particle particle.Texture = texture; particle.Position = position; particle.Tint = tint; particle.Duration = duration; particle.PercentLife = 1f; particle.Scale = scale; particle.Orientation = theta; particle.State = state; }
Partikel können jederzeit zerstört werden. Wir müssen diese Partikel entfernen und gleichzeitig sicherstellen, dass die anderen Partikel in derselben Reihenfolge bleiben. Wir können dies tun, indem wir die Liste der Partikel durchlaufen und gleichzeitig verfolgen, wie viele zerstört wurden. Während wir gehen, bewegen wir jedes aktive Teilchen vor alle zerstörten Teilchen, indem wir es gegen das erste zerstörte Teilchen austauschen. Sobald alle zerstörten Partikel am Ende der Liste sind, deaktivieren wir sie, indem wir die Variable Count
der Liste auf die Anzahl der aktiven Partikel setzen. Zerstörte Partikel verbleiben im zugrunde liegenden Array, werden jedoch nicht aktualisiert oder gezeichnet.
ParticleManager.Update()
aktualisiert jedes Partikel und entfernt zerstörte Partikel aus der Liste.
public void Update() { int removalCount = 0; for (int i = 0; i < particleList.Count; i++) { var particle = particleList[i]; updateParticle(particle); particle.PercentLife -= 1f / particle.Duration; // sift deleted particles to the end of the list Swap(particleList, i - removalCount, i); // if the particle has expired, delete this particle if (particle.PercentLife < 0) removalCount++; } particleList.Count -= removalCount; } private static void Swap(CircularParticleArray list, int index1, int index2) { var temp = list[index1]; list[index1] = list[index2]; list[index2] = temp; }
Das letzte, was in ParticleManager
implementiert werden muss, ist das Zeichnen der Partikel.
public void Draw(SpriteBatch spriteBatch) { for (int i = 0; i < particleList.Count; i++) { var particle = particleList[i]; Vector2 origin = new Vector2(particle.Texture.Width / 2, particle.Texture.Height / 2); spriteBatch.Draw(particle.Texture, particle.Position, null, particle.Color, particle.Orientation, origin, particle.Scale, 0, 0); } }
Die ParticleState
struktur
Dann erstellen Sie eine benutzerdefinierte Klasse oder Struktur, um das Aussehen der Partikel in Shape Blaster anzupassen. In Shape Blaster gibt es verschiedene Arten von Partikeln, die sich geringfügig unterscheiden. Daher erstellen wir zuerst eine enum
für den Partikeltyp. Wir benötigen auch Variablen für die Geschwindigkeit und die Anfangslänge des Partikels.
public enum ParticleType { None, Enemy, Bullet, IgnoreGravity } public struct ParticleState { public Vector2 Velocity; public ParticleType Type; public float LengthMultiplier; }
Jetzt können wir die Aktualisierungsmethode des Partikels schreiben. Es ist eine gute Idee, diese Methode schnell zu machen, da sie möglicherweise für eine große Anzahl von Partikeln aufgerufen werden muss.
Wir fangen einfach an. Fügen Sie die folgende Methode zu ParticleState
hinzu.
public static void UpdateParticle(ParticleManager.Particle particle) { var vel = particle.State.Velocity; particle.Position += vel; particle.Orientation = vel.ToAngle(); // denormalized floats cause significant performance issues if (Math.Abs(vel.X) + Math.Abs(vel.Y) < 0.00000000001f) vel = Vector2.Zero; vel *= 0.97f; // particles gradually slow down x.State.Velocity = vel; }
Feindliche Explosionen
Wir werden gleich zurückkommen und diese Methode verbessern. Lassen Sie uns zuerst einige Partikeleffekte erstellen, damit wir unsere Änderungen tatsächlich testen können. Deklarieren Sie in GameRoot
einen neuen ParticleManager
und rufen Sie dessen Methoden Update()
und Draw()
auf.
// in GameRoot public static ParticleManager ParticleManager { get; private set; } // in GameRoot.Initialize() ParticleManager = new ParticleManager(1024 * 20, ParticleState.UpdateParticle); // in GameRoot.Update() ParticleManager.Update(); // in GameRoot.Draw() spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive); ParticleManager.Draw(); spriteBatch.End();
Deklarieren Sie außerdem eine neue Texture2D
mit dem Namen LineParticle
für die Textur des Partikels in der Art
-Klasse und laden Sie die Textur wie für die anderen Sprites.
Lassen wir jetzt Feinde explodieren. Ändern Sie die Enemy.WasShot()
-Methode.
public void WasShot() { IsExpired = true; for (int i = 0; i < 120; i++) { float speed = 18f * (1f - 1 / rand.NextFloat(1f, 10f)); var state = new ParticleState() { Velocity = rand.NextVector2(speed, speed), Type = ParticleType.Enemy, LengthMultiplier = 1f }; GameRoot.ParticleManager.CreateParticle(Art.LineParticle, Position, Color.LightGreen, 190, 1.5f, state); } }
Dadurch entstehen 120 Partikel, die mit unterschiedlichen Geschwindigkeiten in alle Richtungen nach außen schießen. Die zufällige Geschwindigkeit wird so gewichtet, dass sich Partikel eher in der Nähe der Höchstgeschwindigkeit bewegen. Dies führt dazu, dass sich mehr Partikel am Rand der Explosion befinden, wenn sie sich ausdehnt. Die Partikel halten 190 Frames oder etwas mehr als drei Sekunden.
Sie können das Spiel jetzt ausführen und beobachten, wie Feinde explodieren. Es müssen jedoch noch einige Verbesserungen für die Partikeleffekte vorgenommen werden.
Das erste Problem ist, dass die Partikel nach Ablauf ihrer Dauer abrupt verschwinden. Es wäre schöner, wenn sie reibungslos ausblenden könnten. Aber gehen wir noch ein bisschen weiter und lassen die Partikel heller leuchten, wenn sie sich schnell bewegen. Es sieht auch gut aus, wenn wir sich schnell bewegende Partikel verlängern und sich langsam bewegende Teilchen verkürzen.
Ändern Sie die Methode ParticleState.UpdateParticle()
(Änderungen werden hervorgehoben).
public static void UpdateParticle(ParticleManager.Particle particle) { var vel = particle.State.Velocity; particle.Position += vel; particle.Orientation = vel.ToAngle(); float speed = vel.Length(); float alpha = Math.Min(1, Math.Min(particle.PercentLife * 2, speed * 1f)); alpha *= alpha; particle.Color.A = (byte)(255 * alpha); particle.Scale.X = particle.State.LengthMultiplier * Math.Min(Math.Min(1f, 0.2f * speed + 0.1f), alpha); if (Math.Abs(vel.X) + Math.Abs(vel.Y) < 0.00000000001f) // denormalized floats cause significant performance issues vel = Vector2.Zero; vel *= 0.97f; // particles gradually slow down x.State.Velocity = vel; }
Die Explosionen sehen jetzt viel besser aus, aber sie haben alle die gleiche Farbe. Wir können ihnen mehr Abwechslung geben, indem wir zufällige Farben wählen. Eine Methode zur Erzeugung zufälliger Farben besteht darin, die roten, blauen und grünen Komponenten zufällig auszuwählen. Dies führt jedoch zu vielen stumpfen Farben, und wir möchten, dass unsere Partikel ein neonlichtes Aussehen haben. Wir können mehr Kontrolle über unsere Farben haben, indem wir sie im HSV-Farbraum angeben. HSV steht für Farbton, Sättigung und Wert. Wir möchten Farben mit einem zufälligen Farbton, aber einer festen Sättigung und einem festen Wert auswählen. Wir brauchen eine Hilfsfunktion, die aus HSV-Werten eine Farbe erzeugen kann.
static class ColorUtil { public static Color HSVToColor(float h, float s, float v) { if (h == 0 && s == 0) return new Color(v, v, v); float c = s * v; float x = c * (1 - Math.Abs(h % 2 - 1)); float m = v - c; if (h < 1) return new Color(c + m, x + m, m); else if (h < 2) return new Color(x + m, c + m, m); else if (h < 3) return new Color(m, c + m, x + m); else if (h < 4) return new Color(m, x + m, c + m); else if (h < 5) return new Color(x + m, m, c + m); else return new Color(c + m, m, x + m); } }
Jetzt können wir Enemy.WasShot()
so ändern, dass zufällige Farben verwendet werden. Um die Explosionsfarbe weniger eintönig zu machen, wählen wir für jede Explosion zwei nahegelegene Schlüsselfarben aus und interpolieren sie für jedes Partikel linear um einen zufälligen Betrag.
public void WasShot() { IsExpired = true; float hue1 = rand.NextFloat(0, 6); float hue2 = (hue1 + rand.NextFloat(0, 2)) % 6f; Color color1 = ColorUtil.HSVToColor(hue1, 0.5f, 1); Color color2 = ColorUtil.HSVToColor(hue2, 0.5f, 1); for (int i = 0; i < 120; i++) { float speed = 18f * (1f - 1 / rand.NextFloat(1f, 10f)); var state = new ParticleState() { Velocity = rand.NextVector2(speed, speed), Type = ParticleType.Enemy, LengthMultiplier = 1 }; Color color = Color.Lerp(color1, color2, rand.NextFloat(0, 1)); GameRoot.ParticleManager.CreateParticle(Art.LineParticle, Position, color, 190, 1.5f, state); } }
Die Explosionen sollten wie in der folgenden Animation aussehen.

Sie können mit der Farbgenerierung nach Ihren Wünschen herumspielen. Eine alternative Technik, die gut funktioniert, besteht darin, eine Reihe von Farbmustern für Explosionen von Hand auszuwählen und zufällig aus Ihren vorgewählten Farbschemata auszuwählen.
Kugelexplosionen
Wir können die Kugeln auch explodieren lassen, wenn sie den Bildschirmrand erreichen. Wir werden im Wesentlichen dasselbe tun, was wir für feindliche Explosionen getan haben.
Fügen Sie der Bullet
-Klasse ein statisches Random
Mitglied hinzu.
private static Random rand = new Random();
Ändern Sie dann Bullet.Update()
wie im Beispiel.
// delete bullets that go off-screen if (!GameRoot.Viewport.Bounds.Contains(Position.ToPoint())) { IsExpired = true; for (int i = 0; i < 30; i++) GameRoot.ParticleManager.CreateParticle(Art.LineParticle, Position, Color.LightBlue, 50, 1, new ParticleState() { Velocity = rand.NextVector2(0, 9), Type = ParticleType.Bullet, LengthMultiplier = 1 }); }
Möglicherweise stellen Sie fest, dass es verschwenderisch ist, den Partikeln eine zufällige Richtung zu geben, da mindestens die Hälfte der Partikel sofort vom Bildschirm abweicht (mehr, wenn die Kugel in einer Ecke explodiert). Wir könnten zusätzliche Arbeit leisten, um sicherzustellen, dass Partikel nur Geschwindigkeiten erhalten, die der Wand gegenüberliegen, der sie zugewandt sind. Stattdessen nehmen wir ein Stichwort aus Geometry Wars und lassen alle Partikel von den Wänden abprallen. Alle Partikel, die sich außerhalb des Bildschirms befinden, werden zurückgeworfen.
Fügen Sie die folgenden Zeilen zu ParticleState.UpdateParticle()
zwischen der ersten und der letzten Zeile hinzu.
var pos = x.Position; int width = (int)GameRoot.ScreenSize.X; int height = (int)GameRoot.ScreenSize.Y; // collide with the edges of the screen if (pos.X < 0) vel.X = Math.Abs(vel.X); else if (pos.X > width) vel.X = -Math.Abs(vel.X); if (pos.Y < 0) vel.Y = Math.Abs(vel.Y); else if (pos.Y > height) vel.Y = -Math.Abs(vel.Y);
Schiffsexplosion des Spielers
Wir werden eine wirklich große Explosion machen, wenn der Spieler getötet wird. Ändern Sie PlayerShip.Kill()
wie hier:
public void Kill() { framesUntilRespawn = 60; Color yellow = new Color(0.8f, 0.8f, 0.4f); for (int i = 0; i < 1200; i++) { float speed = 18f * (1f - 1 / rand.NextFloat(1f, 10f)); Color color = Color.Lerp(Color.White, yellow, rand.NextFloat(0, 1)); var state = new ParticleState() { Velocity = rand.NextVector2(speed, speed), Type = ParticleType.None, LengthMultiplier = 1 }; GameRoot.ParticleManager.CreateParticle(Art.LineParticle, Position, color, 190, 1.5f, state); } }
Dies ähnelt den feindlichen Explosionen, aber wir verwenden mehr Partikel und verwenden immer das gleiche Farbschema. Der Partikeltyp ist ebenfalls auf ParticleType.None
festgelegt.
In der Demo verlangsamen sich Partikel von feindlichen Explosionen schneller als Partikel vom explodierenden Schiff des Spielers. Dadurch hält die Explosion des Spielers etwas länger an und sieht etwas epischer aus.
Schwarze Löcher erneut besucht
Nachdem wir Partikeleffekte haben, lassen Sie uns die Schwarzen Löcher erneut betrachten und sie mit Partikeln interagieren lassen.
Wirkung auf Partikel
Schwarze Löcher sollten neben anderen Entitäten auch Partikel betreffen. Daher müssen wir ParticleState.UpdateParticle()
ändern. Fügen Sie die folgenden Zeilen hinzu.
if (x.State.Type != ParticleType.IgnoreGravity) { foreach (var blackHole in EntityManager.BlackHoles) { var dPos = blackHole.Position - pos; float distance = dPos.Length(); var n = dPos / distance; vel += 10000 * n / (distance * distance + 10000); // add tangential acceleration for nearby particles if (distance < 400) vel += 45 * new Vector2(n.Y, -n.X) / (distance + 100); } }
Hier ist n
der Einheitsvektor, der auf das Schwarze Loch zeigt. Die Anziehungskraft ist eine modifizierte Version der inversen Quadratfunktion. Die erste Modifikation ist, dass der Nenner ist \(distance^ 2 + 10.000\). Dies bewirkt, dass sich die Anziehungskraft einem Maximalwert nähert, anstatt gegen unendlich zu tendieren, wenn der Abstand sehr klein wird. Wenn der Abstand viel größer als 100 Pixel ist, wird \(distance^2\) viel größer als 10.000. Daher hat das Hinzufügen von 10.000 zu \(distance^2\) einen sehr geringen Effekt, und die Funktion nähert sich einer normalen inversen Quadratfunktion an. Wenn der Abstand jedoch viel kleiner als 100 Pixel ist, hat der Abstand einen geringen Einfluss auf den Wert des Nenners, und die Gleichung wird ungefähr gleich:
vel += n;
Die zweite Modifikation ist das Hinzufügen einer Seitwärtskomponente zur Geschwindigkeit, wenn die Partikel nahe genug an das Schwarze Loch heranreichen. Dies dient zwei Zwecken. Erstens drehen sich die Partikel im Uhrzeigersinn in Richtung des Schwarzen Lochs. Zweitens, wenn die Partikel nahe genug kommen, erreichen sie ein Gleichgewicht und bilden einen leuchtenden Kreis um das Schwarze Loch.
(V.Y, -V.X)
. Um 90° gegen den Uhrzeigersinn zu drehen, nehmen Sie (-V.Y, V.X)
.Partikel produzieren
Schwarze Löcher produzieren zwei Arten von Partikeln. Erstens sprühen sie regelmäßig Partikel aus, die um sie herum kreisen. Zweitens, wenn ein Schwarzes Loch geschossen wird, sprüht es spezielle Partikel aus, die nicht von seiner Schwerkraft beeinflusst werden.
Fügen Sie der BlackHole.WasShot()
-Methode den folgenden Code hinzu.
float hue = (float)((3 * GameRoot.GameTime.TotalGameTime.TotalSeconds) % 6); Color color = ColorUtil.HSVToColor(hue, 0.25f, 1); const int numParticles = 150; float startOffset = rand.NextFloat(0, MathHelper.TwoPi / numParticles); for (int i = 0; i < numParticles; i++) { Vector2 sprayVel = MathUtil.FromPolar(MathHelper.TwoPi * i / numParticles + startOffset, rand.NextFloat(8, 16)); Vector2 pos = Position + 2f * sprayVel; var state = new ParticleState() { Velocity = sprayVel, LengthMultiplier = 1, Type = ParticleType.IgnoreGravity }; GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, color, 90, 1.5f, state); }
Dies funktioniert meistens genauso wie die anderen Partikelexplosionen. Ein Unterschied besteht darin, dass wir den Farbton basierend auf der insgesamt verstrichenen Spielzeit auswählen. Wenn Sie mehrmals hintereinander auf das Schwarze Loch schießen, wird sich der Farbton der Explosionen allmählich drehen. Dies sieht weniger chaotisch aus als die Verwendung zufälliger Farben, während dennoch Variationen möglich sind.
Für das umlaufende Partikelspray müssen wir der BlackHole
-Klasse eine Variable hinzufügen, um die Richtung zu verfolgen, in die wir derzeit Partikel sprühen.
private float sprayAngle = 0;
Fügen Sie nun der BlackHole.Update()
-Methode Folgendes hinzu.
// The black holes spray some orbiting particles. The spray toggles on and off every quarter second. if ((GameRoot.GameTime.TotalGameTime.Milliseconds / 250) % 2 == 0) { Vector2 sprayVel = MathUtil.FromPolar(sprayAngle, rand.NextFloat(12, 15)); Color color = ColorUtil.HSVToColor(5, 0.5f, 0.8f); // light purple Vector2 pos = Position + 2f * new Vector2(sprayVel.Y, -sprayVel.X) + rand.NextVector2(4, 8); var state = new ParticleState() { Velocity = sprayVel, LengthMultiplier = 1, Type = ParticleType.Enemy }; GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, color, 190, 1.5f, state); } // rotate the spray direction sprayAngle -= MathHelper.TwoPi / 50f;
Dies führt dazu, dass die Schwarzen Löcher Spritzer von lila Partikeln sprühen, die einen kühlen, leuchtenden Ring bilden, der das Schwarze Loch umkreist, wie unten:
Schiffsabgasfeuer
Gemäß den Gesetzen der geometrischen Neonphysik treibt sich das Schiff des Spielers an, indem es einen Strom feuriger Partikel aus seinem Auspuffrohr spritzt. Mit unserer Partikel-Engine ist dieser Effekt einfach zu erzielen und verleiht der Schiffsbewegung ein visuelles Flair.
Während sich das Schiff bewegt, erzeugen wir drei Partikelströme: einen Mittelstrom, der direkt aus dem Schiffsrücken abfeuert, und zwei Seitenströme, deren Winkel sich relativ zum Schiff hin und her drehen. Die beiden Seitenströme schwenken in entgegengesetzte Richtungen, um ein sich kreuzendes Muster zu bilden. Die Seitenströme haben eine rötlichere Farbe, während der Mittelstrom eine heißere gelb-weiße Farbe hat. Die folgende Animation zeigt den Effekt.



Damit das Feuer heller leuchtet als allein durch die Blüte, wird das Schiff zusätzliche Partikel emittieren, die so aussehen:

Diese Partikel werden getönt und mit den regulären Partikeln gemischt. Der Code für den gesamten Effekt ist unten dargestellt.
private void MakeExhaustFire() { if (Velocity.LengthSquared() > 0.1f) { // set up some variables Orientation = Velocity.ToAngle(); Quaternion rot = Quaternion.CreateFromYawPitchRoll(0f, 0f, Orientation); double t = GameRoot.GameTime.TotalGameTime.TotalSeconds; // The primary velocity of the particles is 3 pixels/frame in the direction opposite to which the ship is travelling. Vector2 baseVel = Velocity.ScaleTo(-3); // Calculate the sideways velocity for the two side streams. The direction is perpendicular to the ship's velocity and the // magnitude varies sinusoidally. Vector2 perpVel = new Vector2(baseVel.Y, -baseVel.X) * (0.6f * (float)Math.Sin(t * 10)); Color sideColor = new Color(200, 38, 9); // deep red Color midColor = new Color(255, 187, 30); // orange-yellow Vector2 pos = Position + Vector2.Transform(new Vector2(-25, 0), rot); // position of the ship's exhaust pipe. const float alpha = 0.7f; // middle particle stream Vector2 velMid = baseVel + rand.NextVector2(0, 1); GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, Color.White * alpha, 60f, new Vector2(0.5f, 1), new ParticleState(velMid, ParticleType.Enemy)); GameRoot.ParticleManager.CreateParticle(Art.Glow, pos, midColor * alpha, 60f, new Vector2(0.5f, 1), new ParticleState(velMid, ParticleType.Enemy)); // side particle streams Vector2 vel1 = baseVel + perpVel + rand.NextVector2(0, 0.3f); Vector2 vel2 = baseVel - perpVel + rand.NextVector2(0, 0.3f); GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, Color.White * alpha, 60f, new Vector2(0.5f, 1), new ParticleState(vel1, ParticleType.Enemy)); GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, Color.White * alpha, 60f, new Vector2(0.5f, 1), new ParticleState(vel2, ParticleType.Enemy)); GameRoot.ParticleManager.CreateParticle(Art.Glow, pos, sideColor * alpha, 60f, new Vector2(0.5f, 1), new ParticleState(vel1, ParticleType.Enemy)); GameRoot.ParticleManager.CreateParticle(Art.Glow, pos, sideColor * alpha, 60f, new Vector2(0.5f, 1), new ParticleState(vel2, ParticleType.Enemy)); } }
In diesem Code ist nichts hinterhältiges los. Wir verwenden eine Sinusfunktion, um den Schwenkeffekt in den Seitenströmen zu erzeugen, indem wir ihre Seitengeschwindigkeit über die Zeit variieren. Für jeden Stream erstellen wir zwei überlappende Partikel pro Frame: ein halbtransparent-weißes LineParticle
und ein farbiges Glow-Partikel dahinter. Rufen Sie MakeExhaustFire()
am Ende von PlayerShip.Update()
auf, bevor Sie die Schiffsgeschwindigkeit auf Null setzen.
Abschluss
Mit all diesen Partikeleffekten sieht Shape Blaster ziemlich cool aus. Im letzten Teil dieser Serie werden wir einen weiteren großartigen Effekt hinzufügen: das Warping-Hintergrundgitter.