Crea uno sparatutto al neon in XNA Gameplay di base

In questa serie di tutorial, ti mostrerò come creare uno sparatutto al neon con due gemelli come Geometry Wars, che chiameremo Shape Blaster, in XNA. L'obiettivo di queste esercitazioni non è quello di lasciarti con una replica esatta di Geometry Wars, ma piuttosto di esaminare gli elementi necessari che ti permetteranno di creare la tua variante di alta qualità.

Vi incoraggio a espandere e sperimentare il codice fornito in queste esercitazioni. Tratteremo questi argomenti attraverso le serie:

  1. Imposta il gameplay di base, creando la nave del giocatore e gestendo input, suoni e musica.
  2. Completa l'implementazione delle meccaniche di gioco aggiungendo nemici, gestendo il rilevamento delle collisioni e monitorando il punteggio e le vite del giocatore.
  3. Aggiungi un filtro di fioritura, che è l'effetto che darà alla grafica un bagliore al neon.
  4. Aggiungi effetti di particelle folli e esagerati.
  5. Aggiungi la griglia dello sfondo di deformazione.

Ecco cosa avremo alla fine della serie:

Avvertenza: forte!

Ed ecco cosa avremo alla fine di questa prima parte:

Avvertenza: forte!

La musica e gli effetti sonori che puoi sentire in questi video sono stati creati da RetroModular e puoi leggere come ha fatto a Audiotuts+.

Gli sprite sono di Jacob Zinman-Jeanes, il nostro tutor + designer residente. Tutte le opere d'arte possono essere trovate nel file zip di download del file sorgente.

Il carattere è Nova Square, di Wojciech Kalinowski.

Iniziamo.


Panoramica

In questo tutorial creeremo uno sparatutto twin-stick; il giocatore controllerà la nave con la tastiera, la tastiera e il mouse o le due levette di un gamepad. 

Usiamo un certo numero di classi per realizzare questo:

  • Entità: La classe base per nemici, proiettili e la nave del giocatore. Le entità possono muoversi e essere disegnate.
  • proiettile e PlayerShip.
  • EntityManager: Tiene traccia di tutte le entità nel gioco ed esegue il rilevamento delle collisioni.
  • Ingresso: Aiuta a gestire input da tastiera, mouse e gamepad.
  • Arte: Carica e mantiene i riferimenti alle trame necessarie per il gioco.
  • Suono: Carica e mantiene i riferimenti ai suoni e alla musica.
  • MathUtil e estensioni: Contiene alcuni metodi statici utili e metodi di estensione.
  • GameRoot: Controlla il ciclo principale del gioco. Questo è il Game1 la classe XNA genera automaticamente, rinominata.

Il codice in questo tutorial si propone di essere semplice e facile da capire. Non avrà tutte le funzionalità o un'architettura complicata progettata per supportare ogni possibile esigenza. Piuttosto, farà solo ciò che deve fare. Mantenerlo semplice renderà più facile la comprensione dei concetti, quindi modificarli ed espanderli nel tuo gioco unico.


Entità e la nave del giocatore

Crea un nuovo progetto XNA. Rinominare il Game1 classe a qualcosa di più adatto. L'ho chiamato GameRoot.

Ora iniziamo creando una classe base per le nostre entità di gioco.

 classe astratta Entità immagine Texture2D protetta; // La tinta dell'immagine. Questo ci permetterà anche di cambiare la trasparenza. protetto Colore colore = Colore. Bianco; posizione vettoriale pubblica2, velocità; orientamento del galleggiante pubblico; Raggio flottante pubblico = 20; // utilizzato per il rilevamento pubblico di collisioni circolari IsExpired; // true se l'entità è stata distrutta e deve essere eliminata. Dimensione pubblica Vector2 ottieni immagine di ritorno == null? Vector2.Zero: nuovo Vector2 (image.Width, image.Height);  public void abstract Update (); public virtual void Draw (SpriteBatch spriteBatch) spriteBatch.Draw (image, Position, null, color, Orientation, Size / 2f, 1f, 0, 0); 

