So erzeugen Sie schockierend gute 2D-Blitzeffekte

Lightning hat viele Einsatzmöglichkeiten in Spielen, von der Hintergrundatmosphäre während eines Sturms bis zu den verheerenden Blitzangriffen eines Zauberers. In diesem Lernprogramm erkläre ich, wie Sie programmgesteuerte 2D-Blitzeffekte programmieren: Bolzen, Zweige und sogar Text.

Hinweis: Obwohl dieses Tutorial mit C # und XNA geschrieben wurde, sollten Sie in der Lage sein, in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte anzuwenden.


Letzte Videovorschau


Schritt 1: Zeichnen Sie eine leuchtende Linie

Der Grundbaustein, den wir für den Blitz benötigen, ist ein Liniensegment. Öffnen Sie zunächst Ihre bevorzugte Bildbearbeitungssoftware und zeichnen Sie eine gerade Linie. So sieht meins aus:

Wir möchten Linien mit unterschiedlichen Längen zeichnen, so dass wir das Liniensegment in drei Teile schneiden, wie unten gezeigt. Dadurch können wir das mittlere Segment auf jede beliebige Länge strecken. Da wir das mittlere Segment strecken, können wir es als nur ein Pixel dick speichern. Da die linken und rechten Teile Spiegelbilder voneinander sind, müssen Sie nur eines davon speichern. Wir können es im Code umdrehen.

Lassen Sie uns nun eine neue Klasse für das Zeichnen von Liniensegmenten deklarieren:

public class Line public Vector2 A; public Vector2 B; öffentlicher Schwimmer Dicke; öffentliche Linie ()  öffentliche Linie (Vector2 a, Vector2 b, Floatdicke = 1) A = a; B = b; Dicke = Dicke; 

A und B sind die Endpunkte der Linie. Durch Skalieren und Drehen der Linienstücke können wir eine Linie mit beliebiger Dicke, Länge und Ausrichtung zeichnen. Fügen Sie folgendes hinzu Zeichnen() Methode zum Linie Klasse:

public void Draw (SpriteBatch SpriteBatch, Farbe Farbe) Vector2-Tangens = B - A; Float Rotation = (Float) Math.Atan2 (Tangente.Y, Tangente.X); const float ImageThickness = 8; AuftriebsdickeSkala = Dicke / Bilddicke; Vector2 capOrigin = new Vector2 (Art.HalfCircle.Width, Art.HalfCircle.Height / 2f); Vector2 middleOrigin = new Vector2 (0, Art.LightningSegment.Height / 2f); Vector2 middleScale = new Vector2 (tangent.Length (), thicknessScale); spriteBatch.Draw (Art.LightningSegment, A, Null, Farbe, Rotation, middleOrigin, middleScale, SpriteEffects.None, 0f); spriteBatch.Draw (Art.HalfCircle, A, null, Farbe, Rotation, capOrigin, thicknessScale, SpriteEffects.none, 0f); spriteBatch.Draw (Art.HalfCircle, B, Null, Farbe, Rotation + MathHelper.Pi, capOrigin, thicknessScale, SpriteEffects.none, 0f); 

Hier, Art.LightningSegment und Art.Halbkreis sind statisch Textur2D Variablen, die die Bilder der Teile des Liniensegments enthalten. ImageThickness wird auf die Stärke der Linie ohne Glühen eingestellt. In meinem Bild sind es 8 Pixel. Wir setzen den Ursprung der Kappe auf die rechte Seite und den Ursprung des mittleren Segments auf die linke Seite. Dadurch werden sie nahtlos zusammengefügt, wenn wir beide an Punkt A zeichnen. Das mittlere Segment wird auf die gewünschte Breite gestreckt und eine weitere Kappe wird an Punkt B gezeichnet, die um 180 ° gedreht ist.

