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

Die Erstellung von einem Neon-Vektor-Shooter in XNA: Mehr Gameplay

Read Time: 20 mins
This post is part of a series called Cross-Platform Vector Shooter: XNA.
Make a Neon Vector Shooter in XNA: Basic Gameplay
Make a Neon Vector Shooter in XNA: Bloom and Black Holes

German (Deutsch) translation by Federicco Ancie (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 notwendigen Elemente durchzugehen, die es Ihnen ermöglichen, Ihre eigene hochwertige Variante zu erstellen.


Überblick

In diesem Teil bauen wir auf dem vorherigen Tutorial auf, indem wir Feinde, Kollisionserkennung und Wertung hinzufügen.

Folgendes werden wir am Ende haben:

Wir werden die folgenden neuen Klassen hinzufügen, um dies zu handhaben:

  • Enemy
  • EnemySpawner: Verantwortlich für das Erstellen von Feinden und das schrittweise Erhöhen des Schwierigkeitsgrades des Spiels.
  • PlayerStatus: Verfolgt die Punktzahl, den Highscore und die Leben des Spielers.

Sie haben vielleicht bemerkt, dass es im Video zwei Arten von Feinden gibt, aber es gibt nur eine Enemy-Klasse. Wir könnten für jeden Feindtyp Unterklassen von Enemy ableiten. Ich ziehe es jedoch vor, tiefe Klassenhierarchien zu vermeiden, da sie einige Nachteile haben:

  • Sie fügen mehr Boilerplate-Code hinzu.
  • Sie können die Komplexität des Codes erhöhen und die Verständlichkeit erschweren. Zustand und Funktionalität eines Objekts werden über seine gesamte Vererbungskette verteilt.
  • Sie sind nicht sehr flexibel. Sie können keine Funktionalitätsteile zwischen verschiedenen Zweigen des Vererbungsbaums freigeben, wenn diese Funktionalität nicht in der Basisklasse enthalten ist. Betrachten Sie beispielsweise zwei Klassen, Mammal und Bird, die beide von Animal stammen. Die Bird-Klasse verfügt über eine Fly()-Methode. Dann entscheidest du dich, eine Bat-Klasse hinzuzufügen, die von Mammal abgeleitet ist und auch fliegen kann. Um diese Funktionalität nur mit Vererbung zu teilen, müssten Sie die Fly()-Methode in die Animal-Klasse verschieben, in die sie nicht gehört. Darüber hinaus können Sie keine Methoden aus abgeleiteten Klassen entfernen. Wenn Sie also eine Penguin-Klasse erstellen, die von Bird abgeleitet ist, hätte sie auch eine Fly()-Methode.

In diesem Tutorial werden wir die Komposition der Vererbung vorziehen, um die verschiedenen Arten von Feinden zu implementieren. Wir werden dies tun, indem wir verschiedene, wiederverwendbare Verhaltensweisen erstellen, die wir Feinden hinzufügen können. Wir können dann Verhaltensweisen leicht mischen und anpassen, wenn wir neue Arten von Feinden erschaffen. Wenn wir beispielsweise bereits ein FollowPlayer-Verhalten und ein DodgeBullet-Verhalten hatten, könnten wir einen neuen Feind erstellen, der beides tut, indem wir einfach beide Verhaltensweisen hinzufügen.


Feinde

Feinde haben einige zusätzliche Eigenschaften gegenüber Entitäten. Um dem Spieler etwas Zeit zum Reagieren zu geben, lassen wir Feinde allmählich einblenden, bevor sie aktiv und gefährlich werden.

Lassen Sie uns die grundlegende Struktur der Enemy-Klasse codieren.

Dieser Code lässt Feinde für 60 Frames einblenden und lässt ihre Geschwindigkeit funktionieren. Die Multiplikation der Geschwindigkeit mit 0,8 täuscht einen reibungsähnlichen Effekt vor. Wenn wir Feinde mit konstanter Geschwindigkeit beschleunigen lassen, bewirkt diese Reibung, dass sie sich reibungslos einer Höchstgeschwindigkeit nähern. Ich mag die Einfachheit und Glätte dieser Art von Reibung, aber Sie können je nach gewünschtem Effekt eine andere Formel verwenden.

Die Methode WasShot() wird aufgerufen, wenn der Feind angeschossen wird. Wir werden später in der Serie mehr hinzufügen.

Wir wollen, dass sich verschiedene Arten von Feinden unterschiedlich verhalten. Wir erreichen dies, indem wir Verhaltensweisen zuweisen. Ein Verhalten verwendet eine benutzerdefinierte Funktion, die jeden Frame ausführt, um den Feind zu kontrollieren. Wir implementieren das Verhalten mit einem iterator.

Iteratoren (auch Generatoren genannt) in C# sind spezielle Methoden, die zwischendurch anhalten und später dort wieder aufnehmen können, wo sie aufgehört haben. Sie können einen Iterator erstellen, indem Sie eine Methode mit dem Rückgabetyp IEnumerable<> erstellen und das yield-Schlüsselwort dort verwenden, wo es zurückgegeben und später fortgesetzt werden soll. Bei Iteratoren in C# müssen Sie etwas zurückgeben, wenn Sie nachgeben. Wir müssen nicht wirklich etwas zurückgeben, also liefern unsere Iteratoren einfach null.

Unser einfachstes Verhalten ist das unten gezeigte FollowPlayer()-Verhalten.

Dadurch beschleunigt der Feind einfach mit konstanter Geschwindigkeit auf den Spieler zu. Die Reibung, die wir zuvor hinzugefügt haben, stellt sicher, dass sie schließlich mit einer maximalen Geschwindigkeit (5 Pixel pro Bild, wenn die Beschleunigung 1 ist, da \(0,8 \mal 5 + 1 = 5\)) erreicht wird. In jedem Frame wird diese Methode ausgeführt, bis sie die Yield-Anweisung trifft, und wird dann dort fortgesetzt, wo sie im nächsten Frame aufgehört hat.

Sie fragen sich vielleicht, warum wir uns überhaupt mit Iteratoren beschäftigt haben, da wir die gleiche Aufgabe mit einem einfachen Delegaten leichter hätten erledigen können. Die Verwendung von Iteratoren zahlt sich bei komplexeren Methoden aus, die andernfalls erfordern würden, dass wir den Zustand in Membervariablen in der Klasse speichern.

Unten ist beispielsweise ein Verhalten, das einen Feind in einem quadratischen Muster bewegt:

Das Schöne daran ist, dass es uns nicht nur einige Instanzvariablen spart, sondern den Code auch sehr logisch strukturiert. Sie können sofort sehen, dass sich der Feind nach rechts bewegt, dann nach unten, dann nach links, dann nach oben und dann wiederholt. Würden Sie diese Methode stattdessen als Zustandsmaschine implementieren, wäre der Kontrollfluss weniger offensichtlich.

Fügen wir das Gerüst hinzu, das erforderlich ist, damit Verhaltensweisen funktionieren. Feinde müssen ihr Verhalten speichern, daher fügen wir der Klasse Enemy eine Variable hinzu.

Beachten Sie, dass ein Verhalten den Typ IEnumerator<int> hat, nicht IEnumerable<int>. Sie können sich IEnumerable als Vorlage für das Verhalten und IEnumerator als laufende Instanz vorstellen. Der IEnumerator merkt sich, wo wir uns im Verhalten befinden und wird dort weitermachen, wo er aufgehört hat, wenn Sie seine MoveNext()-Methode aufrufen. In jedem Frame gehen wir alle Verhaltensweisen des Feindes durch und rufen MoveNext() auf jedem von ihnen auf. Wenn MoveNext() false zurückgibt, bedeutet dies, dass das Verhalten abgeschlossen ist und wir es aus der Liste entfernen sollten.

Wir fügen der Enemy-Klasse die folgenden Methoden hinzu:

Und wir ändern die Update()-Methode, um ApplyBehaviours() aufzurufen:

Jetzt können wir eine statische Methode erstellen, um suchende Feinde zu erstellen. Alles, was wir tun müssen, ist das gewünschte Bild auszuwählen und das Verhalten von FollowPlayer() hinzuzufügen.

Um einen Feind zu erschaffen, der sich zufällig bewegt, lassen wir ihn eine Richtung wählen und nehmen dann kleine zufällige Anpassungen an dieser Richtung vor. Wenn wir jedoch bei jedem Frame die Richtung anpassen, wird die Bewegung unruhig, sodass wir die Richtung nur in regelmäßigen Abständen anpassen. Wenn der Feind in den Rand des Bildschirms rennt, lassen wir ihn eine neue zufällige Richtung wählen, die von der Wand weg zeigt.

Wir können jetzt eine Fabrikmethode erstellen, um wandernde Feinde zu erschaffen, ähnlich wie wir es für den Sucher getan haben:


Kollisionserkennung

Zur Kollisionserkennung modellieren wir das Schiff des Spielers, die Feinde und die Kugeln als Kreise. Die Erkennung kreisförmiger Kollisionen ist gut, weil sie einfach, schnell und nicht drehbar ist, wenn sich die Objekte drehen. Wie Sie sich erinnern, hat die Entity-Klasse einen Radius und eine Position (die Position bezieht sich auf den Mittelpunkt der Entity). Dies ist alles, was wir für die zirkuläre Kollisionserkennung benötigen.

Das Testen jeder Entität gegen alle anderen Entitäten, die potenziell kollidieren könnten, kann sehr langsam sein, wenn Sie über eine große Anzahl von Entitäten verfügen. Es gibt viele Techniken, die Sie verwenden können, um die Erkennung von Kollisionen mit breiter Phase zu beschleunigen, wie Quadtrees, Sweep and Prune und BSP-Bäume. Derzeit werden jedoch nur ein paar Dutzend Entitäten gleichzeitig auf dem Bildschirm angezeigt, sodass wir uns nicht um diese komplexeren Techniken kümmern müssen. Wir können sie jederzeit später hinzufügen, wenn wir sie brauchen.

In Shape Blaster kann nicht jede Entität mit jeder anderen Art von Entität kollidieren. Kugeln und das Schiff des Spielers können nur mit Feinden kollidieren. Feinde können auch mit anderen Feinden kollidieren - dies verhindert, dass sie sich überlappen.

Um mit diesen verschiedenen Arten von Kollisionen umzugehen, werden wir dem EntityManager zwei neue Listen hinzufügen, um Kugeln und Feinde im Auge zu behalten. Immer wenn wir dem EntityManager eine Entität hinzufügen, möchten wir sie der entsprechenden Liste hinzufügen, also erstellen wir dafür eine private AddEntity()-Methode. Wir werden auch sicherstellen, dass alle abgelaufenen Entitäten aus allen Listen jedes Frames entfernt werden.

Ersetzen Sie die Aufrufe von entity.Add() in EntityManager.Add() und EntityManager.Update() durch Aufrufe von AddEntity().

Fügen wir nun eine Methode hinzu, die bestimmt, ob zwei Entitäten kollidieren:

Um festzustellen, ob sich zwei Kreise überlappen, prüfen Sie einfach, ob der Abstand zwischen ihnen kleiner ist als die Summe ihrer Radien. Unsere Methode optimiert dies geringfügig, indem sie überprüft, ob das Quadrat der Entfernung kleiner ist als das Quadrat der Summe der Radien. Denken Sie daran, dass es etwas schneller ist, die Entfernung zum Quadrat zu berechnen als die tatsächliche Entfernung.

Je nachdem, welche zwei Objekte kollidieren, passieren verschiedene Dinge. Wenn zwei Feinde kollidieren, wollen wir, dass sie sich gegenseitig wegstoßen. Wenn eine Kugel einen Feind trifft, sollten sowohl die Kugel als auch der Feind zerstört werden. Wenn der Spieler einen Feind berührt, sollte der Spieler sterben und das Level sollte zurückgesetzt werden.

Wir werden der Enemy-Klasse eine HandleCollision()-Methode hinzufügen, um Kollisionen zwischen Feinden zu behandeln:

Diese Methode drückt den aktuellen Feind vom anderen Feind weg. Je näher sie sind, desto schwerer wird es geschoben, da die Größe von (d / d.LengthSquared()) über die Distanz nur eins beträgt.

Respawnen des Spielers

Als nächstes brauchen wir eine Methode, mit der das Schiff des Spielers getötet wird. In diesem Fall verschwindet das Schiff des Spielers für kurze Zeit, bevor es wieder erscheint.

Wir beginnen damit, dass wir zwei neue Mitglieder zu PlayerShip hinzufügen.

Fügen Sie ganz am Anfang von PlayerShip.Update() Folgendes hinzu:

Und wir überschreiben Draw() wie gezeigt:

Schließlich fügen wir PlayerShip eine Kill()-Methode hinzu.

Nachdem alle Teile vorhanden sind, fügen wir dem EntityManager eine Methode hinzu, die alle Entitäten durchläuft und auf Kollisionen prüft.

Rufen Sie diese Methode von Update() direkt nach dem Setzen von isUpdating auf true auf.


Feindlicher Spawner

Als letztes müssen Sie die Klasse EnemySpawner erstellen, die für die Erstellung von Feinden verantwortlich ist. Wir möchten, dass das Spiel einfach beginnt und schwieriger wird, damit der EnemySpawner im Laufe der Zeit immer schneller Feinde erzeugt. Wenn der Spieler stirbt, setzen wir den EnemySpawner auf seine anfängliche Schwierigkeit zurück.

In jedem Frame gibt es einen in inverseSpawnChance, mit dem jeder Feindtyp generiert werden kann. Die Chance, einen Feind zu spawnen, steigt allmählich, bis sie ein Maximum von eins zu zwanzig erreicht. Feinde werden immer mindestens 250 Pixel vom Spieler entfernt erstellt.

Seien Sie vorsichtig mit der while-Schleife in GetSpawnPosition(). Es funktioniert effizient, solange der Bereich, in dem Feinde spawnen können, größer ist als der Bereich, in dem sie nicht spawnen können. Wenn Sie den verbotenen Bereich jedoch zu groß machen, erhalten Sie eine Endlosschleife.

Rufen Sie EnemySpawner.Update() von GameRoot.Update() auf und rufen Sie EnemySpawner.Reset() auf, wenn der Spieler getötet wird.


Punktzahl und Leben

In Shape Blaster beginnen Sie mit vier Leben und erhalten alle 2000 Punkte ein zusätzliches Leben. Sie erhalten Punkte für die Zerstörung von Feinden, wobei verschiedene Arten von Feinden unterschiedliche Punkte wert sind. Jeder zerstörte Feind erhöht auch Ihren Punktemultiplikator um eins. Wenn Sie innerhalb kurzer Zeit keine Feinde töten, wird Ihr Multiplikator zurückgesetzt. Die Gesamtpunktzahl, die Sie von jedem zerstörten Feind erhalten, ist die Anzahl der Punkte, die der Feind wert ist, multipliziert mit Ihrem aktuellen Multiplikator. Wenn Sie alle Ihre Leben verlieren, ist das Spiel vorbei und Sie beginnen ein neues Spiel, wobei Ihre Punktzahl auf Null zurückgesetzt wird.

Um all dies zu handhaben, erstellen wir eine statische Klasse namens PlayerStatus.

Rufen Sie PlayerStatus.Update() von GameRoot.Update() auf, wenn das Spiel nicht angehalten ist.

Als nächstes möchten wir Ihre Punktzahl, Leben und Multiplikator auf dem Bildschirm anzeigen. Dazu müssen wir eine SpriteFont im Content-Projekt und eine entsprechende Variable in der Art-Klasse hinzufügen, die wir Font nennen. Laden Sie die Schriftart in Art.Load() wie bei den Texturen.

Hinweis: In den Shape Blaster-Quelldateien ist eine Schriftart namens Nova Square enthalten, die Sie verwenden können. Um die Schriftart zu verwenden, müssen Sie sie zuerst installieren und dann Visual Studio neu starten, falls es geöffnet war. Sie können dann den Schriftartnamen in der Sprite-Schriftartdatei in "Nova Square" ändern. Das Demoprojekt verwendet diese Schriftart standardmäßig nicht, da dies die Kompilierung des Projekts verhindert, wenn die Schriftart nicht installiert ist.

Ändern Sie das Ende von GameRoot.Draw(), an dem der Cursor wie unten gezeigt gezeichnet wird.

DrawRightAlignedString() ist eine Hilfsmethode zum Zeichnen von Text, der auf der rechten Seite des Bildschirms ausgerichtet ist. Fügen Sie es zu GameRoot hinzu, indem Sie den folgenden Code hinzufügen.

Jetzt sollten Ihr Leben, Ihre Punktzahl und Ihr Multiplikator auf dem Bildschirm angezeigt werden. Wir müssen diese Werte jedoch noch als Reaktion auf Spielereignisse ändern. Fügen Sie der Enemy-Klasse eine Eigenschaft namens PointValue hinzu.

Stellen Sie den Punktwert für verschiedene Feinde auf einen Wert ein, der Ihrer Meinung nach angemessen ist. Ich habe den wandernden Feinden einen Punkt wert gemacht und den suchenden Feinden zwei Punkte.

Fügen Sie als Nächstes die folgenden zwei Zeilen zu Enemy.WasShot() hinzu, um die Punktzahl und den Multiplikator des Spielers zu erhöhen:

Rufen Sie PlayerStatus.RemoveLife() in PlayerShip.Kill() auf. Wenn der Spieler alle seine Leben verliert, rufen Sie PlayerStatus.Reset() auf, um seinen Punktestand und seine Leben zu Beginn eines neuen Spiels zurückzusetzen.

Highscores

Lassen Sie uns die Möglichkeit für das Spiel hinzufügen, Ihre beste Punktzahl zu verfolgen. Wir möchten, dass diese Punktzahl über alle Spiele hinweg erhalten bleibt, also speichern wir sie in einer Datei. Wir halten es wirklich einfach und speichern den Highscore als einzelne Klartextnummer in einer Datei im aktuellen Arbeitsverzeichnis (dies ist das gleiche Verzeichnis, das die .exe-Datei des Spiels enthält).

Fügen Sie PlayerStatus die folgenden Methoden hinzu:

Die Methode LoadHighScore() überprüft zuerst, ob die Highscore-Datei vorhanden ist, und überprüft dann, ob sie eine gültige Ganzzahl enthält. Die zweite Überprüfung wird höchstwahrscheinlich nie fehlschlagen, es sei denn, der Benutzer ändert die Highscore-Datei manuell in etwas Ungültiges, aber es ist ratsam, vorsichtig zu sein.

Wir möchten den Highscore beim Start des Spiels laden und speichern, wenn der Spieler einen neuen Highscore erreicht. Dazu ändern wir den statischen Konstruktor und die Reset()-Methode in PlayerStatus. Wir fügen auch eine Hilfseigenschaft hinzu, IsGameOver, die wir gleich verwenden werden.

Das sorgt dafür, dass der Highscore verfolgt wird. Jetzt müssen wir es anzeigen. Fügen Sie GameRoot.Draw() im selben SpriteBatch-Block, in dem der andere Text gezeichnet wird, den folgenden Code hinzu:

Dadurch werden Ihre Punktzahl und Ihr Highscore nach dem Spiel zentriert auf dem Bildschirm angezeigt.

Als letzte Anpassung erhöhen wir die Zeit, bevor das Schiff im Spiel wieder erscheint, um dem Spieler Zeit zu geben, seine Punktzahl zu sehen. Modifiziere PlayerShip.Kill(), indem du die Respawn-Zeit auf 300 Frames (fünf Sekunden) setzt, wenn der Spieler kein Leben mehr hat.

Das Spiel ist jetzt spielbereit. Es sieht vielleicht nicht nach viel aus, aber es sind alle grundlegenden Mechaniken implementiert. In zukünftigen Tutorials werden wir einen Blütenfilter und Partikeleffekte hinzufügen, um es aufzupeppen. Aber jetzt fügen wir schnell etwas Sound und Musik hinzu, um es interessanter zu machen.


Ton und Musik

Das Abspielen von Sound und Musik ist in XNA einfach. Zuerst fügen wir der Content-Pipeline unsere Soundeffekte und Musik hinzu. Stellen Sie im Bereich Eigenschaften sicher, dass der Inhaltsprozessor für die Musik auf Song und für die Sounds auf Sound Effect eingestellt ist.

Als nächstes erstellen wir eine statische Hilfsklasse für die Sounds.

Da wir mehrere Variationen jedes Sounds haben, wählen die Eigenschaften Explosion, Shot und Spawn einen zufälligen Sound aus den Varianten aus.

Rufen Sie Sound.Load() in GameRoot.LoadContent() auf. Um die Musik abzuspielen, fügen Sie die folgenden zwei Zeilen am Ende von GameRoot.Initialize() hinzu.

Um Sounds in XNA abzuspielen, können Sie einfach die Play()-Methode für einen SoundEffect aufrufen. Diese Methode bietet auch eine Übersteuerung, mit der Sie Lautstärke, Tonhöhe und Panorama des Sounds anpassen können. Ein Trick, um unsere Sounds abwechslungsreicher zu gestalten, besteht darin, diese Mengen bei jedem Spiel anzupassen.

Um den Soundeffekt für das Schießen auszulösen, fügen Sie die folgende Zeile in PlayerShip.Update() innerhalb der if-Anweisung hinzu, in der die Aufzählungszeichen erstellt werden. Beachten Sie, dass wir die Tonhöhe zufällig bis zu einer Quinte einer Oktave nach oben oder unten verschieben, damit sich die Klänge weniger wiederholen.

Lösen Sie auf ähnliche Weise jedes Mal, wenn ein Feind zerstört wird, einen Explosionssoundeffekt aus, indem Sie Enemy.WasShot() Folgendes hinzufügen.

Sie haben jetzt Sound und Musik in Ihrem Spiel. Einfach, nicht wahr?


Abschluss

Damit sind die grundlegenden Spielmechaniken abgeschlossen. Im nächsten Tutorial fügen wir einen Bloom-Filter hinzu, um die Neonlichter zum Leuchten zu bringen.

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.