Erstellen Sie einen Neon-Vektor-Shooter in XNA Basic Gameplay

In dieser Serie von Tutorials zeige ich Ihnen, wie Sie einen Neon-Twin-Shooter wie Geometry Wars erstellen, den wir in XNA Shape Blaster nennen werden. Das Ziel dieser Tutorials ist es nicht, Ihnen eine exakte Nachbildung von Geometry Wars zu geben, sondern die notwendigen Elemente durchzugehen, mit denen Sie eine qualitativ hochwertige Variante erstellen können.

Ich ermutige Sie, den in diesen Tutorials angegebenen Code zu erweitern und mit ihm zu experimentieren. Wir werden diese Themen in der gesamten Serie behandeln:

  1. Richten Sie das grundlegende Gameplay ein, erstellen Sie das Schiff des Spielers und bearbeiten Sie Eingaben, Sound und Musik.
  2. Beenden Sie die Implementierung der Gameplay-Mechanik, indem Sie Gegner hinzufügen, die Kollisionserkennung durchführen und die Punktzahl und das Leben des Spielers verfolgen.
  3. Fügen Sie einen Bloom-Filter hinzu. Dies ist der Effekt, der den Grafiken ein Neonlicht verleiht.
  4. Fügen Sie verrückte, übertriebene Partikeleffekte hinzu.
  5. Fügen Sie das Warping-Hintergrundraster hinzu.

Folgendes haben wir am Ende der Serie:

Warnung: Laut!

Und das haben wir am Ende dieses ersten Teils:

Warnung: Laut!

Die Musik und Soundeffekte, die Sie in diesen Videos hören können, wurden von RetroModular erstellt, und Sie können lesen, wie er dies bei Audiotuts getan hat+.

Die Sprites stammen von Jacob Zinman-Jeanes, unserem ansässigen Designer Tuts +. Das gesamte Bildmaterial befindet sich in der Quelldatei zum Herunterladen.

Der Font ist Nova Square von Wojciech Kalinowski.

Lass uns anfangen.


Überblick

In diesem Tutorial erstellen wir einen Twin-Stick-Shooter. Der Spieler steuert das Schiff mit der Tastatur, der Tastatur und der Maus oder den beiden Daumen eines Gamepads. 

Wir verwenden dazu eine Reihe von Klassen:

  • Entität: Die Basisklasse für Feinde, Kugeln und das Schiff des Spielers. Entitäten können sich bewegen und gezeichnet werden.
  • Kugel und SpielerSchiff.
  • EntityManager: Verfolgt alle Entitäten im Spiel und führt eine Kollisionserkennung durch.
  • Eingang: Hilft bei der Verwaltung von Eingaben über Tastatur, Maus und Gamepad.
  • Kunst: Lädt und enthält Verweise auf die für das Spiel benötigten Texturen.
  • Klingen: Lädt und enthält Verweise auf die Sounds und Musik.
  • MathUtil und Erweiterungen: Enthält einige hilfreiche statische Methoden und Erweiterungsmethoden.
  • GameRoot: Steuert die Hauptschleife des Spiels. Dies ist das Game1 Die Klasse XNA wird automatisch generiert, umbenannt.

Der Code in diesem Lernprogramm soll einfach und verständlich sein. Es hat nicht jedes Feature oder eine komplizierte Architektur, die alle möglichen Anforderungen erfüllt. Vielmehr wird es nur das tun, was es tun muss. Wenn Sie es einfach halten, wird es Ihnen leichter fallen, die Konzepte zu verstehen und sie dann zu einem eigenen, einzigartigen Spiel zu modifizieren und zu erweitern.


Entitäten und das Schiff des Spielers

Erstellen Sie ein neues XNA-Projekt. Benennen Sie das um Game1 Klasse für etwas passender. ich nannte es GameRoot.