XNA's SpriteBatch Klasse erlaubt es Ihnen, es zu übergeben a SpriteSortMode in seinem Konstruktor, der die Reihenfolge angibt, in der die Sprites gezeichnet werden sollen. Wenn Sie die Linie zeichnen, stellen Sie sicher, dass Sie sie übergeben SpriteBatch mit SpriteSortMode einstellen SpriteSortMode.Texture. Dies dient zur Verbesserung der Leistung.

Grafikkarten zeichnen viele Male die gleiche Textur. Jedes Mal, wenn sie Texturen wechseln, gibt es jedoch Overhead. Wenn wir mehrere Zeilen zeichnen, ohne zu sortieren, zeichnen wir unsere Texturen in dieser Reihenfolge:

LightningSegment, HalfCircle, HalfCircle, LightningSegment, HalfCircle, HalfCircle,…

Das bedeutet, dass wir für jede gezeichnete Linie zweimal Texturen wechseln würden. SpriteSortMode.Texture erzählt SpriteBatch die sortieren Zeichnen() ruft per textur auf damit alle Blitzsegmente wird zusammengezogen und alle Halbkreise wird zusammengezogen. Wenn wir diese Linien für die Herstellung von Blitzbolzen verwenden, möchten wir zusätzlich die additive Mischung verwenden, um das Licht aus überlappenden Blitzstücken zusammenzufügen.

SpriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); // Linien zeichnen SpriteBatch.End ();

Schritt 2: Gezackte Linien

Der Blitz neigt dazu, gezackte Linien zu bilden, daher benötigen wir einen Algorithmus, um diese zu erzeugen. Wir tun dies, indem Sie zufällig Punkte entlang einer Linie auswählen und sie um eine zufällige Entfernung von der Linie verschieben. Die Verwendung einer völlig zufälligen Verschiebung neigt dazu, die Linie zu zackig zu machen. Daher glätten wir die Ergebnisse, indem wir begrenzen, wie weit benachbarte Punkte voneinander verschoben werden können.

Die Linie wird geglättet, indem Punkte mit einem ähnlichen Abstand zum vorherigen Punkt platziert werden. Auf diese Weise kann die Linie als Ganzes auf und ab wandern und gleichzeitig verhindern, dass ein Teil der Linie zu zackig wird. Hier ist der Code:

geschützte statische Liste CreateBolt (Vector2-Quelle, Vector2-Ziel, Floatdicke) var results = neue Liste(); Vector2 Tangente = Ziel - Quelle; Vector2 normal = Vector2.Normalize (neuer Vector2 (tangent.Y, -tangent.X)); Floatlänge = Tangens.Length (); Liste Positionen = neue Liste(); Positionen.Add (0); für (int i = 0; i < length / 4; i++) positions.Add(Rand(0, 1)); positions.Sort(); const float Sway = 80; const float Jaggedness = 1 / Sway; Vector2 prevPoint = source; float prevDisplacement = 0; for (int i = 1; i < positions.Count; i++)  float pos = positions[i]; // used to prevent sharp angles by ensuring very close positions also have small perpendicular variation. float scale = (length * Jaggedness) * (pos - positions[i - 1]); // defines an envelope. Points near the middle of the bolt can be further from the central line. float envelope = pos > 0,95 f? 20 * (1-pos): 1; Verschiebung des Schwimmers = Rand (-Sway, Sway); Verschiebung - = (Verschiebung - Vorversetzung) * (1 - Skala); Verschiebung * = Umschlag; Vektor2-Punkt = Quelle + Position * Tangente + Verschiebung * Normal; results.Add (neue Linie (prevPoint, Punkt, Dicke)); prevPoint = Punkt; prevDisplacement = Verschiebung;  results.Add (neue Linie (prevPoint, dest, thickness)); Ergebnisse zurückgeben; 

Der Code sieht ein wenig einschüchternd aus, ist aber nicht so schlimm, wenn Sie die Logik verstanden haben. Wir beginnen mit der Berechnung der Normal- und Tangentenvektoren der Linie zusammen mit der Länge. Dann wählen wir zufällig eine Reihe von Positionen entlang der Linie aus und speichern sie in unserer Positionsliste. Die Positionen werden zwischen skaliert 0 und 1 so dass 0 steht für den Zeilenanfang und 1 repräsentiert den Endpunkt. Diese Positionen werden dann sortiert, damit wir problemlos Liniensegmente zwischen ihnen hinzufügen können.