Tutte le nostre entità (nemici, proiettili e la nave del giocatore) hanno alcune proprietà di base come un'immagine e una posizione. È scaduto sarà usato per indicare che l'entità è stata distrutta e dovrebbe essere rimossa da qualsiasi lista che abbia un riferimento ad essa.

Quindi creiamo un EntityManager per monitorare le nostre entità e per aggiornarle e disegnarle.

 classe statica EntityManager Lista statica entità = nuova lista(); bool statico isUpdating; Elenco statico addedEntities = new List(); public static int Count get return entities.Count;  public static void Add (Entità entità) if (! isUpdating) entities.Add (entity); else addedEntities.Add (entity);  public static void Update () isUpdating = true; foreach (entità var in entità) entity.Update (); isUpdating = false; entità foreach (var entity in addedEntities). Add (entity); addedEntities.Clear (); // rimuove qualsiasi entità scaduta. entities = entities.Where (x =>! x.IsExpired) .ToList ();  public static void Draw (SpriteBatch spriteBatch) foreach (entità var in entità) entity.Draw (spriteBatch); 

Ricorda che se si modifica un elenco mentre si esegue iterazione su di esso, si otterrà un'eccezione. Il codice sopra si occupa di questo accodando le entità aggiunte durante l'aggiornamento in un elenco separato e aggiungendole dopo aver completato l'aggiornamento delle entità esistenti.

Rendili visibili

Dovremo caricare alcune trame se vogliamo disegnare qualsiasi cosa. Faremo una classe statica per contenere i riferimenti a tutte le nostre trame.

 classe statica Art public static Texture2D Player get; set privato;  Cercatore di texture2D statico pubblico get; set privato;  public statico Texture2D Wanderer get; set privato;  Bullet Texture2D statico pubblico get; set privato;  Puntatore Texture2D statico pubblico get; set privato;  public static void Load (contenuto ContentManager) Player = content.Load("Giocatore"); Cercatore = content.Load("Cercatore"); Wanderer = content.Load( "Wanderer"); Bullet = content.Load( "Bullet"); Puntatore = content.Load( "Pointer"); 

Carica l'arte chiamando Art.Load (Contenuto) nel GameRoot.LoadContent (). Inoltre, un certo numero di classi dovrà conoscere le dimensioni dello schermo, quindi aggiungere le seguenti proprietà a GameRoot:

 Istanza statica di GameRoot pubblica get; set privato;  public viewport statico Viewport get return Instance.GraphicsDevice.Viewport;  public statico ScreenSize Vector2 get return new Vector2 (Viewport.Width, Viewport.Height); 

E nel GameRoot costruttore, aggiungere:

 Istanza = questo;

Ora inizieremo a scrivere il PlayerShip classe.

 class PlayerShip: Entity istanza PlayerShip statica privata; istanza PlayerShip statica pubblica get if (instance == null) instance = new PlayerShip (); restituire istanza;  PlayerShip privato () image = Art.Player; Posizione = GameRoot.ScreenSize / 2; Raggio = 10;  public override void Update () // La logica di spedizione va qui

Abbiamo fatto PlayerShip un singleton, imposta la sua immagine e la posiziona al centro dello schermo.

Infine, aggiungiamo la nave giocatore al EntityManager e aggiornalo e disegnalo. Aggiungi il seguente codice in GameRoot:

 // in Initialize (), dopo la chiamata a 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 ();

Disegniamo gli sprite con miscelazione additiva, che fa parte di ciò che darà loro il loro aspetto al neon. Se esegui il gioco a questo punto dovresti vedere la tua nave al centro dello schermo. Tuttavia, non risponde ancora all'input. Risolviamolo.


Ingresso

