Lag en Neon Vector Shooter i XNA Mer spill

I denne serien av opplæringsprogrammer, viser jeg deg hvordan du lager en neon tvillingpinne shooter, som Geometry Wars, i XNA. Målet med disse opplæringsprogrammene er ikke å forlate deg med en nøyaktig kopi av Geometry Wars, men heller å gå over de nødvendige elementene som vil tillate deg å lage din egen variant av høy kvalitet.


Oversikt

I denne delen vil vi bygge videre på den forrige opplæringen ved å legge til fiender, kollisjonssporing og scoring.

Her er hva vi skal ha på slutten av det:

Advarsel: Høyt!

Vi vil legge til følgende nye klasser for å håndtere dette:

  • Fiende
  • EnemySpawner: Ansvarlig for å skape fiender og gradvis øke spillets vanskeligheter.
  • PlayerStatus: Sporer spillernes poengsum, høy poengsum og liv.

Du har kanskje lagt merke til at det er to typer fiender i videoen, men det er bare ett Fiende klasse. Vi kunne utlede underklasser fra Fiende for hver fiendtlig type. Imidlertid foretrekker jeg å unngå dype klassehierarkier fordi de har noen ulemper:

  • De legger til mer boilerplate kode.
  • De kan øke kodenes kompleksitet og gjøre det vanskeligere å forstå. Statens tilstand og funksjonalitet blir spredt over hele arvskjeden.
  • De er ikke veldig fleksible. Du kan ikke dele deler av funksjonalitet mellom forskjellige grener av arvstreet hvis den funksjonaliteten ikke er i grunnklassen. For eksempel, vurder å lage to klasser, Pattedyr og Fugl, som begge kommer fra Dyr. De Fugl klassen har a Fly() metode. Da bestemmer du å legge til en Flaggermus klasse som kommer fra Pattedyr og kan også fly. For å dele denne funksjonaliteten bare ved bruk av arv, må du flytte Fly() metode til Dyr klassen der den ikke tilhører. I tillegg kan du ikke fjerne metoder fra avledede klasser, så hvis du gjorde en Pingvin klasse som er avledet fra Fugl, det ville også hatt en Fly() metode.

For denne opplæringen vil vi favorisere komposisjon over arv for å implementere ulike typer fiender. Vi vil gjøre dette ved å skape ulike, gjenbrukbare atferd som vi kan legge til fiender. Vi kan så enkelt blande og matche atferd når vi lager nye typer fiender. For eksempel, hvis vi allerede hadde en FollowPlayer oppførsel og a DodgeBullet atferd, vi kunne lage en ny fiende som gjør begge bare ved å legge til begge deler.

Relaterte innlegg
  • Intro til Objektorientert programmering for spillutvikling
  • En pragmatisk tilnærming til entitetskomposisjon

fiender

Fiender vil ha noen ekstra egenskaper over enheter. For å gi spilleren litt tid til å reagere, vil vi få fiender til å forsvinne gradvis før de blir aktive og farlige.

La oss kode den grunnleggende strukturen til Fiende klasse.

 klasse fiend: Entitet privat int timeUntilStart = 60; offentlig 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;  

Denne koden vil gjøre fiender fade inn i 60 rammer og vil tillate at hastigheten deres fungerer. Multiplikasjon av hastigheten med 0,8 gir en friksjonslignende effekt. Hvis vi får fiender til å akselerere i konstant hastighet, vil denne friksjonen føre til at de jevnt nærmer seg en maksimal hastighet. Jeg liker enkelheten og glattheten til denne typen friksjon, men du vil kanskje bruke en annen formel, avhengig av hvilken effekt du vil ha.

De Ble skutt() Metoden vil bli kalt når fienden blir skutt. Vi legger til mer på det senere i serien.

Vi vil ha forskjellige typer fiender til å oppføre oss annerledes. Vi oppnår dette ved å tilordne seg atferd. En oppførsel vil bruke noen tilpasset funksjon som kjører hver ramme for å kontrollere fienden. Vi implementerer oppførselen ved hjelp av en iterator.

Iteratorer (også kalt generatorer) i C # er spesielle metoder som kan stoppe delvis og senere gjenoppta hvor de sluttet. Du kan lage en iterator ved å lage en metode med en returtype av IEnumerable <> og bruk avkastningsordet hvor du vil at den skal returnere og senere fortsette. Iteratorer i C # krever at du returnerer noe når du gir. Vi trenger egentlig ikke å returnere noe, så våre iteratorer vil bare gi null.