Beginnen wir mit der Erstellung einer Basisklasse für unsere Spieleinheiten.

 abstrakte Klasse Entity protected Texture2D image; // Der Farbton des Bildes. Dies wird uns auch erlauben, die Transparenz zu ändern. geschützte Farbe Farbe = Farbe.Weiß; öffentliche Vector2-Position, Geschwindigkeit; public float Orientierung; öffentlicher Schwimmer Radius = 20; // für die Erkennung zirkulärer Kollisionen verwendet public bool IsExpired; // true, wenn die Entität gelöscht wurde und gelöscht werden soll. public Vector2 Size get return image == null? Vector2.Zero: neuer Vector2 (image.Width, image.Height);  public abstract void Update (); public virtual void Zeichnen (SpriteBatch SpriteBatch) SpriteBatch.Draw (Bild, Position, Null, Farbe, Ausrichtung, Größe / 2f, 1f, 0, 0); 

Alle unsere Entitäten (Feinde, Kugeln und das Schiff des Spielers) haben einige grundlegende Eigenschaften wie ein Bild und eine Position. Ist abgelaufen wird verwendet, um anzuzeigen, dass die Entität zerstört wurde und aus allen Listen entfernt werden sollte, die einen Verweis darauf enthalten.

Als nächstes erstellen wir eine EntityManager um unsere Entitäten zu verfolgen und sie zu aktualisieren und zu zeichnen.

 statische Klasse EntityManager statische Liste Entitäten = neue Liste(); statisches bool isUpdating; statische Liste addedEntities = neue Liste(); public static int Count get return entity.Count;  public static void Add (Entitätsentität) if (! isUpdating) Entitäten. Hinzufügen (Entität); else addedEntities.Add (entity);  public static void Update () isUpdating = true; foreach (Entität in Entitäten) entity.Update (); isUpdating = false; foreach (Entität in hinzugefügten Entitäten) Entitäten. Hinzufügen (Entität); addedEntities.Clear (); // alle abgelaufenen Entitäten entfernen. Entities = Entities.Where (x =>! x.IsExpired) .ToList ();  public static void Draw (SpriteBatch spriteBatch) foreach (Entität in Entitäten) entity.Draw (spriteBatch); 

Denken Sie daran, wenn Sie eine Liste ändern, während Sie die Liste durchlaufen, wird eine Ausnahme angezeigt. Der obige Code sorgt dafür, dass alle beim Aktualisieren hinzugefügten Entitäten in einer separaten Liste in eine Warteschlange gestellt werden und nach dem Aktualisieren der vorhandenen Entitäten hinzugefügt werden.

Sie sichtbar machen

Wir müssen einige Texturen laden, wenn wir etwas zeichnen wollen. Wir erstellen eine statische Klasse, in der alle unsere Texturen referenziert werden.

 statische Klasse Art public static Texture2D Player get; privates Set;  public static Texture2D Seeker get; privates Set;  public static Texture2D Wanderer get; privates Set;  public static Texture2D Bullet get; privates Set;  public static Texture2D Pointer get; privates Set;  public static void Load (ContentManager-Inhalt) Player = Inhalt.Laden("Spieler"); Sucher = content.Load("Sucher"); Wanderer = content.Load("Wanderer"); Aufzählungszeichen = Inhalt.Laden("Kugel"); Zeiger = Inhalt.Laden("Zeiger"); 

Laden Sie die Kunst durch Aufrufen Art.Load (Inhalt) im GameRoot.LoadContent (). Außerdem müssen einige Klassen die Bildschirmabmessungen kennen. Fügen Sie daher die folgenden Eigenschaften hinzu GameRoot:

 öffentliche statische GameRoot-Instanz get; privates Set;  public static Viewport Viewport get return Instance.GraphicsDevice.Viewport;  public static Vector2 ScreenSize get return new Vector2 (Viewport.Width, Viewport.Height); 

Und in der GameRoot Konstruktor, füge hinzu:

 Instanz = das;

Jetzt fangen wir mit dem Schreiben an SpielerSchiff Klasse.

 class PlayerShip: Entität private statische PlayerShip-Instanz; public static PlayerShip-Instanz get if (Instanz == null) Instanz = neuer PlayerShip (); Instanz zurückgeben;  private PlayerShip () image = Art.Player; Position = GameRoot.ScreenSize / 2; Radius = 10;  public override void Update () // Schiffslogik geht hier hin