Per il movimento, il giocatore può usare WASD sulla tastiera o la levetta sinistra su un gamepad. Per puntare, possono usare i tasti freccia, la levetta destra o il mouse. Non è necessario che il giocatore tenga premuto il pulsante del mouse per scattare perché è scomodo tenere il pulsante continuamente premuto. Questo ci lascia un piccolo problema: come facciamo a sapere se il giocatore punta al mouse, alla tastiera o al gamepad?

Useremo il seguente sistema: aggiungeremo input per tastiera e gamepad insieme. Se il giocatore muove il mouse, passiamo al puntamento del mouse. Se il giocatore preme i tasti freccia o usa la levetta destra, disattiviamo il puntamento del mouse.

Una cosa da notare: premendo una levetta in avanti si restituirà a positivo valore y. Nelle coordinate dello schermo, i valori y aumentano verso il basso. Vogliamo invertire l'asse y sul controller in modo che spingendo la levetta verso l'alto si mirino o ci muoviamo verso la parte superiore dello schermo.

Creeremo una classe statica per tenere traccia dei vari dispositivi di input e occuparci del passaggio tra i diversi tipi di mira.

 classe statica Input tastiera statica privata KeyboardState keyboardState, lastKeyboardState; mouse statico privato mouseState, lastMouseState; GamePadState statico privato gamepadState, lastGamepadState; bool statico privato 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); // Se il giocatore ha premuto uno dei tasti freccia o sta usando un gamepad per mirare, vogliamo disabilitare il puntamento del mouse. Altrimenti, // se il giocatore muove il mouse, abilita il puntamento del mouse. if (new [] Keys.Left, Keys.Right, Keys.Up, Keys.Down .Any (x => keyboardState.IsKeyDown (x)) || gamepadState.ThumbSticks.Right! = Vector2.Zero) isAimingWithMouse = false; altrimenti if (MousePosition! = new Vector2 (lastMouseState.X, lastMouseState.Y)) isAimingWithMouse = true;  // Controlla se una chiave è stata appena premuta public bool statico WasKeyPressed (tasto Keys) return lastKeyboardState.IsKeyUp (chiave) && keyboardState.IsKeyDown (chiave);  public static bool WasButtonPressed (pulsante Buttons) return lastGamepadState.IsButtonUp (button) && gamepadState.IsButtonDown (pulsante);  public static Vector2 GetMovementDirection () Vector2 direction = gamepadState.ThumbSticks.Left; direction.Y * = -1; // inverti l'asse y se (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; // Blocca la lunghezza del vettore fino a un massimo di 1. if (direction.LengthSquared ()> 1) direction.Normalize (); direzione di ritorno;  public static Vector2 GetAimDirection () if (isAimingWithMouse) restituisce GetMouseAimDirection (); Direzione Vector2 = gamepadState.ThumbSticks.Right; direction.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; // Se non è stato inserito l'obiettivo, restituire zero. Altrimenti normalizza la direzione per avere una lunghezza di 1. if (direction == Vector2.Zero) return Vector2.Zero; else return Vector2.Normalize (direction);  private static Vector2 GetMouseAimDirection () Vector2 direction = MousePosition - PlayerShip.Instance.Position; if (direction == Vector2.Zero) return Vector2.Zero; else return Vector2.Normalize (direction);  public static bool WasBombButtonPressed () return WasButtonPressed (Buttons.LeftTrigger) || WasButtonPressed (Buttons.RightTrigger) || WasKeyPressed (Keys.Space); 

Chiamata Input.Update () all'inizio di GameRoot.Update () perché la classe di input funzioni.

Mancia: Potresti notare che ho incluso un metodo per le bombe. Non implementeremo le bombe ora, ma quel metodo è lì per un uso futuro.