Vår enkleste oppførsel vil være FollowPlayer () oppførsel som vises nedenfor.

 IEnumerable FollowPlayer (float acceleration = 1f) mens (true) Velocity + = (PlayerShip.Instance.Position - Position) .ScaleTo (akselerasjon); hvis (Velocity! = Vector2.Zero) Orientering = Velocity.ToAngle (); avkastning 0; 

Dette gjør bare fienden akselerere mot spilleren med en konstant hastighet. Friksjonen vi la til tidligere, sørger for at den til slutt toppes ut med litt maks hastighet (5 piksler per ramme når akselerasjonen er 1 siden \ (0,8 \ ganger 5 + 1 = 5 \)). Hver ramme, denne metoden vil løpe til den treffer avkastningsoppgaven og vil da gjenoppta hvor den sluttet neste ramme.

Du lurer kanskje på hvorfor vi plaget med iteratorer i det hele tatt, siden vi kunne ha gjort den samme oppgaven lettere med en enkel delegat. Bruke iteratorer lønner seg med mer komplekse metoder som ellers ville kreve at vi lagret tilstand i medlemsvariabler i klassen.

For eksempel, nedenfor er en oppførsel som gjør en fiende flytte i et firkantet mønster:

 IEnumerable MoveInASquare () const int framesPerSide = 30; mens (true) // flytte rett for 30 rammer for (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;   

Det som er fint med dette er at det ikke bare sparer oss noen instansvariabler, men det strukturerer også koden på en veldig logisk måte. Du kan se med en gang at fienden vil bevege seg rett, deretter ned, så igjen, da opp, og deretter gjenta. Hvis du skulle implementere denne metoden som en statlig maskin i stedet, ville kontrollstrømmen være mindre åpenbar.

La oss legge til stillasene som trengs for å gjøre atferdene fungerer. Fiender må lagre deres oppførsel, så vi vil legge til en variabel til Fiende klasse.

 privat liste> oppførsel = ny liste> ();

Merk at en oppførsel har typen IEnumerator, ikke IEnumerable. Du kan tenke på IEnumerable som mal for oppførselen og IEnumerator som løpende forekomst. De IEnumerator husker hvor vi er i oppførselen og vil hente opp hvor det gikk av når du ringer det MoveNext () metode. Hver ramme vil vi gjennomgå alle de oppføringene fienden har og ringer MoveNext () på hver av dem. Hvis MoveNext () returnerer falsk, betyr det at oppførselen er fullført, så vi bør fjerne den fra listen.

Vi legger til følgende metoder i Fiende klasse:

 Privat tomt AddBehaviour (IEnumerable oppførsel) behaviours.Add (behaviour.GetEnumerator ());  privat void ApplyBehaviours () for (int i = 0; i < behaviours.Count; i++)  if (!behaviours[i].MoveNext()) behaviours.RemoveAt(i--);  

Og vi vil endre Oppdater() metode for å ringe ApplyBehaviours ():

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

Nå kan vi lage en statisk metode for å skape søker fiender. Alt vi trenger å gjøre er å velge bildet vi ønsker og legge til FollowPlayer () oppførsel.

 offentlig statisk Enemy CreateSeeker (Vector2 posisjon) var fiende = ny Enemy (Art.Seeker, posisjon); enemy.AddBehaviour (enemy.FollowPlayer ()); returnere fiende; 

