Crea uno sparatutto al neon in XNA più gioco

In questa serie di tutorial, ti mostrerò come realizzare uno sparatutto al neon con due gemelli, come Geometry Wars, 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à.


Panoramica

In questa parte svilupperemo il tutorial precedente aggiungendo nemici, rilevamento collisioni e punteggio.

Ecco cosa avremo alla fine di questo:

Avvertenza: forte!

Aggiungeremo le seguenti nuove classi per gestire questo:

  • Nemico
  • EnemySpawner: Responsabile della creazione di nemici e graduale aumento della difficoltà del gioco.
  • PlayerStatus: Tiene traccia del punteggio, del punteggio elevato e delle vite del giocatore.

Potresti aver notato che ci sono due tipi di nemici nel video, ma ce n'è solo uno Nemico classe. Potremmo derivare sottoclassi da Nemico per ogni tipo di nemico. Tuttavia, preferisco evitare le gerarchie di classi profonde perché presentano alcuni inconvenienti:

  • Aggiungono più codice standard.
  • Possono aumentare la complessità del codice e renderlo più difficile da capire. Lo stato e la funzionalità di un oggetto si diffondono su tutta la sua catena ereditaria.
  • Non sono molto flessibili. Non è possibile condividere parti di funzionalità tra diversi rami dell'albero di ereditarietà se tale funzionalità non si trova nella classe base. Ad esempio, considera la possibilità di creare due classi, Mammifero e Uccello, da cui entrambi derivano Animale. Il Uccello la classe ha un Volare() metodo. Quindi decidi di aggiungere un pipistrello classe che deriva da Mammifero e può anche volare. Per condividere questa funzionalità usando solo l'ereditarietà dovresti spostare il file Volare() metodo per il Animale classe in cui non appartiene. Inoltre, non è possibile rimuovere i metodi dalle classi derivate, quindi se si è fatto un Pinguino classe derivata da Uccello, avrebbe anche avuto un Volare() metodo.

Per questo tutorial, favoriremo la composizione sull'ereditarietà per l'implementazione dei diversi tipi di nemici. Lo faremo creando vari comportamenti riutilizzabili che possiamo aggiungere ai nemici. Possiamo quindi facilmente combinare i comportamenti quando creiamo nuovi tipi di nemici. Ad esempio, se abbiamo già avuto un FollowPlayer comportamento e a DodgeBullet comportamento, potremmo fare un nuovo nemico che fa entrambi semplicemente aggiungendo entrambi i comportamenti.

Post correlati
  • Introduzione alla programmazione orientata agli oggetti per lo sviluppo di giochi
  • Un approccio pragmatico alla composizione delle entità

Nemici

I nemici avranno alcune proprietà aggiuntive rispetto alle entità. Per dare al giocatore un po 'di tempo per reagire, faremo svanire gradualmente i nemici prima che diventino attivi e pericolosi.

Cerchiamo di codificare la struttura di base del Nemico classe.

 class Enemy: Entity private int timeUntilStart = 60; public bool IsActive get return timeUntilStart <= 0;   public Enemy(Texture2D image, Vector2 position)  this.image = image; Position = position; Radius = image.Width / 2f; color = Color.Transparent;  public override void Update()  if (timeUntilStart <= 0)  // enemy behaviour logic goes here.  else  timeUntilStart--; color = Color.White * (1 - timeUntilStart / 60f);  Position += Velocity; Position = Vector2.Clamp(Position, Size / 2, GameRoot.ScreenSize - Size / 2); Velocity *= 0.8f;  public void WasShot()  IsExpired = true;  

Questo codice farà sbiadire i nemici per 60 fotogrammi e permetterà alla loro velocità di funzionare. Moltiplicare la velocità di 0.8 falsi un effetto simile ad attrito. Se facciamo accelerare i nemici a una velocità costante, questa attrito li farà avvicinare facilmente alla massima velocità. Mi piace la semplicità e la scorrevolezza di questo tipo di attrito, ma potresti voler usare una formula diversa a seconda dell'effetto desiderato.

Il Fu sparato() il metodo verrà chiamato quando il nemico viene colpito. Aggiungeremo altro in seguito nella serie.