Die Schleife durchläuft die zufällig ausgewählten Punkte und verschiebt sie entlang des Normalen um einen zufälligen Betrag. Der Skalierungsfaktor dient dazu, zu scharfe Winkel zu vermeiden, und die Hüllkurve sorgt dafür, dass der Blitz tatsächlich zum Zielpunkt gelangt, indem die Verschiebung begrenzt wird, wenn wir uns dem Ende nähern.


Schritt 3: Animation

Der Blitz sollte hell blinken und dann ausgeblendet werden. Um dies zu bewältigen, erstellen wir eine Blitz Klasse.

Klasse LightningBolt öffentliche Liste Segmente = neue Liste(); öffentlicher Float Alpha get; einstellen;  public float FadeOutRate get; einstellen;  public Farbton get; einstellen;  public bool IsComplete get return Alpha <= 0;   public LightningBolt(Vector2 source, Vector2 dest) : this(source, dest, new Color(0.9f, 0.8f, 1f))   public LightningBolt(Vector2 source, Vector2 dest, Color color)  Segments = CreateBolt(source, dest, 2); Tint = color; Alpha = 1f; FadeOutRate = 0.03f;  public void Draw(SpriteBatch spriteBatch)  if (Alpha <= 0) return; foreach (var segment in Segments) segment.Draw(spriteBatch, Tint * (Alpha * 0.6f));  public virtual void Update()  Alpha -= FadeOutRate;  protected static List CreateBolt (Vector2-Quelle, Vector2-Ziel, Floatdicke) //… //…

Um dies zu verwenden, erstellen Sie einfach ein neues Blitz und Ruf an Aktualisieren() und Zeichnen() jeder Frame. Berufung Aktualisieren() lässt es verblassen. Ist komplett wird Ihnen sagen, wenn der Bolzen vollständig ausgeblendet ist.

Sie können jetzt Ihre Schrauben zeichnen, indem Sie den folgenden Code in Ihrer Game-Klasse verwenden:

LightningBolt-Schraube; MouseState mouseState, lastMouseState; geschütztes überschreiben void Update (GameTime gameTime) lastMouseState = mouseState; mouseState = Mouse.GetState (); var screenSize = new Vector2 (GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height); var mousePosition = new Vector2 (mouseState.X, mouseState.Y); if (MouseWasClicked ()) bolt = new LightningBolt (screenSize / 2, mousePosition); if (bolt! = null) bolt.Update ();  private bool MouseWasClicked () return mouseState.LeftButton == ButtonState.Pressed && lastMouseState.LeftButton == ButtonState.Released;  protected override void Draw (GameTime gameTime) GraphicsDevice.Clear (Color.Black); spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); if (bolt! = null) bolt.Draw (spriteBatch); spriteBatch.End (); 

Schritt 4: Zweigblitz

Du kannst den ... benutzen Blitz Klasse als Baustein, um interessantere Blitzeffekte zu erzeugen. Zum Beispiel können Sie die Bolzen wie folgt verzweigen lassen:

Um den Blitzzweig zu erstellen, wählen wir zufällige Punkte entlang des Blitzes aus und fügen neue Schrauben hinzu, die von diesen Punkten abzweigen. In dem folgenden Code erstellen wir drei bis sechs Äste, die sich in einem Winkel von 30 ° vom Hauptbolzen trennen.