For å lage en fiende som beveger seg tilfeldig, vil vi få den til å velge en retning og deretter lage små tilfeldige justeringer i den retningen. Men hvis vi justerer retningen hver ramme, vil bevegelsen være jitteraktig, så vi justerer kun retningen med jevne mellomrom. Hvis fienden løper inn i kanten av skjermen, vil vi få den til å velge en ny tilfeldig retning som peker bort fra veggen.

 IEnumerable MoveRandomly () float direction = rand.NextFloat (0, MathHelper.TwoPi); mens (sann) retning + = rand.NextFloat (-0,1f, 0,1f); retning = MathHelper.WrapAngle (retning); for (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;   

Vi kan nå lage en fabrikkmetode for å skape vandrende fiender, akkurat som vi gjorde for søkeren:

 offentlig statisk Enemy CreateWanderer (Vector2 posisjon) var fiende = ny Enemy (Art.Wanderer, posisjon); enemy.AddBehaviour (enemy.MoveRandomly ()); returnere fiende; 

Kollisjonsdeteksjon

For kollisjonsdeteksjon modellerer vi spillerens skip, fiender og kuler som sirkler. Sirkulær kollisjonsdeteksjon er fin fordi det er enkelt, det er raskt, og det endres ikke når objektene roterer. Hvis du husker, Entity klassen har en radius og en posisjon (posisjonen refererer til sentrum av enheten). Dette er alt vi trenger for sirkulær kollisjonsdeteksjon.

Testing av hver enhet mot alle andre enheter som potensielt kan kollidere kan være veldig sakte hvis du har et stort antall enheter. Det er mange teknikker du kan bruke til å øke hastigheten på bråkfasekollisjonsdeteksjon, som quadtrees, feie og beskjære og BSP-trær. Men for nå vil vi bare ha et par dusin enheter på skjermen av gangen, så vi vil ikke bekymre deg for disse mer komplekse teknikkene. Vi kan alltid legge dem til senere hvis vi trenger dem.

I Shape Blaster kan ikke alle enheter kollidere med alle andre typer enheter. Kuler og spillerens skip kan bare kollidere med fiender. Fiender kan også kollidere med andre fiender - dette vil hindre at de overlapper.

For å håndtere disse forskjellige typer kollisjoner, legger vi til to nye lister til EntityManager å holde styr på kuler og fiender. Når vi legger til en enhet i EntityManager, Vi vil legge det til den aktuelle listen, så vi skal lage en privat AddEntity () metode for å gjøre det. Vi vil også være sikker på å fjerne utgåtte enheter fra alle listene hver ramme.

 statisk liste fiender = ny liste(); statisk liste kuler = ny liste(); privat statisk tomrom AddEntity (Entity entity) entities.Add (enhet); hvis (enhet er Bullet) bullets.Add (enhet som Bullet); ellers hvis (enhet er Enemy) enemies.Add (enhet som Enemy);  // // i // Update () bullets = bullets.Where (x =>! X.IsExpired) .ToList (); fiender = enemies.Where (x =>! x.IsExpired) .ToList ();

Bytt anrop til entity.Add () i EntityManager.Add () og EntityManager.Update () med anrop til AddEntity ().

La oss nå legge til en metode som vil avgjøre om to enheter kolliderer:

 privat statisk bool IsColliding (Entitet a, enhet b) float radius = a.Radius + b.Radius; returnere! a.IsExpired &&! b.IsExpired && Vector2.DistanceSquared (a.Position, b.Position) < radius * radius; 

For å finne ut om to sirkler overlapper, må du bare sjekke om avstanden mellom dem er mindre enn summen av deres radius. Metoden optimaliserer dette litt ved å sjekke om kvadratet av avstanden er mindre enn kvadratet av summen av radien. Husk at det er litt raskere å beregne avstanden som er kvadret enn den faktiske avstanden.

Ulike ting vil skje, avhengig av hvilke to gjenstander som kolliderer. Hvis to fiender kolliderer, vil vi at de skal presse hverandre unna. Hvis en kule treffer en fiende, skal både kula og fiende bli ødelagt. Hvis spilleren berører en fiende, skal spilleren dø og nivået skal nullstilles.

Vi legger til en HandleCollision () metode til Fiende klasse for å håndtere sammenstøt mellom fiender:

 Offentlig tomgang HandleCollision (Enemy andre) var d = Posisjon - annen.Posisjon; Hastighet + = 10 * d / (d.LengthSquared () + 1); 

Denne metoden vil presse den nåværende fienden bort fra den andre fienden. Jo nærmere de er, jo vanskeligere vil det bli presset, fordi størrelsen på (d / d.LengthSquared ()) er bare en over avstanden.

Responere spilleren

Deretter trenger vi en metode for å håndtere spillerens skip som blir drept. Når dette skjer, forsvinner spillers skip i en kort tid før de respekteres.

Vi starter med å legge to nye medlemmer til PlayerShip.

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

I begynnelsen av PlayerShip.Update (), legg til følgende:

 hvis (IsDead) framesUntilRespawn--; komme tilbake; 

Og vi tilsidesetter Tegne() som vist:

 Offentlig overstyring ugyldig Tegn (SpriteBatch spriteBatch) if (! IsDead) base. Draw (spriteBatch); 

Til slutt legger vi til en Drepe() metode til PlayerShip.

 Offentlig tomrom Kill () framesUntilRespawn = 60; 

Nå som alle stykkene er på plass, legger vi til en metode for EntityManager som går gjennom alle enhetene og sjekker for kollisjoner.

 statisk tomrom HandleCollisions () // håndter kollisjoner mellom fiender for (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 ()); gå i stykker; 

Ring denne metoden fra Oppdater() umiddelbart etter innstillingen isUpdating til ekte.


Enemy Spawner

Den siste tingen å gjøre er å lage EnemySpawner klassen, som er ansvarlig for å skape fiender. Vi vil at spillet skal begynne enkelt og bli vanskeligere, så EnemySpawner vil skape fiender i økende grad etter hvert som tiden går. Når spilleren dør, vil vi nullstille EnemySpawner til den første vanskeligheten.

 statisk klasse EnemySpawner static Tilfeldig rand = ny Tilfeldig (); statisk float inverseSpawnChance = 60; offentlig statisk ugyldig oppdatering () 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;  privat statisk Vector2 GetSpawnPosition () Vector2 pos; gjør pos = ny Vector2 (rand.Next ((int) GameRoot.ScreenSize.X), rand.Next ((int) GameRoot.ScreenSize.Y));  mens (Vector2.DistanceSquared (pos, PlayerShip.Instance.Position) < 250 * 250); return pos;  public static void Reset()  inverseSpawnChance = 60;  

Hver ramme, det er en i inverseSpawnChance å generere hver type fiende. Sjansen for å gyte en fiende øker gradvis til den når maksimalt en på tjue. Fiender er alltid opprettet minst 250 piksler unna spilleren.

Vær forsiktig med mens sløyfen er i GetSpawnPosition (). Det vil fungere effektivt så lenge området der fiender kan gyte er større enn området der de ikke kan gyte. Men hvis du gjør det forbudte området for stort, vil du få en uendelig sløyfe.

Anrop EnemySpawner.Update () fra GameRoot.Update () og ring EnemySpawner.Reset () når spilleren blir drept.


Poengsum og liv

I Shape Blaster begynner du med fire liv, og vil få et ekstra liv hvert 2000 poeng. Du mottar poeng for å ødelegge fiender, med forskjellige typer fiender som er verdt forskjellige mengder poeng. Hver fiende ødelagt øker også poengmultiplikatoren din med en. Hvis du ikke dreper noen fiender innen kort tid, blir multiplikatoren nullstillet. Den totale mengden poeng mottatt fra hver fiende du ødelegger er antall poeng fienden er verdt multiplisert med din nåværende multiplikator. Hvis du mister alle dine liv, er spillet over, og du starter et nytt spill med nullstillingen din til null.

For å håndtere alt dette, vil vi lage en statisk klasse kalt PlayerStatus.

 statisk klasse PlayerStatus // tid det tar, i sekunder, for en multiplikator å utløpe. private const float multiplikatorExpiryTime = 0,8f; privat const int maxMultiplier = 20; offentlig statisk int Lever get; privat sett;  statisk statisk int Resultat get; privat sett;  statisk statisk int Multiplikator get; privat sett;  privat statisk float multiplikatorTimeLeft; // tid til gjeldende multiplikator utløper privat statisk int scoreForExtraLife; // poeng kreves for å få et ekstra liv // Statisk konstruktør statisk PlayerStatus () Reset ();  Statisk statisk tomt Tilbakestill () Score = 0; Multiplikator = 1; Lever = 4; scoreForExtraLife = 2000; multiplierTimeLeft = 0;  offentlig statisk ugyldig oppdatering () if (Multiplikator> 1) // oppdater multiplikator timeren hvis ((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; Bor ++;  offentlig statisk tomrom IncreaseMultiplier () hvis (PlayerShip.Instance.IsDead) returnerer; multiplierTimeLeft = multiplikatorExpiryTime; hvis (Multiplikator < maxMultiplier) Multiplier++;  public static void ResetMultiplier()  Multiplier = 1;  public static void RemoveLife()  Lives--;  

Anrop PlayerStatus.Update () fra GameRoot.Update () når spillet ikke er pauset.

Deretter ønsker vi å vise poengsum, liv og multiplikator på skjermen. For å gjøre dette må vi legge til en SpriteFont i Innhold prosjekt og en tilsvarende variabel i Kunst klassen, som vi vil nevne Font. Legg inn skrifttypen i Art.Load () som vi gjorde med teksturer.

Merk: Det er en font som heter Nova Square, inkludert i Shape Blaster-kildefilene du kan bruke. For å bruke skriften må du først installere den og deretter starte Visual Studio om den var åpen. Du kan da endre skrifttypenavnet i sprite skriftfilen til "Nova Square". Demo-prosjektet bruker ikke denne fonten som standard fordi det forhindrer prosjektet i å kompilere hvis skrifttypen ikke er installert.

Endre slutten av GameRoot.Draw () hvor markøren er trukket som vist nedenfor.

 spriteBatch.Begin (0, BlendState.Additive); spriteBatch.DrawString (Art.Font, "Lives:" + PlayerStatus.Lives, ny Vector2 (5), Color.White); DrawRightAlignedString ("Score:" + PlayerStatus.Score, 5); DrawRightAlignedString ("Multiplikator:" + PlayerStatus.Multiplier, 35); // tegne egendefinerte musepekeren spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();

DrawRightAlignedString () er en hjelpemetode for å tegne tekstjustert på høyre side av skjermen. Legg det til GameRoot ved å legge til koden nedenfor.

 privat tomt DrawRightAlignedString (streng tekst, float y) var textWidth = Art.Font.MeasureString (text) .X; spriteBatch.DrawString (Art.Font, tekst, ny Vector2 (ScreenSize.X - textWidth - 5, y), Color.White); 

Nå skal dine liv, poeng og multiplikator vises på skjermen. Imidlertid må vi stadig endre disse verdiene som svar på spillbegivenheter. Legg til en eiendom som heter PointValue til Fiende klasse.

 offentlig int PointValue get; privat sett; 

Sett poengverdien for forskjellige fiender til noe du føler er passende. Jeg gjorde de vandrende fiender verdt et poeng, og de søker fiender verdt to poeng.

Deretter legger du til følgende to linjer til Enemy.WasShot () for å øke spillerens poengsum og multiplikator:

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

Anrop PlayerStatus.RemoveLife () i PlayerShip.Kill (). Hvis spilleren mister hele livet, ring PlayerStatus.Reset () å tilbakestille sin poengsum og lever i starten av et nytt spill.

Rekorder

La oss legge til evnen til spillet til å spore din beste poengsum. Vi vil at denne poengsummen skal fortsette over spillene, så vi lagrer den til en fil. Vi vil holde det veldig enkelt og lagre høy poengsum som et enkelt ren tekstnummer i en fil i den nåværende arbeidskatalogen (dette vil være den samme katalogen som inneholder spillets .exe fil).

Legg til følgende metoder for PlayerStatus:

 privat const streng highScoreFilename = "highscore.txt"; privat statisk int LoadHighScore () // returner lagret høy poengsum hvis mulig og returner 0 ellers int score; returner File.Exists (highScoreFilename) && int.TryParse (File.ReadAllText (highScoreFilename), ut score)? poeng: 0;  privat statisk tomt SaveHighScore (int score) File.WriteAllText (highScoreFilename, score.ToString ()); 

De LoadHighScore () Metoden kontrollerer først at high score-filen eksisterer, og kontrollerer deretter at den inneholder et gyldig heltall. Den andre kontrollen vil sannsynligvis aldri mislykkes, med mindre brukeren manuelt redigerer high score-filen til noe ugyldig, men det er godt å være forsiktig.

Vi ønsker å laste høy poengsum når spillet starter, og lagre det når spilleren får en ny høy poengsum. Vi vil endre den statiske konstruktøren og Tilbakestille() metoder i PlayerStatus å gjøre slik. Vi legger også til en hjelperegenskap, IsGameOver som vi skal bruke på et øyeblikk.

 offentlig statisk bool IsGameOver get return Lives == 0;  statisk PlayerStatus () HighScore = LoadHighScore (); Tilbakestille();  Statisk statisk tomt Tilbakestill () hvis (Score> HighScore) SaveHighScore (HighScore = Score); Resultat = 0; Multiplikator = 1; Lever = 4; scoreForExtraLife = 2000; multiplierTimeLeft = 0; 

Det tar seg av å spore høy poengsum. Nå må vi vise den. Legg til følgende kode til GameRoot.Draw () i samme SpriteBatch blokkere hvor den andre teksten er tegnet:

 hvis (PlayerStatus.IsGameOver) string text = "Spill over \ n" + "Ditt poeng:" + PlayerStatus.Score + "\ n" + "High Score:" + PlayerStatus.HighScore; Vector2 textSize = Art.Font.MeasureString (tekst); spriteBatch.DrawString (Art.Font, tekst, ScreenSize / 2 - textSize / 2, Color.White); 

Dette vil gjøre det vise poengsummen og høy poengsum på spillet over, sentrert på skjermen.

Som en endelig justering øker vi tiden før skipet responwns på spillet over for å gi spilleren tid til å se sin score. endre PlayerShip.Kill () ved å sette responstiden til 300 bilder (fem sekunder) hvis spilleren er ute av livet.

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

Spillet er nå klar til å spille. Det kan ikke se ut som mye, men det har alle grunnleggende mekanikkene implementert. I fremtidige opplæringsprogrammer vil vi legge til et blomstfilter og partikkeleffekter for å krydre det opp. Men akkurat nå, la oss raskt legge til litt lyd og musikk for å gjøre det mer interessant.


Lyd og musikk

Å spille lyd og musikk er enkelt i XNA. Først legger vi til våre lydeffekter og musikk til innholdsrørledningen. I Eiendommer ruten, sørg for at innholdsprosessoren er satt til Sang for musikken og Lyd effekt for lydene.

Deretter lager vi en statisk hjelpeklasse for lydene.

 statisk klasse Lyd offentlig statisk sangmusikk get; privat sett;  privat statisk avlesning Tilfeldig rand = ny tilfeldig (); private statiske SoundEffect [] eksplosjoner; // returnere en tilfeldig eksplosjonslyd offentlig statisk SoundEffect Explosion get return eksplosjoner [rand.Next (explosions.Length)];  private statiske SoundEffect [] skudd; offentlig statisk SoundEffect Shot get return shots [rand.Next (shots.Length)];  Private statiske SoundEffect [] spawns; offentlig statisk SoundEffect Spawn get return spawns [rand.Next (spawns.Length)];  Statisk statisk tomgang Load (ContentManager innhold) Music = content.Load( "Lyd / Musikk"); // Disse linq-uttrykkene er bare en fancy måte å laste alle lydene av hver kategori inn i en matrise. eksplosjoner = Enumerable.Range (1, 8) .Velg (x => content.Load("Lyd / eksplosjon-0" + x)). ToArray (); shots = Enumerable.Range (1, 4) .Velg (x => content.Load("Sound / shoot-0" + x)). ToAray (); spawns = Enumerable.Range (1, 8) .Velg (x => content.Load("Sound / spawn-0" + x)). ToAray (); 

Siden vi har flere varianter av hver lyd, vil Eksplosjon, Skudd, og gyte egenskaper vil velge en lyd tilfeldig blant varianter.

Anrop Sound.Load () i GameRoot.LoadContent (). For å spille av musikken, legg til følgende to linjer på slutten av GameRoot.Initialize ().

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

For å spille lyder i XNA, kan du bare ringe Spille() metode på en Lyd effekt. Denne metoden gir også en overbelastning som gjør at du kan justere volumet, tonehøyde og panne av lyden. Et triks for å gjøre lydene våre mer varierte, er å justere disse mengdene på hvert spill.

For å utløse lydeffekten for opptak, legg til følgende linje inn PlayerShip.Update (), inne i if-setningen hvor kulene er opprettet. Legg merke til at vi tilfeldigvis skifter banen opp eller ned, opp til en femtedel av en oktav, for å gjøre lydene mindre repeterende.

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

Likeledes utløse en eksplosjonslyd effekt hver gang en fiende blir ødelagt ved å legge til følgende Enemy.WasShot ().

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

Du har nå lyd og musikk i spillet ditt. Lett, er det ikke?


Konklusjon

Det bryter opp den grunnleggende spillmekanikken. I neste opplæring legger vi til et blomstfilter for å få neonlysene til å lyse.