Maak een Neon Vector Shooter in XNA meer gameplay

In deze serie tutorials laat ik je zien hoe je een neon-tweepijps-schietspel maakt, zoals Geometry Wars, in XNA. Het doel van deze tutorials is om je niet te laten met een exacte replica van Geometry Wars, maar om de nodige elementen door te nemen die je in staat stellen om je eigen hoogwaardige variant te maken.


Overzicht

In dit deel bouwen we verder op de vorige tutorial door vijanden toe te voegen, botsingsdetectie en scoren.

Dit is wat we zullen hebben aan het einde ervan:

Waarschuwing: Loud!

We zullen de volgende nieuwe klassen toevoegen om dit te behandelen:

  • Vijand
  • EnemySpawner: Verantwoordelijk voor het maken van vijanden en het geleidelijk aan vergroten van de moeilijkheidsgraad van de game.
  • PlayerStatus: Volgt de score van de speler, hoge score en levens.

Je hebt misschien gemerkt dat er twee soorten vijanden in de video zitten, maar er is er maar één Vijand klasse. We kunnen subklassen afleiden Vijand voor elk type vijand. Ik mis echter de voorkeur voor deep class-hiërarchieën omdat ze een aantal nadelen hebben:

  • Ze voegen meer boilerplate code toe.
  • Ze kunnen de complexiteit van de code vergroten en het moeilijker maken om te begrijpen. De status en functionaliteit van een object wordt verspreid over de gehele overervingsketen.
  • Ze zijn niet erg flexibel. U kunt geen delen van functionaliteit delen tussen verschillende takken van de overervingsboom als die functionaliteit niet in de basisklasse voorkomt. Overweeg bijvoorbeeld om twee klassen te maken, Zoogdier en Vogel, waaruit beide voortkomen Dier. De Vogel klasse heeft een Vlieg() methode. Dan besluit u om een ​​toe te voegen Knuppel klasse die is afgeleid van Zoogdier en kan ook vliegen. Als u deze functionaliteit alleen via overerving wilt delen, moet u de Vlieg() methode om de Dier klas waar het niet thuishoort. Bovendien kunt u methoden niet verwijderen uit afgeleide klassen, dus als u een pinguïn klasse die is afgeleid van Vogel, het zou ook een Vlieg() methode.

Voor deze zelfstudie hebben we de voorkeur voor compositie ten opzichte van overerving voor het implementeren van de verschillende soorten vijanden. We zullen dit doen door verschillende, herbruikbare gedragingen te creëren die we aan vijanden kunnen toevoegen. We kunnen dan gemakkelijk gedrag combineren en matchen wanneer we nieuwe typen vijanden maken. Als we bijvoorbeeld al een FollowPlayer gedrag en a DodgeBullet gedrag, kunnen we een nieuwe vijand maken die beide doet door beide gedragingen toe te voegen.

gerelateerde berichten
  • Inleiding tot object-georiënteerde programmering voor spelontwikkeling
  • Een pragmatische benadering van entiteitssamenstelling

vijanden

Vijanden hebben een paar extra eigenschappen ten opzichte van entiteiten. Om de speler de tijd te geven om te reageren, zullen we vijanden geleidelijk doen vervagen voordat ze actief en gevaarlijk worden.

Laten we de basisstructuur van de code coderen Vijand klasse.

 class Enemy: Entity private int timeUntilStart = 60; public bool IsActive krijg 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;  

Deze code zorgt ervoor dat vijanden 60 frames vervagen en hun snelheid laten werken. Het vermenigvuldigen van de snelheid met 0,8 maakt een wrijvingsachtig effect. Als we vijanden met een constante snelheid laten accelereren, zorgt deze wrijving ervoor dat ze soepel een maximale snelheid naderen. Ik hou van de eenvoud en zachtheid van dit soort wrijving, maar misschien wil je een andere formule gebruiken, afhankelijk van het effect dat je wilt.

De Was doodgeschoten() methode wordt aangeroepen wanneer de vijand wordt neergeschoten. We zullen er later in de serie meer aan toevoegen.

We willen dat verschillende soorten vijanden zich anders gedragen. We zullen dit bereiken door gedrag toe te wijzen. Een gedrag gebruikt een aangepaste functie die elk frame uitvoert om de vijand te besturen. We zullen het gedrag implementeren met behulp van een iterator.