Wir machten SpielerSchiff Ein Singleton, stellen Sie das Bild ein und platzieren Sie es in der Mitte des Bildschirms.

Schließlich fügen wir das Spielerschiff zum hinzu EntityManager und aktualisieren und zeichnen. Fügen Sie den folgenden Code hinzu GameRoot:

 // in Initialize (), nach dem Aufruf von base.Initialize () EntityManager.Add (PlayerShip.Instance); // in Update () EntityManager.Update (); // in Draw () GraphicsDevice.Clear (Color.Black); spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (spriteBatch); spriteBatch.End ();

Wir zeichnen die Sprites mit additive Mischung, Das ist ein Teil dessen, was ihnen ihren Neon-Look verleiht. Wenn Sie das Spiel zu diesem Zeitpunkt ausführen, sollten Sie Ihr Schiff in der Mitte des Bildschirms sehen. Es reagiert jedoch noch nicht auf Eingaben. Lass uns das reparieren.


Eingang

Für die Bewegung kann der Spieler WASD auf der Tastatur oder den linken Daumen auf einem Gamepad verwenden. Zum Zielen können sie die Pfeiltasten, den rechten Daumen oder die Maus verwenden. Es ist nicht erforderlich, dass der Spieler zum Schießen die Maustaste gedrückt hält, da es unangenehm ist, die Taste ununterbrochen zu drücken. Dies lässt uns ein kleines Problem: Wie wissen wir, ob der Spieler mit der Maus, der Tastatur oder dem Gamepad zielt??

Wir verwenden folgendes System: Wir fügen Tastatur- und Gamepad-Eingaben zusammen hinzu. Wenn der Spieler die Maus bewegt, wechseln wir zur Zielmaus. Wenn der Spieler die Pfeiltasten drückt oder den rechten Dreieck verwendet, schalten wir die Maus aus.

Eine Bemerkung: Wenn Sie einen Daumen nach vorne schieben, wird ein zurückgegeben positiv y-Wert In Bildschirmkoordinaten erhöhen y-Werte das Gehen abwärts. Wir möchten die y-Achse des Controllers invertieren, so dass das Drücken des Zeigesticks nach oben gerichtet ist oder uns zum oberen Rand des Bildschirms bewegt.