Klasse BranchLightning Liste Schrauben = neue Liste(); public bool IsComplete get return bolt.Count == 0;  public Vector2 End get; privates Set;  private Vector2-Richtung; statisch Random rand = new Random (); public BranchLightning (Vector2-Start, Vector2-Ende) End = Ende; Richtung = Vector2.Normalize (Ende - Start); Erstellen (Anfang, Ende);  public void Update () bolt = bolt.Where (x =>! x.IsComplete) .ToList (); foreach (var Bolzen in Bolzen) Bolzen.Update ();  public void Draw (SpriteBatch SpriteBatch) foreach (Var-Bolzen in Schrauben) bolt.Draw (SpriteBatch);  private void Create (Vector2-Start, Vector2-Ende) var mainBolt = new LightningBolt (Anfang, Ende); Bolzen.Add (mainBolt); int numBranches = rand.Next (3, 6); Vector2 diff = Ende - Start; // Wähle eine Reihe von zufälligen Punkten zwischen 0 und 1 und sortiere sie float [] branchPoints = Enumerable.Range (0, numBranches) .Select (x => Rand (0, 1f)) .OrderBy (x => x). ToArray (); für (int i = 0; i < branchPoints.Length; i++)  // Bolt.GetPoint() gets the position of the lightning bolt at specified fraction (0 = start of bolt, 1 = end) Vector2 boltStart = mainBolt.GetPoint(branchPoints[i]); // rotate 30 degrees. Alternate between rotating left and right. Quaternion rot = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathHelper.ToRadians(30 * ((i & 1) == 0 ? 1 : -1))); Vector2 boltEnd = Vector2.Transform(diff * (1 - branchPoints[i]), rot) + boltStart; bolts.Add(new LightningBolt(boltStart, boltEnd));   static float Rand(float min, float max)  return (float)rand.NextDouble() * (max - min) + min;  

Schritt 5: Blitztext

Unten sehen Sie ein Video eines anderen Effekts, den Sie aus den Blitzbolzen machen können:

Zuerst müssen wir die Pixel in dem Text erhalten, den wir zeichnen möchten. Wir machen das, indem wir unseren Text zu a zeichnen RenderTarget2D und Zurücklesen der Pixeldaten mit RenderTarget2D.GetData(). Wenn Sie mehr über Textpartikeleffekte erfahren möchten, habe ich hier ein ausführlicheres Tutorial.

Die Koordinaten der Pixel speichern wir im Text als Liste. Dann wählen wir in jedem Frame zufällig Paare dieser Punkte aus und erstellen einen Blitz zwischen ihnen. Wir möchten es so gestalten, dass je näher zwei Punkte aneinander liegen, desto größer ist die Chance, dass wir einen Bolzen zwischen ihnen schaffen. Es gibt eine einfache Technik, mit der wir das erreichen können: Wir wählen den ersten Punkt nach dem Zufallsprinzip aus, dann wählen wir nach dem Zufallsprinzip eine feste Anzahl anderer Punkte aus und wählen den nächsten Punkt aus.

Die Anzahl der getesteten Kandidatenpunkte beeinflusst das Aussehen des Blitztextes. Wenn Sie eine größere Anzahl von Punkten überprüfen, können Sie sehr nahe Punkte finden, zwischen denen Schrauben gezogen werden. Dies macht den Text sehr ordentlich und lesbar, jedoch mit weniger langen Blitzen zwischen den Buchstaben. Kleinere Zahlen lassen den Blitz verrückter, aber weniger lesbar erscheinen.

public void Update () foreach (var-Partikel in TextPartikeln) float x = Partikel.X / 500f; if (rand.Next (50) == 0) Vector2 nextParticle = Vector2.Zero; float nextDist = float.MaxValue; für (int i = 0; i < 50; i++)  var other = textParticles[rand.Next(textParticles.Count)]; var dist = Vector2.DistanceSquared(particle, other); if (dist < nearestDist && dist > 10 * 10) nextDist = dist; nextParticle = Sonstiges;  if (nextDist < 200 * 200 && nearestDist > 10 * 10) Schrauben.Add (neuer LightningBolt (Partikel, nächstgelegener Artikel, Farbe.Weiß));  für (int i = Schrauben.Zahl - 1; i> = 0; i--) Schrauben [i] .Update (); if (Bolzen [i] .IsComplete) Bolzen.RemoveAt (i); 