Iterators (ook wel generators genoemd) in C # zijn speciale methoden die halverwege kunnen stoppen en later kunnen hervatten waar ze gebleven waren. U kunt een iterator maken door een methode te maken met een retourtype van IEnumerable <> en het gebruik van het sleutelwoord waarmee u het rendement wilt laten terugkeren en later wilt hervatten. Iterators in C # vereisen dat u iets retourneert wanneer u opgeeft. We hoeven niet echt iets terug te sturen, dus onze iterators zullen eenvoudig nul opleveren.

Ons eenvoudigste gedrag is de FollowPlayer () gedrag hieronder weergegeven.

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

Dit zorgt er simpelweg voor dat de vijand in een constant tempo naar de speler accelereert. De wrijving die we eerder hebben toegevoegd, zorgt ervoor dat deze uiteindelijk een maximum snelheid haalt (5 pixels per frame wanneer versnelling 1 is sinds \ (0.8 \ keer 5 + 1 = 5 \)). Bij elk frame wordt deze methode uitgevoerd totdat deze de yield-instructie bereikt en vervolgens verdergaat waar deze het volgende frame heeft verlaten.

U vraagt ​​zich misschien af ​​waarom we überhaupt last hebben van iterators, omdat we dezelfde taak gemakkelijker hadden kunnen volbrengen met een eenvoudige afgevaardigde. Het gebruik van iterators loont met complexere methoden die anders vereisen dat we de status opslaan in lidvariabelen in de klasse.

Hieronder is bijvoorbeeld een gedrag dat een vijand in een vierkant patroon laat bewegen:

 IEnumerable MoveInASquare () const int framesPerSide = 30; while (true) // ga naar rechts voor 30 frames voor (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;   

Het leuke hieraan is dat het ons niet alleen enkele instantievariabelen bespaart, maar ook de code op een heel logische manier structureert. Je kunt meteen zien dat de vijand rechts gaat, dan naar beneden, dan naar links, dan omhoog en dan herhaalt. Als u deze methode als een staatsmachine zou implementeren, zou de regelstroom minder voor de hand liggen.

Laten we de steigers toevoegen die nodig zijn om gedrag te laten werken. Vijanden moeten hun gedrag opslaan, dus we zullen een variabele toevoegen aan de Vijand klasse.

 privélijst> behaviors = new Lijst> ();

Merk op dat een gedraging het type heeft IEnumerator, niet IEnumerable. Je kunt denken aan de IEnumerable als de sjabloon voor het gedrag en de IEnumerator als de lopende instantie. De IEnumerator onthoudt waar we ons in het gedrag bevinden en zal verdergaan waar het gebleven was toen je het opriep MoveNext () methode. In elk frame zullen we alle gedragingen doornemen die de vijand heeft en oproepen MoveNext () op elk van hen. Als MoveNext () geeft false terug, dit betekent dat het gedrag is voltooid, dus we moeten het van de lijst verwijderen.

We zullen de volgende methoden toevoegen aan de Vijand klasse:

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

En we zullen de Bijwerken() methode om te bellen ApplyBehaviours ():

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

Nu kunnen we een statische methode maken om op zoek te gaan naar vijanden. Het enige dat we moeten doen is de gewenste afbeelding kiezen en de afbeelding toevoegen FollowPlayer () gedrag.

 public static Enemy CreateSeeker (Vector2-positie) var enemy = new Enemy (Art.Seeker, position); enemy.AddBehaviour (enemy.FollowPlayer ()); keer vijand terug; 