Wir erstellen eine statische Klasse, um die verschiedenen Eingabegeräte im Auge zu behalten und um zwischen den verschiedenen Zielarten zu wechseln.

 statische Klasse Eingabe privates statisches KeyboardState keyboardState, lastKeyboardState; privates statisches MouseState mouseState, lastMouseState; privates statisches GamePadState GamepadState, lastGamepadState; privates statisches bool isAimingWithMouse = false; public static Vector2 MousePosition get return new Vector2 (mouseState.X, mouseState.Y);  public static void Update () lastKeyboardState = keyboardState; lastMouseState = mouseState; lastGamepadState = GamepadState; keyboardState = Keyboard.GetState (); mouseState = Mouse.GetState (); gamepadState = GamePad.GetState (PlayerIndex.One); // Wenn der Spieler eine der Pfeiltasten gedrückt hat oder ein Gamepad verwendet, um zu zielen, möchten wir das Mausziel deaktivieren. Andernfalls, // wenn der Spieler die Maus bewegt, aktivieren Sie das Zielen mit der Maus. if (new [] Keys.Left, Keys.Right, Keys.Up, Keys.Down .Any (x => keyboardState.IsKeyDown (x)) || gamepadState.ThumbSticks.Right! = Vector2.Zero) isAimingWithMouse = falsch; else if (MousePosition! = new Vector2 (lastMouseState.X, lastMouseState.Y)) isAimingWithMouse = true;  // Überprüft, ob gerade eine Taste gedrückt wurde. Public static bool WasKeyPressed (Keys Key) return lastKeyboardState.IsKeyUp (key) && keyboardState.IsKeyDown (key);  public static bool WasButtonPressed (Schaltflächenschaltfläche) return lastGamepadState.IsButtonUp (Schaltfläche) && gamepadState.IsButtonDown (Schaltfläche);  public static Vector2 GetMovementDirection () Vector2 direction = gamepadState.ThumbSticks.Left; Richtung.Y * = -1; // Invertiere die Y-Achse, wenn (keyboardState.IsKeyDown (Keys.A)) direction.X - = 1; if (keyboardState.IsKeyDown (Keys.D)) direction.X + = 1; if (keyboardState.IsKeyDown (Keys.W)) direction.Y - = 1; if (keyboardState.IsKeyDown (Keys.S)) direction.Y + = 1; // Die Länge des Vektors auf maximal 1 klemmen. If (direction.LengthSquared ()> 1) direction.Normalize (); Rückkehr Richtung;  public static Vector2 GetAimDirection () if (isAimingWithMouse) return GetMouseAimDirection (); Vector2 direction = gamepadState.ThumbSticks.Right; Richtung.Y * = -1; if (keyboardState.IsKeyDown (Keys.Left)) direction.X - = 1; if (keyboardState.IsKeyDown (Keys.Right)) direction.X + = 1; if (keyboardState.IsKeyDown (Keys.Up)) direction.Y - = 1; if (keyboardState.IsKeyDown (Keys.Down)) direction.Y + = 1; // Wenn keine Zieleingabe vorliegt, wird Null zurückgegeben. Ansonsten normalisieren Sie die Richtung auf eine Länge von 1. Wenn (Richtung == Vector2.Zero) Rückgabe Vector2.Zero; sonst Rückgabe Vector2.Normalize (Richtung);  private static Vector2 GetMouseAimDirection () Vector2 direction = MousePosition - PlayerShip.Instance.Position; if (Richtung == Vector2.Zero) return Vector2.Zero; sonst Rückgabe Vector2.Normalize (Richtung);  public static bool WasBombButtonPressed () return WasButtonPressed (Buttons.LeftTrigger) || WasButtonPressed (Buttons.RightTrigger) || WasKeyPressed (Keys.Space); 

Anruf Input.Update () am Anfang von GameRoot.Update () damit die Eingabe-Klasse funktioniert.

Spitze: Möglicherweise stellen Sie fest, dass ich eine Methode für Bomben eingesetzt habe. Wir werden keine Bomben jetzt einsetzen, aber diese Methode ist für die zukünftige Verwendung verfügbar.

Möglicherweise bemerken Sie auch in GetMovementDirection () Ich hab geschrieben direction.LengthSquared ()> 1. Verwenden LengthSquared () ist eine kleine Leistungsoptimierung; Das Berechnen des Quadrats der Länge ist etwas schneller als das Berechnen der Länge selbst, da die relativ langsame Quadratwurzeloperation vermieden wird. Im gesamten Programm wird Code angezeigt, der Längen- oder Entfernungsquadrate verwendet. In diesem speziellen Fall ist der Leistungsunterschied vernachlässigbar, aber diese Optimierung kann sich in engen Schleifen auswirken.

Ziehen um

Wir sind jetzt bereit, das Schiff in Bewegung zu setzen. Fügen Sie diesen Code dem hinzu PlayerShip.Update () Methode:

 konst Schwimmgeschwindigkeit = 8; Geschwindigkeit = Geschwindigkeit * Input.GetMovementDirection (); Position + = Geschwindigkeit; Position = Vector2.Clamp (Position, Größe / 2, GameRoot.ScreenSize - Größe / 2); if (Velocity.LengthSquared ()> 0) Orientierung = Velocity.ToAngle ();

Dadurch bewegt sich das Schiff mit einer Geschwindigkeit von bis zu acht Pixeln pro Bild, klemmt seine Position, so dass es nicht aus dem Bildschirm verschwinden kann, und dreht das Schiff in die Richtung, in die es sich bewegt.

ToAngle () ist eine einfache Erweiterungsmethode, die in unserem definiert wird Erweiterungen Klasse wie folgt:

 public static float ToAngle (dieser Vector2-Vektor) return (float) Math.Atan2 (vector.Y, vector.X); 

Schießen