Vogliamo che diversi tipi di nemici si comportino diversamente. Lo faremo assegnando comportamenti. Un comportamento utilizzerà alcune funzioni personalizzate che eseguono ciascun fotogramma per controllare il nemico. Implementeremo il comportamento usando un iteratore.

Gli iteratori (chiamati anche generatori) in C # sono metodi speciali che possono fermarsi a metà strada e successivamente riprendere dal punto in cui erano stati interrotti. È possibile creare un iteratore creando un metodo con un tipo restituito IEnumerable <> e usando la parola chiave yield dove vuoi che ritorni e successivamente riprenda. Gli iteratori in C # richiedono che tu restituisca qualcosa quando cedi. Non abbiamo davvero bisogno di restituire nulla, quindi i nostri iteratori porteranno semplicemente zero.

Il nostro comportamento più semplice sarà il FollowPlayer () comportamento mostrato di seguito.

 IEnumerable FollowPlayer (float acceleration = 1f) while (true) Velocity + = (PlayerShip.Instance.Position - Position) .ScaleTo (acceleration); if (Velocity! = Vector2.Zero) Orientation = Velocity.ToAngle (); yield return 0; 

Questo fa semplicemente accelerare il nemico verso il giocatore a un ritmo costante. L'attrito che abbiamo aggiunto in precedenza assicurerà che alla fine si superi a una velocità massima (5 pixel per frame quando l'accelerazione è 1 poiché \ (0.8 \ times 5 + 1 = 5 \)). Ogni fotogramma, questo metodo verrà eseguito fino a quando non raggiunge l'istruzione di rendimento e quindi riprenderà da dove ha lasciato il prossimo fotogramma.

Forse ti starai chiedendo perché abbiamo infastidito gli iteratori, dal momento che avremmo potuto svolgere lo stesso compito più facilmente con un semplice delegato. Usare gli iteratori paga con metodi più complessi che altrimenti ci richiederebbero di memorizzare lo stato nelle variabili membro della classe.

Ad esempio, di seguito è riportato un comportamento che fa muovere un nemico in uno schema quadrato:

 IEnumerable MoveInASquare () const int framesPerSide = 30; while (true) // sposta a destra per 30 frame per (int i = 0; i < framesPerSide; i++)  Velocity = Vector2.UnitX; yield return 0;  // move down for (int i = 0; i < framesPerSide; i++)  Velocity = Vector2.UnitY; yield return 0;  // move left for (int i = 0; i < framesPerSide; i++)  Velocity = -Vector2.UnitX; yield return 0;  // move up for (int i = 0; i < framesPerSide; i++)  Velocity = -Vector2.UnitY; yield return 0;   

La cosa bella di questo è che non ci salva solo alcune variabili di istanza, ma struttura anche il codice in modo molto logico. Puoi vedere subito che il nemico si muoverà a destra, poi in basso, poi a sinistra, poi in alto, e quindi ripetere. Se invece si implementasse questo metodo come macchina di stato, il flusso di controllo sarebbe meno ovvio.

Aggiungiamo l'impalcatura necessaria per far funzionare i comportamenti. I nemici devono memorizzare i loro comportamenti, quindi aggiungeremo una variabile al Nemico classe.

 Elenco privato> comportamenti = nuova lista> ();

Si noti che un comportamento ha il tipo IEnumerator, non IEnumerable. Puoi pensare al IEnumerable come modello per il comportamento e il IEnumerator come l'istanza in esecuzione. Il IEnumerator ricorda dove siamo nel comportamento e riprenderà da dove era stato interrotto quando lo chiamate MoveNext () metodo. Ogni frame passeremo attraverso tutti i comportamenti che il nemico ha e chiama MoveNext () su ciascuno di essi. Se MoveNext () restituisce false, significa che il comportamento è stato completato, quindi dovremmo rimuoverlo dall'elenco.

Aggiungeremo i seguenti metodi al Nemico classe:

 vuoto privato AddBehaviour (IEnumerable comportamento) behaviours.Add (behaviour.GetEnumerator ());  private void ApplyBehaviours () for (int i = 0; i < behaviours.Count; i++)  if (!behaviours[i].MoveNext()) behaviours.RemoveAt(i--);  

E modificheremo il Aggiornare() metodo da chiamare ApplyBehaviours ():

 se (timeUntilStart <= 0) ApplyBehaviours(); //… 