Om een ​​vijand te maken die willekeurig beweegt, laten we hem een ​​richting kiezen en dan kleine willekeurige aanpassingen in die richting maken. Als we echter de richting van elk frame aanpassen, wordt de beweging schokkerig, dus we passen de richting alleen periodiek aan. Als de vijand tegen de rand van het scherm aanloopt, laten we hem een ​​nieuwe willekeurige richting kiezen die van de muur af wijst.

 IEnumerable MoveRandomly () zweefrichting = rand.NextFloat (0, MathHelper.TwoPi); while (true) direction + = rand.NextFloat (-0.1f, 0.1f); richting = MathHelper.WrapAngle (richting); voor (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;   

We kunnen nu een fabrieksmethode maken voor zwervende vijanden, net zoals we dat deden voor de zoeker:

 public static Enemy CreateWanderer (Vector2-positie) var enemy = new Enemy (Art.Wanderer, position); enemy.AddBehaviour (enemy.MoveRandomly ()); keer vijand terug; 

Collision Detection

Voor botsingdetectie modelleren we het schip van de speler, de vijanden en de kogels als cirkels. Circulaire detectie van botsingen is leuk omdat het eenvoudig is, het is snel en het verandert niet wanneer de objecten draaien. Als je je dat herinnert, de Entiteit klasse heeft een straal en een positie (de positie verwijst naar het midden van de entiteit). Dit is alles wat we nodig hebben voor detectie van circulaire botsingen.

Het testen van elke entiteit tegen alle andere entiteiten die mogelijk zouden kunnen botsen, kan erg traag zijn als u een groot aantal entiteiten heeft. Er zijn veel technieken die u kunt gebruiken om breedveldige botsingsdetectie te versnellen, zoals quadtrees, sweep en snoeien, en BSP-bomen. Voorlopig zullen we echter slechts een paar dozijn entiteiten tegelijk op het scherm hebben, dus we zullen ons geen zorgen maken over deze meer complexe technieken. We kunnen ze altijd later toevoegen als we ze nodig hebben.

In Shape Blaster kan niet elke entiteit botsen met elk ander type entiteit. Kogels en het schip van de speler kunnen alleen met vijanden botsen. Vijanden kunnen ook botsen met andere vijanden - dit voorkomt dat ze elkaar overlappen.

Om met deze verschillende soorten botsingen om te gaan, zullen we twee nieuwe lijsten toevoegen aan de EntityManager om kogels en vijanden te volgen. Telkens wanneer we een entiteit toevoegen aan de EntityManager, we willen het toevoegen aan de juiste lijst, dus we zullen een privé maken AddEntity () methode om dit te doen. We zullen ook zeker zijn dat alle verlopen entiteiten uit alle lijsten van elk frame worden verwijderd.

 statische lijst vijanden = nieuwe lijst(); statische lijst kogels = nieuwe lijst(); private static void AddEntity (Entity entity) entities.Add (entity); if (entity is Bullet) bullets.Add (entity as Bullet); anders als (entiteit vijanden is) vijanden. Toevoegen (entiteit als vijand);  // ... // in Update () bullets = bullets.Where (x =>! X.IsExpired) .ToList (); vijanden = vijanden. Waar (x =>! x.IsExpired) .ToList ();

Vervang de oproepen door entity.Add () in EntityManager.Add () en EntityManager.Update () met oproepen naar AddEntity ().

Laten we nu een methode toevoegen die bepaalt of twee entiteiten botsen:

 private static bool IsColliding (Entiteit a, Entiteit b) zweefradius = a.Radius + b.Radius; terugkeer! a.IsExpired &&! b.IsExpired && Vector2.DistanceSquared (a.Position, b.Position) < radius * radius; 

Om te bepalen of twee cirkels elkaar overlappen, controleert u eenvoudig of de afstand tussen hen kleiner is dan de som van hun radii. Onze methode optimaliseert dit enigszins door te controleren of het kwadraat van de afstand kleiner is dan het kwadraat van de som van de radii. Onthoud dat het een beetje sneller is om de afstand in het kwadraat te berekenen dan de werkelijke afstand.

Verschillende dingen zullen gebeuren afhankelijk van welke twee objecten botsen. Als twee vijanden tegen elkaar botsen, willen we dat ze elkaar weg duwen. Als een kogel een vijand raakt, moeten de kogel en de vijand allebei worden vernietigd. Als de speler een vijand aanraakt, moet de speler sterven en moet het niveau worden gereset.

We voegen een toe HandleCollision () methode om de Vijand klasse om botsingen tussen vijanden aan te pakken:

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

Deze methode duwt de huidige vijand weg van de andere vijand. Hoe dichterbij ze zijn, hoe moeilijker het wordt geduwd, omdat de omvang van (d / d.LengthSquared ()) is slechts één over de afstand.

Respawning the Player

Vervolgens hebben we een methode nodig om het schip van de speler te laten doden. Wanneer dit gebeurt, verdwijnt het schip van de speler voor een korte tijd voordat het opnieuw gaat duiken.

We beginnen met het toevoegen van twee nieuwe leden aan PlayerShip.

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

Aan het begin van PlayerShip.Update (), voeg het volgende toe:

 if (IsDead) framesUntilRespawn--; terug te keren; 

En we negeren Trek() zoals getoond:

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

Ten slotte voegen we een toe Doden() methode om PlayerShip.

 openbare leegte Kill () framesUntilRespawn = 60; 

Nu alle stukken op hun plaats zitten, zullen we een methode toevoegen aan de EntityManager dat gaat door alle entiteiten en controleert op botsingen.

 static void HandleCollisions () // omgaan met botsingen tussen vijanden voor (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 ()); breken; 

Noem deze methode van Bijwerken() onmiddellijk na het instellen isUpdating naar waar.


Enemy Spawner

Het laatste ding om te doen is het maken van de EnemySpawner klas, die verantwoordelijk is voor het maken van vijanden. We willen dat het spel gemakkelijk begint en harder wordt, dus het EnemySpawner zal vijanden in toenemende mate creëren naarmate de tijd vordert. Wanneer de speler overlijdt, stellen we de EnemySpawner tot zijn aanvankelijke moeilijkheid.

 static class EnemySpawner statisch Random rand = nieuw Willekeurig (); 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;  

Elk frame, er is er een in inverseSpawnChance van het genereren van elk type vijand. De kans om een ​​vijand te spawnen neemt geleidelijk toe tot een maximum van één op twintig. Vijanden worden altijd op ten minste 250 pixels van de speler verwijderd.

Wees voorzichtig met de while-lus GetSpawnPosition (). Het zal efficiënt werken zolang het gebied waarin vijanden kunnen spawnen groter is dan het gebied waar ze niet kunnen spawnen. Als je echter het verboden gebied te groot maakt, krijg je een oneindige lus.

telefoontje EnemySpawner.Update () van GameRoot.Update () en bel EnemySpawner.Reset () wanneer de speler wordt gedood.


Scoor en leef

In Shape Blaster begin je met vier levens en krijg je om de 2000 punten een extra leven. Je krijgt punten voor het vernietigen van vijanden, waarbij verschillende soorten vijanden verschillende aantallen punten waard zijn. Elke vernietigde vijand verhoogt ook je scorevermenigvuldiger met één. Als je binnen een korte tijd geen vijanden doodt, wordt je multiplier gereset. Het totale aantal punten dat je ontvangt van elke vijand die je vernietigt, is het aantal punten dat de vijand waard is vermenigvuldigd met je huidige vermenigvuldiger. Als je je hele leven verliest, is het spel afgelopen en begin je een nieuw spel waarbij je score op nul wordt teruggezet.

Om dit allemaal aan te pakken, maken we een statische klasse genaamd PlayerStatus.

 statische klasse PlayerStatus // hoeveelheid tijd die het kost, in seconden, voordat een vermenigvuldigingsfactor verloopt. private const float multiplierExpiryTime = 0.8f; private const int maxMultiplier = 20; public static int Lives get; privé set;  public static int Score get; privé set;  public static int Multiplier get; privé set;  private static float multiplierTimeLeft; // tijd totdat de huidige vermenigvuldiger de privé-statische intr-scoreForFortraLife eindigt; // score vereist om een ​​extra leven te krijgen // Statische constructor static PlayerStatus () Reset ();  public static void Reset () Score = 0; Vermenigvuldiger = 1; Levens = 4; scoreForExtraLife = 2000; multiplierTimeLeft = 0;  public static void Update () if (Multiplier> 1) // update de multiplier timer als ((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; Woont ++;  openbare static void IncreaseMultiplier () if (PlayerShip.Instance.IsDead) return; multiplierTimeLeft = multiplierExpiryTime; if (Multiplier < maxMultiplier) Multiplier++;  public static void ResetMultiplier()  Multiplier = 1;  public static void RemoveLife()  Lives--;  

telefoontje PlayerStatus.Update () van GameRoot.Update () wanneer het spel niet is gepauzeerd.

Vervolgens willen we je score, lives en multiplier op het scherm weergeven. Om dit te doen, moeten we een toevoegen SpriteFont in de Inhoud project en een bijbehorende variabele in de Kunst klas, die we zullen noemen doopvont. Laad het lettertype in Art.Load () zoals we deden met de texturen.

Notitie: Er is een lettertype met de naam Nova Square meegeleverd met de Shape Blaster-bronbestanden die u mogelijk gebruikt. Om het lettertype te gebruiken, moet u het eerst installeren en vervolgens Visual Studio opnieuw opstarten als het open was. U kunt vervolgens de naam van het lettertype in het sprite-lettertypebestand wijzigen in "Nova Square". Het demoproject gebruikt dit lettertype niet standaard omdat dit voorkomt dat het project compileert als het lettertype niet is geïnstalleerd.

Wijzig het einde van GameRoot.Draw () waar de cursor wordt getekend zoals hieronder weergegeven.

 spriteBatch.Begin (0, BlendState.Additive); spriteBatch.DrawString (Art.Font, "Lives:" + PlayerStatus.Lives, new Vector2 (5), Color.White); DrawRightAlignedString ("Score:" + PlayerStatus.Score, 5); DrawRightAlignedString ("Multiplier:" + PlayerStatus.Multiplier, 35); // teken de aangepaste muiscursor spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();

DrawRightAlignedString () is een hulpmethode voor het tekenen van tekst die aan de rechterkant van het scherm is uitgelijnd. Voeg het toe aan GameRoot door de onderstaande code toe te voegen.

 private void DrawRightAlignedString (tekenreeks, zwevend y) var textWidth = Art.Font.MeasureString (text) .X; spriteBatch.DrawString (Art.Font, tekst, nieuwe Vector2 (ScreenSize.X - textWidth - 5, y), Color.White); 

Nu moeten je levens, score en multiplier op het scherm worden weergegeven. We moeten deze waarden echter nog steeds aanpassen in reactie op game-evenementen. Voeg een eigenschap toe met de naam PointValue naar de Vijand klasse.

 public int PointValue krijg; privé set; 

Stel de puntwaarde voor verschillende vijanden in op iets waarvan u denkt dat het geschikt is. Ik maakte de zwervende vijanden één punt waard en de zoekende vijanden twee punten waard.

Voeg vervolgens de volgende twee regels toe aan Enemy.WasShot () om de score en multiplier van de speler te verhogen:

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

telefoontje PlayerStatus.RemoveLife () in PlayerShip.Kill (). Als de speler zijn hele leven verliest, bel dan PlayerStatus.Reset () om hun score opnieuw in te stellen en leeft aan het begin van een nieuw spel.

Hoge scores

Laten we de mogelijkheid voor het spel toevoegen om je beste score bij te houden. We willen dat deze score blijft behouden voor alle spelen, dus we bewaren het in een bestand. We houden het heel simpel en slaan de hoogste score op als één enkel tekstnummer in een bestand in de huidige werkdirectory (dit is dezelfde map die de game bevat .exe het dossier).

Voeg de volgende methoden toe aan PlayerStatus:

 private const string highScoreFilename = "highscore.txt"; private static int LoadHighScore () // retourneer de opgeslagen hoogste score indien mogelijk en retourneer 0 anders int score; return File.Exists (highScoreFilename) && int.TryParse (File.ReadAllText (highScoreFilename), out score)? score: 0;  private static void SaveHighScore (int score) File.WriteAllText (highScoreFilename, score.ToString ()); 

De LoadHighScore () methode controleert eerst of het bestand met de hoogste score bestaat en controleert vervolgens of het een geldig geheel getal bevat. De tweede controle zal hoogstwaarschijnlijk nooit mislukken tenzij de gebruiker het bestand met de hoogste score handmatig bewerkt tot iets ongeldig, maar het is goed om voorzichtig te zijn.

We willen de hoogste score laden wanneer het spel opstart en opslaan wanneer de speler een nieuwe hoge score krijgt. We zullen de statische constructor en wijzigen Reset () methoden in PlayerStatus om dit te doen. We voegen ook een helper-eigenschap toe, IsGameOver die we in een moment zullen gebruiken.

 public static bool IsGameOver krijg return Lives == 0;  static PlayerStatus () HighScore = LoadHighScore (); Reset ();  public static void Reset () if (Score> HighScore) SaveHighScore (HighScore = Score); Score = 0; Vermenigvuldiger = 1; Levens = 4; scoreForExtraLife = 2000; multiplierTimeLeft = 0; 

Dat zorgt voor het volgen van de hoge score. Nu moeten we het weergeven. Voeg de volgende code toe aan GameRoot.Draw () in hetzelfde SpriteBatch blok waar de andere tekst is getekend:

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

Dit zorgt ervoor dat uw score en hoge score op het spel worden weergegeven, gecentreerd op het scherm.

Als laatste aanpassing verhogen we de tijd voordat het schip opnieuw wordt uitgespeeld om de speler de tijd te geven om zijn score te zien. Wijzigen PlayerShip.Kill () door de respawn-tijd in te stellen op 300 frames (vijf seconden) als de speler geen levens meer heeft.

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

Het spel is nu klaar om te spelen. Het ziet er misschien niet zo uit, maar het heeft alle basismechanismen geïmplementeerd. In toekomstige zelfstudies zullen we een bloeifilter en deeltjeseffecten toevoegen om het op te fleuren. Maar laten we nu snel wat geluid en muziek toevoegen om het interessanter te maken.


Geluid en muziek

Geluid en muziek afspelen is gemakkelijk in XNA. Eerst voegen we onze geluidseffecten en muziek toe aan de inhoudspijplijn. In de eigenschappen venster, zorg ervoor dat de inhoudprocessor is ingesteld op lied voor de muziek en Geluidseffect voor de geluiden.

Vervolgens maken we een statische helperklasse voor de geluiden.

 static class Sound public static Song Music get; privé set;  private static readonly Random rand = nieuw Willekeurig (); privé-statische SoundEffect [] -explosies; // stuur een willekeurig explosie-geluid terug openbare static SoundEffect Explosion krijg return-explosies [rand. Next (explosions.Length)];  privé statische SoundEffect [] -opnamen; public static SoundEffect Shot krijg return shots [rand.Next (shots.Length)];  private static SoundEffect [] spawns; public static SoundEffect Spawn krijg return spawns [rand.Next (spawns.Length)];  public static void Load (inhoud ContentManager) Music = content.Load( "Sound / Music"); // Deze linq-expressies zijn gewoon een mooie manier om alle geluiden van elke categorie in een array te laden. explosions = Enumerable.Range (1, 8) .Select (x => content.Load("Geluid / explosie-0" + x)). ToArray (); shots = Enumerable.Range (1, 4) .Select (x => content.Load("Sound / shoot-0" + x)). ToArray (); spawns = Enumerable.Range (1, 8) .Select (x => content.Load("Geluid / spawn-0" + x)). ToArray (); 

Aangezien we meerdere variaties van elk geluid hebben, is de Explosie, Schot, en Paaien eigenschappen zullen willekeurig een geluid uit de varianten kiezen.

telefoontje Sound.load () in GameRoot.LoadContent (). Om de muziek af te spelen, voegt u aan het einde van de volgende twee regels toe GameRoot.Initialize ().

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

Om geluiden in XNA te spelen, kunt u gewoon de Spelen() methode op a Geluidseffect. Deze methode biedt ook een overbelasting waarmee u het volume, de toonhoogte en de pan van het geluid kunt aanpassen. Een truc om onze geluiden gevarieerder te maken, is om deze hoeveelheden bij elk spel aan te passen.

Als u het geluidseffect voor opname wilt activeren, voegt u de volgende regel toe PlayerShip.Update (), in de if-statement waar de kogels worden gemaakt. Merk op dat we willekeurig de toonhoogte omhoog of omlaag verplaatsen, tot een vijfde van een octaaf, om de geluiden minder repetitief te maken.

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

Voer op dezelfde manier een explosie-effect uit telkens wanneer een vijand wordt vernietigd door het volgende toe te voegen Enemy.WasShot ().

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

Je hebt nu geluid en muziek in je spel. Makkelijk, is het niet?


Conclusie

Dat omhult de basis gameplay mechanica. In de volgende zelfstudie voegen we een bloeifilter toe om de neonlichten te laten gloeien.