Wenn Sie das Spiel jetzt ausführen, sollten Sie das Schiff herumfliegen können. Jetzt lass es schießen.

Erstens brauchen wir eine Klasse für Kugeln.

 Klasse Bullet: Entity public Bullet (Position Vector2, Geschwindigkeit Vector2) image = Art.Bullet; Position = Position; Geschwindigkeit = Geschwindigkeit; Orientierung = Geschwindigkeit.ToAngle (); Radius = 8;  public override void Update () if (Velocity.LengthSquared ()> 0) Orientierung = Velocity.ToAngle (); Position + = Geschwindigkeit; // Löschen von Aufzählungszeichen, die nicht auf dem Bildschirm erscheinen if (! GameRoot.Viewport.Bounds.Contains (Position.ToPoint ())) IsExpired = true; 

Wir möchten eine kurze Abklingzeit zwischen den Aufzählungszeichen. Fügen Sie dem Feld also die folgenden Felder hinzu SpielerSchiff Klasse.

 const int cooldownFrames = 6; int cooldownRemaining = 0; statisch Random rand = new Random ();

Fügen Sie außerdem den folgenden Code hinzu PlayerShip.Update ().

 var aim = Input.GetAimDirection (); if (aim.LengthSquared ()> 0 && cooldownRemaining <= 0)  cooldownRemaining = cooldownFrames; float aimAngle = aim.ToAngle(); Quaternion aimQuat = Quaternion.CreateFromYawPitchRoll(0, 0, aimAngle); float randomSpread = rand.NextFloat(-0.04f, 0.04f) + rand.NextFloat(-0.04f, 0.04f); Vector2 vel = MathUtil.FromPolar(aimAngle + randomSpread, 11f); Vector2 offset = Vector2.Transform(new Vector2(25, -8), aimQuat); EntityManager.Add(new Bullet(Position + offset, vel)); offset = Vector2.Transform(new Vector2(25, 8), aimQuat); EntityManager.Add(new Bullet(Position + offset, vel));  if (cooldownRemaining > 0) cooldownRemaining--;

Dieser Code erstellt zwei Aufzählungszeichen, die sich parallel zueinander bewegen. Es fügt der Richtung ein wenig Zufall hinzu. Dadurch werden die Schüsse wie ein Maschinengewehr verteilt. Wir addieren zwei Zufallszahlen zusammen, da dies dazu führt, dass ihre Summe eher zentriert ist (um null herum) und weniger wahrscheinlich Kugeln in die Ferne schicken. Wir verwenden eine Quaternion, um die Ausgangsposition der Geschosse in Fahrtrichtung zu drehen.

Wir haben auch zwei neue Hilfsmethoden verwendet:

  • Random.NextFloat () gibt ein Float zwischen einem minimalen und einem maximalen Wert zurück.
  • MathUtil.FromPolar () schafft ein Vector2 aus einem Winkel und einer Größe.
 // in Extensions public static float NextFloat (Random rand, float minValue, float maxValue) return (float) rand.NextDouble () * (maxValue - minValue) + minValue;  // in MathUtil public static Vector2 FromPolar (Float-Winkel, Float-Größe) Rückgabewert * new Vector2 ((Float) Math.Cos (Winkel), (Float) Math.Sin (Winkel)); 

Benutzerdefinierter Cursor

Es gibt noch eine weitere Sache, die wir jetzt tun sollten Eingang Klasse. Lassen Sie uns einen benutzerdefinierten Mauszeiger zeichnen, um leichter zu sehen, wohin das Schiff zielt. Im GameRoot.Draw, einfach zeichnen Art.Pointer an der Mausposition.

 spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (spriteBatch); // den benutzerdefinierten Mauszeiger zeichnen spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();

Fazit

Wenn Sie das Spiel jetzt testen, können Sie das Schiff mit den WASD-Tasten oder oder mit dem linken Stift bewegen und den ununterbrochenen Strom von Kugeln mit den Pfeiltasten, der Maus oder dem rechten Daumen zielen.

Im nächsten Teil werden wir das Spiel komplettieren, indem wir Feinde und eine Punktzahl hinzufügen.