Si può anche notare in GetMovementDirection () scrissi direction.LengthSquared ()> 1. utilizzando LengthSquared () è una piccola ottimizzazione delle prestazioni; calcolare il quadrato della lunghezza è un po 'più veloce del calcolo della lunghezza stessa perché evita l'operazione della radice quadrata relativamente lenta. Vedrai il codice usando i quadrati di lunghezze o distanze nel corso del programma. In questo caso particolare, la differenza di prestazioni è trascurabile, ma questa ottimizzazione può fare la differenza quando viene utilizzata in cicli stretti.

In movimento

Ora siamo pronti a far muovere la nave. Aggiungi questo codice al PlayerShip.Update () metodo:

 const float speed = 8; Velocity = speed * Input.GetMovementDirection (); Posizione + = Velocità; Position = Vector2.Clamp (Position, Size / 2, GameRoot.ScreenSize - Size / 2); if (Velocity.LengthSquared ()> 0) Orientation = Velocity.ToAngle ();

Questo farà muovere la nave ad una velocità fino a otto pixel per fotogramma, bloccando la sua posizione in modo che non possa uscire dallo schermo e ruotare la nave per affrontare la direzione in cui si muove.

ToAngle () è un semplice metodo di estensione definito nel nostro estensioni classe in questo modo:

 float statico pubblico ToAngle (questo vettore Vector2) return (float) Math.Atan2 (vector.Y, vector.X); 

Tiro

Se corri adesso, dovresti riuscire a far volare la nave. Ora facciamolo sparare.

Innanzitutto, abbiamo bisogno di una classe per i proiettili.

 classe Bullet: Entity public Bullet (posizione Vector2, velocità Vector2) image = Art.Bullet; Posizione = posizione; Velocità = velocità; Orientation = Velocity.ToAngle (); Raggio = 8;  public override void Update () if (Velocity.LengthSquared ()> 0) Orientation = Velocity.ToAngle (); Posizione + = Velocità; // elimina i punti elenco che vengono visualizzati fuori schermo se (! GameRoot.Viewport.Bounds.Contains (Position.ToPoint ())) IsExpired = true; 

Vogliamo un breve periodo di attesa tra i proiettili, quindi aggiungi i seguenti campi al PlayerShip classe.

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

Inoltre, aggiungere il seguente codice a 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--;

Questo codice crea due punti elenco che viaggiano paralleli l'uno all'altro. Aggiunge una piccola quantità di casualità alla direzione. Questo rende gli scatti un po 'sparsi come una mitragliatrice. Aggiungiamo insieme due numeri casuali perché questo rende più probabile che la somma sia centrata (intorno allo zero) e meno probabilità di inviare proiettili lontano. Usiamo un quaternione per ruotare la posizione iniziale dei proiettili nella direzione in cui viaggiano.

Abbiamo anche utilizzato due nuovi metodi di supporto:

  • Random.NextFloat () restituisce un float tra un valore minimo e massimo.
  • MathUtil.FromPolar () crea a Vector2 da un angolo e grandezza.
 // in Estensioni public static float NextFloat (questo Random rand, float minValue, float maxValue) return (float) rand.NextDouble () * (maxValue - minValue) + minValue;  // in MathUtil public static Vector2 FromPolar (float angle, float magnitude) return magnitude * new Vector2 ((float) Math.Cos (angle), (float) Math.Sin (angle)); 

Cursore personalizzato

C'è un'altra cosa che dovremmo fare ora che abbiamo il Ingresso classe. Disegniamo un cursore del mouse personalizzato per rendere più facile vedere dove punta la nave. Nel GameRoot.Draw, semplicemente disegnare Art.Pointer alla posizione del mouse.

 spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (SpriteBatch); // disegna il cursore del mouse personalizzato spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();

Conclusione

Se stai testando il gioco ora, sarai in grado di spostare la nave con i tasti WASD o o con la levetta sinistra e mirare al flusso continuo di proiettili con i tasti freccia, il mouse o la levetta destra.

Nella parte successiva, completeremo il gameplay aggiungendo nemici e un punteggio.