7 days of WordPress themes, graphics & videos - for free!* Unlimited asset downloads! Start 7-Day Free Trial
Advertisement
  1. Game Development
  2. Programming

Erstellen eines Neon-Vektor-Shooters in XNA: Bloom und Schwarze Löcher

Read Time: 15 mins
This post is part of a series called Cross-Platform Vector Shooter: XNA.
Make a Neon Vector Shooter in XNA: More Gameplay
Make a Neon Vector Shooter in XNA: Particle Effects

German (Deutsch) translation by Katharina Grigorovich-Nevolina (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 grundlegende Gameplay für unseren Neon-Twin-Stick-Shooter Shape Blaster eingerichtet. In diesem Tutorial erstellen wir den charakteristischen Neon-Look, indem wir einen Bloom-Nachbearbeitungsfilter hinzufügen.

Einfache Effekte wie dieser oder Partikeleffekte können ein Spiel erheblich attraktiver machen, ohne dass Änderungen am Gameplay erforderlich sind. Die effektive Nutzung visueller Effekte ist ein wichtiger Aspekt in jedem Spiel. Nach dem Hinzufügen des Bloom-Filters fügen wir dem Spiel auch schwarze Löcher hinzu.


Bloom-Nachbearbeitungseffekt

Bloom beschreibt den Effekt, den Sie sehen, wenn Sie ein Objekt mit einem hellen Licht dahinter betrachten und das Licht über das Objekt zu bluten scheint. In Shape Blaster lässt der Bloom-Effekt die hellen Linien der Schiffe und Partikel wie helle, leuchtende Neonlichter aussehen.

Sunlight blooming through the treesSunlight blooming through the treesSunlight blooming through the trees
Sonnenlicht blüht durch die Bäume

Um Bloom in unserem Spiel anzuwenden, müssen wir unsere Szene auf ein Renderziel rendern und dann unseren Bloom-Filter auf dieses Renderziel anwenden.

Bloom arbeitet in drei Schritten:

  1. Extrahieren Sie die hellen Teile des Bildes.
  2. Verwischen Sie die hellen Teile.
  3. Kombinieren Sie das unscharfe Bild mit dem Originalbild, während Sie einige Helligkeits- und Sättigungsanpassungen vornehmen.

Für jeden dieser Schritte ist ein Shader erforderlich - im Wesentlichen ein kurzes Programm, das auf Ihrer Grafikkarte ausgeführt wird. Shader in XNA sind in einer speziellen Sprache namens High-Level Shader Language(HLSL) geschrieben. Die folgenden Beispielbilder zeigen das Ergebnis jedes Schritts.

Initial imageInitial imageInitial image
Erstes Bild
The bright areas extracted from the imageThe bright areas extracted from the imageThe bright areas extracted from the image
Die aus dem Bild extrahierten hellen Bereiche
The bright areas after blurringThe bright areas after blurringThe bright areas after blurring
Die hellen Bereiche nach dem Verwischen
The final result after recombining with the original imageThe final result after recombining with the original imageThe final result after recombining with the original image
Das Endergebnis nach der Rekombination mit dem Originalbild

Hinzufügen von Bloom zu Shape Blaster

Für unseren Bloom-Filter verwenden wir das XNA Bloom Postprocess Sample.

Die Integration des Blütenmusters in unser Projekt ist einfach. Suchen Sie zunächst die beiden Codedateien aus dem Beispiel BloomComponent.cs und BloomSettings.cs und fügen Sie sie dem ShapeBlaster-Projekt hinzu. Fügen Sie außerdem BloomCombine.fx, BloomExtract.fx und GaussianBlur.fx zum Content-Pipeline-Projekt hinzu.

Fügen Sie in GameRoot eine using-Anweisung für den BloomPostprocess-Namespace hinzu und fügen Sie eine BloomComponent-Mitgliedsvariable hinzu.

Fügen Sie im GameRoot-Konstruktor die folgenden Zeilen hinzu.

Fügen Sie zum Schluss ganz am Anfang von GameRoot.Draw() die folgende Zeile hinzu.

Das ist es. Wenn Sie das Spiel jetzt ausführen, sollten Sie die Blüte in Wirkung sehen.

Wenn Sie bloom.BeginDraw() aufrufen, werden nachfolgende Zeichenaufrufe an ein Renderziel umgeleitet, auf das bloom angewendet wird. Wenn Sie base.Draw() am Ende der GameRoot.Draw()-Methode aufrufen, wird die Draw()-Methode der BloomComponent aufgerufen. Hier wird die Blüte angewendet und die Szene in den hinteren Puffer gezeichnet. Daher muss zwischen den Aufrufen von bloom.BeginDraw() und base.Draw() alles gezeichnet werden, was bloom angewendet werden muss.

Tipp: Wenn Sie etwas ohne Blüte zeichnen möchten (z. B. die Benutzeroberfläche), zeichnen Sie es nach dem Aufruf von base.Draw().

Sie können die Bloom-Einstellungen nach Ihren Wünschen anpassen. Ich habe folgende Werte gewählt:

  • 0.25 für die Bloom-Schwelle. Dies bedeutet, dass Teile des Bildes, die weniger als ein Viertel der vollen Helligkeit aufweisen, nicht zur Blüte beitragen.
  • 4 für die Unschärfemenge. Für die mathematisch geneigten Personen ist dies die Standardabweichung der Gaußschen Unschärfe. Größere Werte verwischen die Lichtblüte mehr. Beachten Sie jedoch, dass der Unschärfeshader unabhängig von der Unschärfemenge so eingestellt ist, dass eine feste Anzahl von Samples verwendet wird. Wenn Sie diesen Wert zu hoch einstellen, wird die Unschärfe über den Radius hinausgehen, aus dem die Shader-Samples stammen, und Artefakte werden angezeigt. Idealerweise sollte dieser Wert nicht mehr als ein Drittel Ihres Abtastradius betragen, um sicherzustellen, dass der Fehler vernachlässigbar ist.
  • 2 für die Bloom-Intensität, die bestimmt, wie stark die Blüte das Endergebnis beeinflusst.
  • 1 für die Basisintensität, die bestimmt, wie stark das Originalbild das Endergebnis beeinflusst.
  • 1.5 für die Blütensättigung. Dies führt dazu, dass das Leuchten um helle Objekte gesättigtere Farben aufweist als die Objekte selbst. Ein hoher Wert wurde gewählt, um das Aussehen von Neonlichtern zu simulieren. Wenn Sie in die Mitte eines hellen Neonlichts schauen, sieht es fast weiß aus, während das Leuchten um es herum stärker gefärbt ist.
  • 1 für die Basensättigung. Dieser Wert beeinflusst die Sättigung des Basisbildes.
Without bloomWithout bloomWithout bloom
Ohne Bloom
With bloomWith bloomWith bloom
Mit Bloom

Bloom unter der Haube

Der Bloom-Filter ist in der BloomComponent-Klasse implementiert. Die Bloom-Komponente erstellt und lädt zunächst die erforderlichen Ressourcen in ihrer LoadContent()-Methode. Hier werden die drei erforderlichen Shader geladen und drei Renderziele erstellt.

Das erste Renderziel, sceneRenderTarget, dient zum Halten der Szene, auf die die Blüte angewendet wird. Die anderen beiden, renderTarget1 und renderTarget2, werden verwendet, um die Zwischenergebnisse zwischen den einzelnen Rendering-Durchläufen vorübergehend zu speichern. Diese Renderziele werden zur Hälfte der Spielauflösung erstellt, um die Leistungskosten zu senken. Dies verringert nicht die endgültige Qualität der Blüte, da wir die Blütenbilder ohnehin unscharf machen.

Bloom erfordert vier Rendering-Durchgänge, wie in diesem Diagramm gezeigt:

Render target diagramRender target diagramRender target diagram

In XNA kapselt die Effect-Klasse einen Shader. Sie schreiben den Code für den Shader in eine separate Datei, die Sie der Inhaltspipeline hinzufügen. Dies sind die Dateien mit der Erweiterung .fx, die wir zuvor hinzugefügt haben. Sie laden den Shader in ein Effekt-Objekt, indem Sie die Content.Load <Effect>()-Methode in LoadContent() aufrufen. Der einfachste Weg, einen Shader in einem 2D-Spiel zu verwenden, besteht darin, das Effekt-Objekt als Parameter an SpriteBatch.Begin() zu übergeben.

Es gibt verschiedene Arten von Shadern, aber für den Bloom-Filter werden nur Pixel-Shader (manchmal auch Fragment-Shader genannt) verwendet. Ein Pixel-Shader ist ein kleines Programm, das für jedes gezeichnete Pixel einmal ausgeführt wird und die Farbe des Pixels bestimmt. Wir werden jeden der verwendeten Shader durchgehen.

Der BloomExtract Shader

Der BloomExtract-Shader ist der einfachste der drei Shader. Seine Aufgabe besteht darin, die Bereiche des Bildes zu extrahieren, die heller als ein bestimmter Schwellenwert sind, und dann die Farbwerte neu zu skalieren, um den gesamten Farbbereich zu nutzen. Alle Werte unterhalb des Schwellenwerts werden schwarz.

Der vollständige Shader-Code wird unten angezeigt.

Machen Sie sich keine Sorgen, wenn Sie mit HLSL nicht vertraut sind. Lassen Sie uns untersuchen, wie dies funktioniert.

In diesem ersten Teil wird ein Textur-Sampler namens TextureSampler deklariert. SpriteBatch bindet eine Textur an diesen Sampler, wenn er mit diesem Shader zeichnet. Die Angabe, an welches Register gebunden werden soll, ist optional. Wir verwenden den Sampler, um Pixel aus der gebundenen Textur nachzuschlagen.

BloomThreshold ist ein Parameter, den wir aus unserem C#-Code festlegen können.

Dies ist unsere Pixel-Shader-Funktionsdeklaration, die Texturkoordinaten als Eingabe verwendet und eine Farbe zurückgibt. Die Farbe wird als float4 zurückgegeben. Dies ist eine Sammlung von vier Floats, ähnlich wie bei einem Vector4 in XNA. Sie speichern die Rot-, Grün-, Blau- und Alphakomponenten der Farbe als Werte zwischen Null und Eins.

TEXCOORD0 und COLOR0 werden als Semantik bezeichnet und geben dem Compiler an, wie der texCoord-Parameter und der Rückgabewert verwendet werden. Für jede Pixelausgabe enthält texCoord die Koordinaten des entsprechenden Punkts in der Eingabetextur, wobei (0, 0) die obere linke Ecke und (1, 1) die untere rechte Ecke ist.

Hier wird die ganze eigentliche Arbeit erledigt. Es ruft die Pixelfarbe von der Textur ab, subtrahiert BloomThreshold von jeder Farbkomponente und skaliert sie dann wieder hoch, sodass der Maximalwert eins ist. Die Funktion saturate() klemmt dann die Farbkomponenten zwischen Null und Eins.

Möglicherweise stellen Sie fest, dass c und BloomThreshold nicht vom gleichen Typ sind, da c ein float4 und BloomThreshold ein float ist. Mit HLSL können Sie Operationen mit diesen verschiedenen Typen ausführen, indem Sie den float im Wesentlichen in einen float4 verwandeln, bei dem alle Komponenten gleich sind. (c - BloomThreshold) wird effektiv:

Der Rest des Shaders erstellt einfach eine Technik, die die Pixel-Shader-Funktion verwendet, die für das Shader-Modell 2.0 kompiliert wurde.

Der GaussianBlur Shader

Eine Gaußscher Weichzeichner verwischt ein Bild mit einer Gaußschen Funktion. Für jedes Pixel im Ausgabebild addieren wir die Pixel im Eingabebild, gewichtet mit ihrem Abstand vom Zielpixel. Nahe gelegene Pixel tragen stark zur endgültigen Farbe bei, während entfernte Pixel sehr wenig beitragen.

Da entfernte Pixel vernachlässigbare Beiträge leisten und die Suche nach Texturen kostspielig ist, werden Pixel nur innerhalb eines kurzen Radius abgetastet, anstatt die gesamte Textur abzutasten. Dieser Shader tastet Punkte innerhalb von 14 Pixeln des aktuellen Pixels ab.

Bei einer naiven Implementierung werden möglicherweise alle Punkte in einem Quadrat um das aktuelle Pixel abgetastet. Dies kann jedoch kostspielig sein. In unserem Beispiel müssten wir Punkte innerhalb eines 29 x 29-Quadrats abtasten (14 Punkte auf beiden Seiten des mittleren Pixels plus des mittleren Pixels). Das sind insgesamt 841 Samples für jedes Pixel in unserem Bild. Zum Glück gibt es eine schnellere Methode. Es stellt sich heraus, dass eine 2D-Gaußsche Unschärfe gleichbedeutend damit ist, das Bild zuerst horizontal und dann wieder vertikal zu verwischen. Für jede dieser eindimensionalen Unschärfen sind nur 29 Abtastwerte erforderlich, wodurch sich unsere Gesamtzahl auf 58 Abtastwerte pro Pixel verringert.

Ein weiterer Trick wird verwendet, um die Effizienz der Unschärfe weiter zu erhöhen. Wenn Sie die GPU anweisen, zwischen zwei Pixeln abzutasten, wird eine Mischung der beiden Pixel ohne zusätzliche Leistungskosten zurückgegeben. Da unsere Unschärfe ohnehin Pixel miteinander vermischt, können wir zwei Pixel gleichzeitig abtasten. Dies halbiert die Anzahl der benötigten Proben fast.

Unten finden Sie die relevanten Teile des GaussianBlur-Shaders.

Der Shader ist eigentlich ganz einfach; Es werden nur ein Array von Offsets und ein entsprechendes Array von Gewichten benötigt und die gewichtete Summe berechnet. Die gesamte komplexe Mathematik befindet sich tatsächlich im C#-Code, der die Offset- und Weight-Arrays auffüllt. Dies erfolgt in den Methoden SetBlurEffectParameters() und ComputeGaussian() der BloomComponent-Klasse. Bei der Durchführung des horizontalen Weiczeichner-Durchlaufs werden SampleOffsets nur mit horizontalen Offsets gefüllt (die y-Komponenten sind alle Null), und natürlich gilt das Gegenteil für den vertikalen Durchlauf.

Der BloomCombine Shader

Der BloomCombine-Shader erledigt einige Dinge gleichzeitig. Es kombiniert die Blütentextur mit der ursprünglichen Textur und passt gleichzeitig die Intensität und Sättigung jeder Textur an.

Der Shader deklariert zunächst zwei Textur-Sampler und vier Float-Parameter.

Zu beachten ist, dass SpriteBatch die Textur, die Sie beim Aufrufen von SpriteBatch.Draw() übergeben, automatisch an den ersten Sampler bindet, jedoch nichts automatisch an den zweiten Sampler bindet. Der zweite Sampler wird manuell in BloomComponent.Draw() mit der folgenden Zeile festgelegt.

Als nächstes haben wir eine Hilfsfunktion, die die Sättigung einer Farbe anpasst.

Diese Funktion nimmt eine Farbe und einen Sättigungswert an und gibt eine neue Farbe zurück. Bei einer Sättigung von 1 bleibt die Farbe unverändert. Wenn Sie 0 übergeben, wird Grau zurückgegeben, und wenn Sie Werte größer als 1 übergeben, wird eine Farbe mit erhöhter Sättigung zurückgegeben. Das Übergeben negativer Werte liegt tatsächlich außerhalb des Verwendungszwecks, invertiert jedoch die Farbe, wenn Sie dies tun.

Die Funktion ermittelt zunächst die Leuchtkraft der Farbe, indem eine gewichtete Summe verwendet wird, die auf der Empfindlichkeit unserer Augen gegenüber rotem, grünem und blauem Licht basiert. Es interpoliert dann linear zwischen Grau und der Originalfarbe um den angegebenen Sättigungsgrad. Diese Funktion wird von der Pixel-Shader-Funktion aufgerufen.

Auch dieser Shader ist ziemlich einfach. Wenn Sie sich fragen, warum das Basisbild in Bereichen mit heller Blüte abgedunkelt werden muss, denken Sie daran, dass das Hinzufügen von zwei Farben die Helligkeit erhöht und alle Farbkomponenten, die einen Wert größer als eins (volle Helligkeit) ergeben, auf eins gekürzt werden . Da das Bloom-Bild dem Basisbild ähnlich ist, würde dies dazu führen, dass ein Großteil des Bildes mit einer Helligkeit von über 50% maximal wird. Durch Abdunkeln des Basisbilds werden alle Farben wieder in den Farbbereich zurückgeführt, den wir ordnungsgemäß anzeigen können.


Schwarze Löcher

Einer der interessantesten Feinde in Geometry Wars ist das Schwarze Loch. Lassen Sie uns untersuchen, wie wir in Shape Blaster etwas Ähnliches machen können. Wir werden jetzt die Grundfunktionalität erstellen und den Feind im nächsten Tutorial erneut besuchen, um Partikeleffekte und Partikelinteraktionen hinzuzufügen.

A black hole with orbiting particles
Ein Schwarzes Loch mit umlaufenden Partikeln

Grundfunktionalität

Die schwarzen Löcher ziehen das Schiff des Spielers, in der Nähe befindliche Feinde und (nach dem nächsten Tutorial) Partikel ein, stoßen aber Kugeln ab.

Es gibt viele mögliche Funktionen, die wir zur Anziehung oder Abstoßung verwenden können. Am einfachsten ist es, eine konstante Kraft anzuwenden, damit das Schwarze Loch unabhängig von der Entfernung des Objekts mit der gleichen Stärke zieht. Eine andere Möglichkeit besteht darin, die Kraft linear von Null in einem maximalen Abstand auf die volle Stärke für Objekte direkt über dem Schwarzen Loch zu erhöhen.

Wenn wir die Schwerkraft realistischer modellieren möchten, können wir das umgekehrte Quadrat der Entfernung verwenden, was bedeutet, dass die Schwerkraft proportional zu \(1/ Entfernung ^2\) ist. Wir werden tatsächlich jede dieser drei Funktionen verwenden, um verschiedene Objekte zu handhaben. Die Kugeln werden mit einer konstanten Kraft abgestoßen, die Feinde und das Schiff des Spielers werden mit einer linearen Kraft angezogen und die Partikel verwenden eine umgekehrte quadratische Funktion.

Wir werden eine neue Klasse für Schwarze Löcher machen. Beginnen wir mit der Grundfunktionalität.

Die schwarzen Löcher brauchen zehn Schüsse, um zu töten. Wir passen die Skalierung des Sprites leicht an, damit es pulsiert. Wenn Sie entscheiden, dass die Zerstörung von Schwarzen Löchern auch Punkte bringen soll, müssen Sie ähnliche Anpassungen an der BlackHole-Klasse vornehmen wie an der feindlichen Klasse.

Als nächstes lassen wir die Schwarzen Löcher tatsächlich eine Kraft auf andere Entitäten ausüben. Wir benötigen eine kleine Hilfsmethode von unserem EntityManager.

Diese Methode könnte durch Verwendung eines komplizierteren räumlichen Partitionierungsschemas effizienter gestaltet werden, aber für die Anzahl der Entitäten, die wir haben werden, ist es in Ordnung, wie es ist. Jetzt können wir die Schwarzen Löcher dazu bringen, in ihrer Update()-Methode Kraft anzuwenden.

Schwarze Löcher wirken sich nur auf Objekte innerhalb eines ausgewählten Radius (250 Pixel) aus. Auf Kugeln innerhalb dieses Radius wird eine konstante Abstoßungskraft ausgeübt, während auf alles andere eine lineare Anziehungskraft ausgeübt wird.

Wir müssen dem EntityManager die Kollisionsbehandlung für Schwarze Löcher hinzufügen. Fügen Sie eine List<> für Schwarze Löcher hinzu, wie wir es für die anderen Entitätstypen getan haben, und fügen Sie den folgenden Code in EntityManager.HandleCollisions() hinzu.

Öffnen Sie abschließend die EnemySpawner-Klasse und lassen Sie sie einige schwarze Löcher erstellen. Ich habe die maximale Anzahl von Schwarzen Löchern auf zwei begrenzt und eine 1:600-Chance gegeben, dass ein Schwarzes Loch jeden Frame hervorbringt.


Schlussfolgerung

Wir haben Bloom mit verschiedenen Shadern und schwarzen Löchern mit verschiedenen Kraftformeln hinzugefügt. Shape Blaster fängt an, ziemlich gut auszusehen. Im nächsten Teil werden wir einige verrückte, übertriebene Partikeleffekte hinzufügen.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Scroll to top
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.