Ora possiamo creare un metodo statico per creare nemici alla ricerca. Tutto quello che dobbiamo fare è scegliere l'immagine che vogliamo e aggiungere il FollowPlayer () comportamento.

 public static Enemy CreateSeeker (posizione Vector2) var enemy = new Enemy (Art.Seeker, position); enemy.AddBehaviour (enemy.FollowPlayer ()); restituire il nemico; 

Per rendere un nemico che si muove in modo casuale, lo faremo scegliere una direzione e quindi apportare piccole regolazioni casuali in quella direzione. Tuttavia, se regoliamo la direzione ogni fotogramma, il movimento sarà nervoso, quindi regoleremo la direzione solo periodicamente. Se il nemico corre sul bordo dello schermo, lo faremo scegliere una nuova direzione casuale che punta lontano dal muro.

 IEnumerable MoveRandomly () float direction = rand.NextFloat (0, MathHelper.TwoPi); while (true) direction + = rand.NextFloat (-0.1f, 0.1f); direction = MathHelper.WrapAngle (direction); per (int i = 0; i < 6; i++)  Velocity += MathUtil.FromPolar(direction, 0.4f); Orientation -= 0.05f; var bounds = GameRoot.Viewport.Bounds; bounds.Inflate(-image.Width, -image.Height); // if the enemy is outside the bounds, make it move away from the edge if (!bounds.Contains(Position.ToPoint())) direction = (GameRoot.ScreenSize / 2 - Position).ToAngle() + rand.NextFloat(-MathHelper.PiOver2, MathHelper.PiOver2); yield return 0;   

Ora possiamo creare un metodo di produzione per la creazione di nemici erranti, proprio come abbiamo fatto per il ricercatore:

 pubblico statico Enemy CreateWanderer (posizione Vector2) var enemy = new Enemy (Art. Wanderer, position); enemy.AddBehaviour (enemy.MoveRandomly ()); restituire il nemico; 

Rilevamento collisione