Schritt 6: Optimierung

Der Lightning-Text kann, wie oben gezeigt, problemlos laufen, wenn Sie einen Computer der Spitzenklasse haben, aber er ist sehr anstrengend. Jeder Bolzen hält über 30 Rahmen, und wir erstellen Dutzende neuer Bolzen für jeden Rahmen. Da jeder Blitz ein paar hundert Liniensegmente haben kann und jedes Liniensegment aus drei Teilen besteht, zeichnen wir am Ende eine Menge Sprites. In meiner Demo werden zum Beispiel über 25.000 Bilder pro Bild mit deaktivierten Optimierungen gezeichnet. Wir können es besser machen.

Anstatt jeden Bolzen bis zum Ausblenden zu zeichnen, können wir jeden neuen Bolzen zu einem Renderziel ziehen und das Renderziel für jeden Frame ausblenden. Das heißt, anstatt jeden Bolzen für 30 oder mehr Rahmen zu zeichnen, zeichnen wir ihn nur einmal. Es bedeutet auch, dass keine zusätzlichen Leistungskosten entstehen, wenn unsere Blitze langsamer ausgeblendet werden und länger halten.

Zuerst ändern wir die LightningText Klasse, um nur jeden Bolzen für einen Rahmen zu zeichnen. In deiner Spiel Klasse, erkläre zwei RenderTarget2D Variablen: currentFrame und lastFrame. Im LoadContent (), initialisiere sie wie folgt:

lastFrame = new RenderTarget2D (GraphicsDevice, screenSize.X, screenSize.Y, false, SurfaceFormat.HdrBlendable, DepthFormat.None); currentFrame = new RenderTarget2D (GraphicsDevice, screenSize.X, screenSize.Y, false, SurfaceFormat.HdrBlendable, DepthFormat.None);

Beachten Sie, dass das Oberflächenformat auf festgelegt ist HdrBlendable. HDR steht für High Dynamic Range und bedeutet, dass unsere HDR-Oberfläche eine größere Farbpalette darstellen kann. Dies ist erforderlich, da das Renderziel Farben haben kann, die heller als Weiß sind. Wenn sich mehrere Blitzschrauben überlappen, benötigen wir ein Renderziel, um die gesamte Summe ihrer Farben zu speichern, die sich über den Standardfarbbereich hinaus summieren kann. Während diese helleren Farben immer noch weiß auf dem Bildschirm angezeigt werden, ist es wichtig, die volle Helligkeit zu speichern, damit sie korrekt ausgeblendet werden.

XNA-Tipp: Beachten Sie außerdem, dass Sie für das HDR-Blending das XNA-Projektprofil auf Hi-Def setzen müssen. Sie können dies tun, indem Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt klicken, Eigenschaften auswählen und dann das Hi-Def-Profil auf der Registerkarte XNA Game Studio auswählen.

Bei jedem Frame zeichnen wir zunächst den Inhalt des letzten Frames in das aktuelle Frame, jedoch etwas dunkler. Wir fügen dann alle neu erstellten Schrauben zum aktuellen Rahmen hinzu. Schließlich rendern wir unseren aktuellen Frame auf dem Bildschirm und tauschen dann die beiden Renderziele für den nächsten Frame aus, lastFrame bezieht sich auf den gerade gerenderten Rahmen.

void DrawLightningText () GraphicsDevice.SetRenderTarget (currentFrame); GraphicsDevice.Clear (Color.Black); // Zeichne das letzte Bild bei 96% Helligkeit spriteBatch.Begin (0, BlendState.Opaque, SamplerState.PointClamp, null, null); spriteBatch.Draw (lastFrame, Vector2.Zero, Color.White * 0.96f); spriteBatch.End (); // neue Schrauben mit additiver Mischung zeichnen spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); lightningText.Draw (); spriteBatch.End (); // Zeichne das Ganze in den Backbuffer GraphicsDevice.SetRenderTarget (null); spriteBatch.Begin (0, BlendState.Opaque, SamplerState.PointClamp, null, null); spriteBatch.Draw (currentFrame, Vector2.Zero, Color.White); spriteBatch.End (); Swap (ref currentFrame, ref lastFrame);  ungültigen Swap(ref T a, ref T b) T temp = a; a = b; b = Temp; 