Per il rilevamento delle collisioni, modelleremo la nave del giocatore, i nemici e i proiettili come cerchi. Il rilevamento delle collisioni circolari è bello perché è semplice, è veloce e non cambia quando gli oggetti ruotano. Se ricordi, il Entità la classe ha un raggio e una posizione (la posizione si riferisce al centro dell'entità). Questo è tutto ciò di cui abbiamo bisogno per il rilevamento delle collisioni circolari.

Testare ciascuna entità contro tutte le altre entità che potrebbero potenzialmente collidere può essere molto lenta se si dispone di un numero elevato di entità. Esistono molte tecniche che è possibile utilizzare per accelerare il rilevamento di collisioni di fase ampia, come quadrifori, sweep e prune e alberi BSP. Tuttavia, per ora, avremo solo poche decine di entità sullo schermo alla volta, quindi non ci preoccuperemo di queste tecniche più complesse. Possiamo sempre aggiungerli più tardi se ne abbiamo bisogno.

In Shape Blaster, non tutte le entità possono entrare in collisione con ogni altro tipo di entità. I proiettili e la nave del giocatore possono scontrarsi solo con i nemici. I nemici possono anche scontrarsi con altri nemici - questo impedirà loro di sovrapporsi.

Per gestire questi diversi tipi di collisioni, aggiungeremo due nuovi elenchi al EntityManager per tenere traccia di proiettili e nemici. Ogni volta che aggiungiamo un'entità al EntityManager, vorremmo aggiungerlo all'elenco appropriato, quindi faremo un privato AddEntity () metodo per farlo. Saremo anche sicuri di rimuovere tutte le entità scadute da tutti gli elenchi di ogni frame.

 Elenco statico nemici = nuova lista(); Elenco statico pallottole = nuova lista(); vuoto statico privato AddEntity (Entity Entity) entities.Add (entity); if (entity is Bullet) bullets.Add (entity as Bullet); else if (entity is Enemy) enemy.Add (entity as Enemy);  // ... // in Update () bullets = bullets.Where (x =>! X.IsExpired) .ToList (); enemy = enemy.Where (x =>! x.IsExpired) .ToList ();

Sostituisci le chiamate a entity.Add () nel EntityManager.Add () e EntityManager.Update () con chiamate a AddEntity ().

Ora aggiungiamo un metodo che determinerà se due entità si scontrano:

 private static bool IsColliding (Entity a, Entity b) float radius = a.Radius + b.Radius; return! a.IsExpired &&! b.IsExpired && Vector2.DistanceSquared (a.Position, b.Position) < radius * radius; 

Per determinare se due cerchi si sovrappongono, è sufficiente verificare se la distanza tra loro è inferiore alla somma dei loro raggi. Il nostro metodo ottimizza questo leggermente controllando se il quadrato della distanza è inferiore al quadrato della somma dei raggi. Ricorda che è un po 'più veloce calcolare la distanza al quadrato della distanza effettiva.

Cose diverse accadranno a seconda di quali due oggetti si scontrano. Se due nemici si scontrano, vogliamo che si spingano a vicenda. Se un proiettile colpisce un nemico, entrambi i proiettili e il nemico dovrebbero essere distrutti. Se il giocatore tocca un nemico, il giocatore dovrebbe morire e il livello dovrebbe resettare.

Aggiungeremo un HandleCollision () metodo per il Nemico classe per gestire le collisioni tra i nemici:

 public void HandleCollision (Enemy other) var d = Position - other.Position; Velocity + = 10 * d / (d.LengthSquared () + 1); 

Questo metodo spingerà il nemico attuale lontano dall'altro nemico. Più vicini sono, più difficile sarà spinto, perché la grandezza di (d / d.LengthSquared ()) è solo uno sulla distanza.

Ripetizione del giocatore

Quindi abbiamo bisogno di un metodo per gestire la nave del giocatore che viene uccisa. Quando ciò accade, la nave del giocatore sparirà per un breve periodo prima di rinascere.

Iniziamo aggiungendo due nuovi membri a PlayerShip.

 int framesUntilRespawn = 0; public bool IsDead get return framesUntilRespawn> 0; 

All'inizio di PlayerShip.Update (), aggiungere il seguente:

 if (IsDead) framesUntilRespawn--; ritorno; 

E abbiamo l'override Disegnare() come mostrato:

 public override void Draw (SpriteBatch spriteBatch) if (! IsDead) base.Draw (spriteBatch); 

Infine, aggiungiamo a Uccidere() metodo a PlayerShip.

 public void Kill () framesUntilRespawn = 60; 

Ora che tutti i pezzi sono a posto, aggiungeremo un metodo al EntityManager che passa attraverso tutte le entità e controlla le collisioni.

 static void HandleCollisions () // gestisce le collisioni tra nemici per (int i = 0; i < enemies.Count; i++) for (int j = i + 1; j < enemies.Count; j++)  if (IsColliding(enemies[i], enemies[j]))  enemies[i].HandleCollision(enemies[j]); enemies[j].HandleCollision(enemies[i]);   // handle collisions between bullets and enemies for (int i = 0; i < enemies.Count; i++) for (int j = 0; j < bullets.Count; j++)  if (IsColliding(enemies[i], bullets[j]))  enemies[i].WasShot(); bullets[j].IsExpired = true;   // handle collisions between the player and enemies for (int i = 0; i < enemies.Count; i++)  if (enemies[i].IsActive && IsColliding(PlayerShip.Instance, enemies[i]))  PlayerShip.Instance.Kill(); enemies.ForEach(x => x.WasShot ()); rompere; 

Chiama questo metodo da Aggiornare() immediatamente dopo l'impostazione isUpdating a vero.


Spawner nemico

L'ultima cosa da fare è fare il EnemySpawner classe, che è responsabile per la creazione di nemici. Vogliamo che il gioco inizi facilmente e diventi più difficile, quindi il gioco EnemySpawner creerà i nemici ad un ritmo crescente con il passare del tempo. Quando il giocatore muore, ripristineremo il EnemySpawner alla sua difficoltà iniziale.

 classe statica EnemySpawner static Random rand = new Random (); static float inverseSpawnChance = 60; public static void Update () if (! PlayerShip.Instance.IsDead && EntityManager.Count < 200)  if (rand.Next((int)inverseSpawnChance) == 0) EntityManager.Add(Enemy.CreateSeeker(GetSpawnPosition())); if (rand.Next((int)inverseSpawnChance) == 0) EntityManager.Add(Enemy.CreateWanderer(GetSpawnPosition()));  // slowly increase the spawn rate as time progresses if (inverseSpawnChance > 20) inverseSpawnChance - = 0.005f;  private static Vector2 GetSpawnPosition () Vector2 pos; do pos = new Vector2 (rand.Next ((int) GameRoot.ScreenSize.X), rand.Next ((int) GameRoot.ScreenSize.Y));  while (Vector2.DistanceSquared (pos, PlayerShip.Instance.Position) < 250 * 250); return pos;  public static void Reset()  inverseSpawnChance = 60;  

Ogni frame, ce n'è uno in inverseSpawnChance di generare ogni tipo di nemico. La possibilità di generare un nemico aumenta gradualmente fino a raggiungere un massimo di uno su venti. I nemici vengono sempre creati ad almeno 250 pixel di distanza dal giocatore.

Fai attenzione al ciclo while in GetSpawnPosition (). Funzionerà in modo efficiente finché l'area in cui i nemici possono spawnare è più grande dell'area in cui non possono spawnare. Tuttavia, se rendi l'area proibita troppo grande, otterrai un ciclo infinito.

Chiamata EnemySpawner.Update () a partire dal GameRoot.Update () e chiama EnemySpawner.Reset () quando il giocatore viene ucciso.


Punteggio e vita

In Shape Blaster, inizierai con quattro vite e otterrai una vita aggiuntiva ogni 2000 punti. Ottieni punti per distruggere i nemici, con diversi tipi di nemici che valgono diversi punti. Ogni nemico distrutto aumenta anche il moltiplicatore del punteggio di uno. Se non uccidi nessun nemico entro un breve lasso di tempo, il tuo moltiplicatore verrà resettato. La quantità totale di punti ricevuti da ciascun nemico che distruggi è il numero di punti che il nemico ha moltiplicato per il moltiplicatore corrente. Se perdi tutte le vite, il gioco è finito e inizi una nuova partita con il tuo punteggio azzerato.

Per gestire tutto questo, faremo una classe statica chiamata PlayerStatus.

 classe statica PlayerStatus // quantità di tempo che impiega, in secondi, affinché un moltiplicatore scada. private const float multiplierExpiryTime = 0.8f; private const int maxMultiplier = 20; public static int Vive get; set privato;  public static int Punteggio get; set privato;  public static int Multiplier get; set privato;  private static float multiplierTimeLeft; // tempo fino al moltiplicatore corrente scade private static int scoreForExtraLife; // punteggio richiesto per guadagnare una vita extra // Statico costruttore statico PlayerStatus () Reset ();  public static void Reset () Score = 0; Moltiplicatore = 1; Vive = 4; scoreForExtraLife = 2000; multiplierTimeLeft = 0;  public static void Update () if (Moltiplicatore> 1) // aggiorna il timer moltiplicatore if ((multiplierTimeLeft - = (float) GameRoot.GameTime.ElapsedGameTime.TotalSeconds) <= 0)  multiplierTimeLeft = multiplierExpiryTime; ResetMultiplier();    public static void AddPoints(int basePoints)  if (PlayerShip.Instance.IsDead) return; Score += basePoints * Multiplier; while (Score >= scoreForExtraLife) scoreForExtraLife + = 2000; Vive ++;  public static void IncreaseMultiplier () if (PlayerShip.Instance.IsDead) return; multiplierTimeLeft = multiplierExpiryTime; if (Moltiplicatore < maxMultiplier) Multiplier++;  public static void ResetMultiplier()  Multiplier = 1;  public static void RemoveLife()  Lives--;  

Chiamata PlayerStatus.Update () a partire dal GameRoot.Update () quando il gioco non è in pausa.

Successivamente vogliamo mostrare il tuo punteggio, le vite e il moltiplicatore sullo schermo. Per fare ciò dovremo aggiungere un SpriteFont nel Soddisfare progetto e una variabile corrispondente nel Arte classe, che nomineremo Font. Carica il carattere Art.Load () come abbiamo fatto con le trame.

Nota: C'è un font chiamato Nova Square incluso nei file sorgente di Shape Blaster che puoi usare. Per utilizzare il carattere, è necessario prima installarlo e quindi riavviare Visual Studio se era aperto. È quindi possibile modificare il nome del font nel file del font sprite su "Nova Square". Il progetto demo non usa questo tipo di carattere di default perché impedirà la compilazione del progetto se il font non è installato.

Modifica la fine di GameRoot.Draw () dove il cursore è disegnato come mostrato di seguito.

 spriteBatch.Begin (0, BlendState.Additive); spriteBatch.DrawString (Art.Font, "Lives:" + PlayerStatus.Lives, new Vector2 (5), Color.White); DrawRightAlignedString ("Punteggio:" + PlayerStatus.Score, 5); DrawRightAlignedString ("Moltiplicatore:" + PlayerStatus.Multiplier, 35); // disegna il cursore del mouse personalizzato spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();

DrawRightAlignedString () è un metodo di supporto per disegnare il testo allineato sul lato destro dello schermo. Aggiungilo a GameRoot aggiungendo il codice qui sotto.

 private void DrawRightAlignedString (testo stringa, float y) var textWidth = Art.Font.MeasureString (testo) .X; spriteBatch.DrawString (Art.Font, text, new Vector2 (ScreenSize.X - textWidth - 5, y), Color.White); 

Ora le tue vite, punteggio e moltiplicatore dovrebbero essere visualizzati sullo schermo. Tuttavia, dobbiamo ancora modificare questi valori in risposta agli eventi di gioco. Aggiungi una proprietà chiamata PointValue al Nemico classe.

 public int PointValue get; set privato; 

Imposta il valore in punti per diversi nemici su qualcosa che ritieni appropriato. Ho fatto guadagnare ai nemici vagabondi un punto, e i nemici in cerca valevano due punti.

Quindi, aggiungere le seguenti due righe a Enemy.WasShot () per aumentare il punteggio e il moltiplicatore del giocatore:

 PlayerStatus.AddPoints (PointValue); PlayerStatus.IncreaseMultiplier ();

Chiamata PlayerStatus.RemoveLife () nel PlayerShip.Kill (). Se il giocatore perde tutte le loro vite, chiama PlayerStatus.Reset () per resettare il proprio punteggio e vivere all'inizio di una nuova partita.

Miglior punteggio

Aggiungiamo la possibilità per il gioco di tracciare il tuo punteggio migliore. Vogliamo che questo punteggio persista attraverso le riproduzioni, quindi lo salveremo in un file. Lo terremo molto semplice e conserveremo il punteggio più alto come un singolo numero di testo normale in un file nella directory di lavoro corrente (questa sarà la stessa directory che contiene il gioco .EXE file).

Aggiungere i seguenti metodi a PlayerStatus:

 stringa const privata highScoreFilename = "highscore.txt"; private static int LoadHighScore () // restituisce il punteggio elevato salvato se possibile e restituisce 0 altrimenti punteggio int; return File.Exists (highScoreFilename) && int.TryParse (File.ReadAllText (highScoreFilename), out score)? punteggio: 0;  private static void SaveHighScore (int score) File.WriteAllText (highScoreFilename, score.ToString ()); 

Il LoadHighScore () il metodo controlla prima che esista il file con punteggio più alto, quindi verifica che contenga un numero intero valido. Molto probabilmente il secondo controllo non fallirà mai a meno che l'utente modifichi manualmente il file di punteggio elevato su qualcosa di non valido, ma è bene essere prudenti.

Vogliamo caricare il punteggio più alto all'avvio del gioco e salvarlo quando il giocatore ottiene un nuovo punteggio elevato. Modificheremo il costruttore statico e Reset() metodi in PlayerStatus fare così. Aggiungeremo anche una proprietà helper, IsGameOver che useremo in un momento.

 public static bool IsGameOver get return Lives == 0;  static PlayerStatus () HighScore = LoadHighScore (); Reset();  public static void Reset () if (Punteggio> HighScore) SaveHighScore (HighScore = Score); Punteggio = 0; Moltiplicatore = 1; Vive = 4; scoreForExtraLife = 2000; multiplierTimeLeft = 0; 

Questo si occupa di tracciare il punteggio più alto. Ora dobbiamo mostrarlo. Aggiungere il seguente codice a GameRoot.Draw () nello stesso SpriteBatch blocca dove viene disegnato l'altro testo:

 if (PlayerStatus.IsGameOver) string text = "Game Over \ n" + "Punteggio:" + PlayerStatus.Score + "\ n" + "Punteggio più alto:" + PlayerStatus.HighScore; Vector2 textSize = Art.Font.MeasureString (testo); spriteBatch.DrawString (Art.Font, text, ScreenSize / 2 - textSize / 2, Color.White); 

Questo farà visualizzare il tuo punteggio e il punteggio più alto al termine del gioco, centrato sullo schermo.

Come aggiustamento finale, aumenteremo il tempo prima che la nave si rigenerino al termine del gioco per dare al giocatore il tempo di vedere il proprio punteggio. Modificare PlayerShip.Kill () impostando il tempo di respawn a 300 fotogrammi (cinque secondi) se il giocatore ha esaurito le vite.

 // in PlayerShip.Kill () PlayerStatus.RemoveLife (); framesUntilRespawn = PlayerStatus.IsGameOver? 300: 120;

Il gioco è ora pronto per giocare. Potrebbe non sembrare molto, ma ha implementato tutte le meccaniche di base. Nei prossimi tutorial aggiungeremo un filtro bloom e effetti particellari per renderlo più piccante. Ma in questo momento, aggiungiamo rapidamente suoni e musica per renderlo più interessante.


Suono e musica

Riprodurre suoni e musica è facile in XNA. Per prima cosa, aggiungiamo i nostri effetti sonori e musica alla pipeline dei contenuti. Nel Proprietà riquadro, assicurarsi che il processore del contenuto sia impostato su Canzone per la musica e Effetto sonoro per i suoni.

Successivamente, creiamo una classe helper statica per i suoni.

 classe statica Suono public static Song Music get; set privato;  statico privato readonly Random rand = new Random (); esplosioni statiche private SoundEffect []; // restituisce un suono esplosione casuale statico pubblico SoundEffect Explosion get return explosions [rand.Next (explosions.Length)];  colpi statici privati ​​SoundEffect []; public static SoundEffect Shot get return shots [rand.Next (shots.Length)];  statico privato SoundEffect [] spawns; public static SoundEffect Spawn get return spawns [rand.Next (spawns.Length)];  public static void Load (contenuto ContentManager) Music = content.Load( "Sound / Music"); // Queste espressioni di linq sono solo un modo stravagante per caricare tutti i suoni di ogni categoria in un array. explosions = Enumerable.Range (1, 8) .Select (x => content.Load("Suono / esplosione-0" + x)). ToArray (); shots = Enumerable.Range (1, 4) .Select (x => content.Load("Sound / shoot-0" + x)). ToArray (); spawns = Enumerable.Range (1, 8) .Seleziona (x => content.Load("Suono / spawn-0" + x)). ToArray (); 

Dal momento che abbiamo più variazioni di ogni suono, il Esplosione, Tiro, e uova le proprietà selezioneranno un suono a caso tra le varianti.

Chiamata Sound.load () nel GameRoot.LoadContent (). Per riprodurre la musica, aggiungi le seguenti due righe alla fine di GameRoot.Initialize ().

 MediaPlayer.IsRepeating = true; MediaPlayer.Play (Sound.Music);

Per riprodurre suoni in XNA, puoi semplicemente chiamare il Giocare() metodo su a Effetto sonoro. Questo metodo fornisce anche un sovraccarico che consente di regolare il volume, l'intonazione e il pan del suono. Un trucco per rendere i nostri suoni più vari è regolare queste quantità su ogni gioco.

Per attivare l'effetto sonoro per la ripresa, aggiungi la seguente riga in PlayerShip.Update (), all'interno dell'istruzione if in cui vengono creati i punti elenco. Nota che spostiamo casualmente il pitch in su o in giù, fino a un quinto di un'ottava, per rendere i suoni meno ripetitivi.

 Sound.Shot.Play (0.2f, rand.NextFloat (-0.2f, 0.2f), 0);

Allo stesso modo, innesca un effetto sonoro esplosivo ogni volta che un nemico viene distrutto aggiungendo il seguente a Enemy.WasShot ().

 Sound.Explosion.Play (0.5f, rand.NextFloat (-0.2f, 0.2f), 0);

Ora hai suoni e musica nel tuo gioco. Facile, no??


Conclusione

Questo avvolge le meccaniche di base del gameplay. Nel prossimo tutorial, aggiungeremo un filtro a fioritura per far risplendere le luci al neon.