Schritt 7: Andere Variationen

Wir haben über das Erstellen von Zweigblitzen und Blitztext gesprochen, aber das sind sicherlich nicht die einzigen Effekte, die Sie erzielen können. Schauen wir uns ein paar andere Variationen des Blitzes an, die Sie verwenden können.

Blitz bewegen

Oft möchten Sie einen beweglichen Blitzschlag machen. Sie können dies tun, indem Sie am Endpunkt der Schraube des vorherigen Rahmens einen neuen kurzen Bolzen hinzufügen.

Vector2 lightningEnd = new Vector2 (100, 100); Vector2 lightningVelocity = new Vector2 (50, 0); void Update (GameTime gameTime) Bolts.Add (neuer LightningBolt (LightningEnd, LightningEnd + LightningVelocity)); LightningEnd + = LightningVelocity; //…

Glatter Blitz

Sie haben vielleicht bemerkt, dass der Blitz an den Gelenken heller leuchtet. Dies ist auf die additive Mischung zurückzuführen. Möglicherweise möchten Sie einen glatteren, gleichmäßigeren Blick für Ihren Blitz. Dies können Sie erreichen, indem Sie die Füllstandsfunktion so ändern, dass der Maximalwert der Quell- und Zielfarben ausgewählt wird (siehe unten).

private static readonly BlendState maxBlend = new BlendState () AlphaBlendFunction = BlendFunction.Max, ColorBlendFunction = BlendFunction.Max, AlphaDestinationBlend = Blend.One, AlphaSourceBlend = Blend.One, ColorDestinationBlend = Blend.One, Colour.Nach.

Dann in deiner Zeichnen() Funktion, Anruf SpriteBatch.Begin () mit maxBlend als die BlendState anstatt BlendState.Additive. Die Abbildungen unten zeigen den Unterschied zwischen additiver Mischung und maximaler Mischung auf einem Blitz.


Bei max Blending lässt sich natürlich nicht das Licht mehrerer Bolzen oder des Hintergrunds gut addieren. Wenn Sie möchten, dass der Bolzen selbst glatt aussieht, aber auch mit anderen Bolzen additiv gemischt wird, können Sie den Bolzen zuerst mit maximaler Überblendung auf ein Renderziel rendern und dann das Renderziel mit additiver Überblendung auf den Bildschirm ziehen. Verwenden Sie nicht zu viele große Renderziele, da dies die Leistung beeinträchtigen kann.

Eine andere Alternative, die für eine große Anzahl von Schrauben besser geeignet ist, besteht darin, den in die Liniensegmentbilder eingebauten Glühen zu beseitigen und ihn unter Verwendung eines Nachbearbeitungseffekts wieder hinzuzufügen. Die Details zum Verwenden von Shadern und zum Erstellen von Leuchteffekten gehen über den Rahmen dieses Lernprogramms hinaus. Sie können jedoch zunächst das XNA Bloom Sample verwenden. Diese Technik erfordert keine weiteren Renderziele, wenn Sie weitere Schrauben hinzufügen.


Fazit

Lightning ist ein großartiger Spezialeffekt, um deine Spiele zu beschleunigen. Die in diesem Tutorial beschriebenen Effekte sind ein guter Ausgangspunkt, aber mit Blitz können Sie sicherlich nicht alles tun. Mit ein bisschen Fantasie können Sie alle Arten von beeindruckenden Blitzeffekten erzeugen! Laden Sie den Quellcode herunter und experimentieren Sie mit Ihrem eigenen.

Wenn Ihnen dieser Artikel gefallen hat, sehen Sie sich auch mein Tutorial zu 2D-Wassereffekten an.