Gör en Neon Vector Shooter i XNA Mer spel

I den här serien av handledningar visar jag dig hur man gör en neon tvillingskytt, som Geometry Wars, i XNA. Målet med dessa handledningar är att inte lämna dig en exakt kopia av Geometry Wars, utan snarare att gå över de nödvändiga elementen som gör att du kan skapa din egen högkvalitativa variant.


Översikt

I denna del bygger vi på den tidigare handledningen genom att lägga till fiender, kollisionsdetektering och poäng.

Här är vad vi ska ha i slutet av det:

Varning: Högt!

Vi lägger till följande nya klasser för att hantera detta:

  • Fiende
  • EnemySpawner: Ansvarig för att skapa fiender och gradvis öka spelets svårigheter.
  • PlayerStatus: Spårar spelarens poäng, hög poäng och liv.

Du kanske har märkt att det finns två typer av fiender i videon, men det finns bara en Fiende klass. Vi kunde härleda underklasser från Fiende för varje fiende typ. Jag föredrar emellertid att undvika djupa klasshierarkier eftersom de har några nackdelar:

  • De lägger till mer boilerplate-kod.
  • De kan öka kodens komplexitet och göra det svårare att förstå. Status och funktionalitet hos ett objekt sprids ut över hela arvskedjan.
  • De är inte så flexibla. Du kan inte dela funktionsstycken mellan olika grenar av arvsträdet om den funktionen inte ligger i basklassen. Tänk på att du gör två klasser, Däggdjur och Fågel, vilka båda härledas från Djur. De Fågel klassen har a Flyga() metod. Då väljer du att lägga till en fladdermus klass som härrör från Däggdjur och kan också flyga. För att dela denna funktion med endast arv borde du behöva flytta Flyga() metod till Djur klass där den inte hör hemma. Dessutom kan du inte ta bort metoder från härledda klasser, så om du gjorde en Pingvin klass som härrör från Fågel, det skulle också ha en Flyga() metod.

För denna handledning kommer vi att gynna komposition över arv för att genomföra olika typer av fiender. Vi kommer att göra detta genom att skapa olika, återanvändbara beteenden som vi kan lägga till fiender. Vi kan då enkelt mixa och matcha beteenden när vi skapar nya typer av fiender. Till exempel, om vi redan hade en FollowPlayer beteende och a DodgeBullet beteende, vi kunde göra en ny fiende som gör det bara genom att lägga till båda beteenden.

relaterade inlägg
  • Introduktion till Objektorienterad programmering för spelutveckling
  • En pragmatisk tillvägagångssätt för entitetskompositionen

fiender

Fiender kommer att ha några ytterligare egenskaper över enheter. För att ge spelaren lite tid att reagera, kommer vi att göra fiender gradvis blekna innan de blir aktiva och farliga.

Låt oss koda grundstrukturen för Fiende klass.

 Klass Enemy: Entity 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;  

Den här koden kommer att göra fiender blekna i 60 ramar och tillåter deras hastighet att fungera. Multiplicera hastigheten med 0,8 försvinner en friktionsliknande effekt. Om vi ​​gör fiender accelererar i konstant takt, kommer denna friktion att få dem att smidigt nå en maximal hastighet. Jag gillar enkelheten och smidigheten av denna typ av friktion, men du kanske vill använda en annan formel beroende på vilken effekt du vill ha.

De Blev skjuten() Metoden kommer att kallas när fienden blir skjuten. Vi lägger till mer till det senare i serien.

Vi vill att olika typer av fiender ska agera annorlunda. Vi gör detta genom att tilldela beteenden. Ett beteende kommer att använda någon anpassad funktion som kör varje ram för att styra fienden. Vi genomför implementeringen med hjälp av en iterator.

Iteratorer (även kallade generatorer) i C # är speciella metoder som kan sluta halvvägs och senare återuppta var de slutade. Du kan göra en iterator genom att göra en metod med en returtyp av IEnumerable <> och använda avkastningsordet där du vill att den ska återvända och senare återupptas. Iteratorer i C # kräver att du returnerar något när du ger ut. Vi behöver verkligen inte returnera någonting, så våra iteratorer kommer helt enkelt ge noll.

Vårt enklaste beteende kommer att vara FollowPlayer () beteende som visas nedan.

 IEnumerable FollowPlayer (float acceleration = 1f) while (true) Velocity + = (PlayerShip.Instance.Position - Position) .ScaleTo (acceleration); om (Velocity! = Vector2.Zero) Orientering = Velocity.ToAngle (); avkastningsavkastning 0; 

Detta gör att fienden snabbt accelererar mot spelaren i en konstant takt. Friktionen som vi lagt till tidigare kommer att se till att den så småningom toppar ut med en viss maxhastighet (5 pixlar per ram när accelerationen är 1 sedan \ (0,8 \ gånger 5 + 1 = 5 \)). Varje ram, denna metod kommer att köras tills den träffar avkastningsberäkningen och sedan återupptas där den slog av nästa ram.

Du kanske undrar varför vi störde med iteratorer alls, eftersom vi lättare kunde ha utfört samma uppgift med en enkel delegat. Att använda iteratorer lönar sig med mer komplexa metoder som annars skulle kräva att vi lagrar tillstånd i medlemsvariabler i klassen.

Till exempel nedan är ett beteende som gör en fiende rörlig i ett kvadratiskt mönster:

 IEnumerable FlyttaInASquare () const int framesPerSide = 30; medan (true) // flytta rätt för 30 ramar för (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;   

Vad som är trevligt med detta är att det inte bara sparar oss några instansvariabler, men det strukturerar också koden på ett mycket logiskt sätt. Du kan se direkt att fienden kommer att flytta rätt, sedan ner, sedan vänster, sedan uppåt, och sedan upprepa. Om du skulle implementera denna metod som en statlig maskin istället skulle kontrollflödet vara mindre uppenbart.

Låt oss lägga till byggnadsställningen som behövs för att göra beteenden fungerande. Fiender måste lagra sina beteenden, så vi lägger till en variabel till Fiende klass.

 privat lista> beteende = ny lista> ();

Observera att ett beteende har typen IEnumerator, inte IEnumerable. Du kan tänka på IEnumerable som mall för beteendet och IEnumerator som löpande instans. De IEnumerator kommer ihåg var vi befinner oss i beteendet och tar upp var det slutade när du ringer det MoveNext () metod. Varje ram kommer vi att gå igenom alla de beteenden fienden har och ringa MoveNext () på var och en av dem. Om MoveNext () returnerar false, det betyder att beteendet har slutförts så att vi ska ta bort det från listan.

Vi lägger till följande metoder i Fiende klass:

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

Och vi ändrar Uppdatering() metod att ringa ApplyBehaviours ():

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

Nu kan vi göra en statisk metod för att skapa sökande fiender. Allt vi behöver göra är att välja den bild vi vill ha och lägga till FollowPlayer () beteende.

 offentlig statisk Enemy CreateSeeker (Vector2 position) var fiende = ny fiend (Art.Seeker, position); enemy.AddBehaviour (enemy.FollowPlayer ()); återvända fiende; 

För att göra en fiende som rör sig slumpmässigt, får vi den välja en riktning och sedan göra små slumpmässiga justeringar i den riktningen. Om vi ​​justerar riktningen varje ram, blir rörelsen jitterig, så vi justerar just riktningen periodiskt. Om fienden går in i kanten av skärmen, får vi välja en ny slumpmässig riktning som pekar bort från väggen.

 IEnumerable MoveRandomly () float direction = rand.NextFloat (0, MathHelper.TwoPi); medan (sant) riktning + = rand.NextFloat (-0,1f, 0,1f); direction = MathHelper.WrapAngle (riktning); för (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 nu göra en fabriksmetod för att skapa vandrande fiender, som vi gjorde för sökaren:

 offentlig statisk Enemy CreateWanderer (Vector2 position) var fiende = ny fiende (Art.Wanderer, position); enemy.AddBehaviour (enemy.MoveRandomly ()); återvända fiende; 

Kollisionsdetektering

För kollisionsdetektering modellerar vi spelarens skepp, fienderna och kulorna som cirklar. Cirkulär kollisionsdetektering är bra eftersom det är enkelt, det är snabbt och det ändras inte när objekten roterar. Om du kommer ihåg, Entitet klassen har en radie och en position (positionen hänvisar till enhetens centrum). Detta är allt vi behöver för cirkulär kollisionsdetektering.

Att testa varje enhet mot alla andra enheter som potentiellt kan kollidera kan vara mycket långsam om du har ett stort antal enheter. Det finns många tekniker du kan använda för att påskynda bredfasskollisionsdetektering, som quadtrees, sweep and prune, och BSP-träd. Men för nu kommer vi bara ha några dussin enheter på skärmen åt gången, så vi kommer inte oroa oss för dessa mer komplexa tekniker. Vi kan alltid lägga till dem senare om vi behöver dem.

I Shape Blaster kan inte varje enhet kollidera med alla andra typer av enheter. Kulor och spelarens skepp kan endast kollidera med fiender. Fiender kan också kollidera med andra fiender - det kommer att förhindra att de överlappar varandra.

För att hantera dessa olika typer av kollisioner lägger vi till två nya listor till EntityManager att hålla koll på kulor och fiender. När vi lägger till en enhet i EntityManager, Vi vill lägga till den i den lämpliga listan, så vi gör en privat AddEntity () metod för att göra det. Vi kommer också att vara säker på att ta bort alla utgått enheter från alla listor varje ram.

 statisk lista fiender = ny lista(); statisk lista kulor = ny lista(); privat statisk tomt AddEntity (Entity entity) entities.Add (enhet); om (enhet är Bullet) bullets.Add (enhet som Bullet); annars om (enhet är fiende) fiender.Add (enhet som fiende);  // ... // i Uppdatering () bullets = bullets.Where (x =>! X.IsExpired) .ToList (); fiender = enemies.Where (x =>! x.IsExpired) .ToList ();

Byt samtalen till entity.Add () i EntityManager.Add () och EntityManager.Update () med samtal till AddEntity ().

Låt oss nu lägga till en metod som avgör om två enheter kolliderar:

 privat statisk bool IsColliding (Enhet a, Enhet b) floatradie = a.Radius + b.Radius; returnera! a.IsExpired &&! b.IsExpired && Vector2.DistanceSquared (a.Position, b.Position) < radius * radius; 

För att bestämma om två cirklar överlappar varandra, kontrollera bara om avståndet mellan dem är mindre än summan av deras radier. Vår metod optimerar detta något genom att kontrollera om kvadraten av avståndet är mindre än kvadraten av summan av radierna. Kom ihåg att det är lite snabbare att beräkna avståndet kvadrerat än det faktiska avståndet.

Olika saker kommer att hända beroende på vilka två objekt som kolliderar. Om två fiender kolliderar, vill vi att de ska driva varandra. Om en kula träffar en fiende, ska kula och fiende båda förstöras. Om spelaren berör en fiende ska spelaren dö och nivån bör återställas.

Vi lägger till en HandleCollision () metod till Fiende klass för att hantera kollisioner mellan fiender:

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

Denna metod kommer att driva den nuvarande fienden bort från den andra fienden. Ju närmare de är desto hårdare kommer det att drivas, eftersom storleken på (d / d.LengthSquared ()) är bara en över avståndet.

Resparking Player

Nästa behöver vi en metod för att hantera spelarens skepp att bli dödad. När detta händer, kommer spelarens skepp att försvinna en kort stund innan de respekteras.

Vi börjar med att lägga till två nya medlemmar till PlayerShip.

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

I början av PlayerShip.Update (), lägg till följande:

 om (IsDead) framesUntilRespawn--; lämna tillbaka; 

Och vi åsidosätter Dra() som visat:

 offentlig åsidosätt rubbning (SpriteBatch spriteBatch) if (! IsDead) base.Traw (spriteBatch); 

Slutligen lägger vi till en Döda() metod till PlayerShip.

 public void Kill () framesUntilRespawn = 60; 

Nu när alla bitar är på plats lägger vi till en metod för EntityManager som går igenom alla enheter och kontrollerar kollisioner.

 statiska void HandleCollisions () // hantera kollisioner mellan fiender för (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 ()); ha sönder; 

Ring denna metod från Uppdatering() omedelbart efter inställningen isUpdating till Sann.


Enemy Spawner

Det sista att göra är att göra EnemySpawner klass, som är ansvarig för att skapa fiender. Vi vill att spelet ska börja lätt och bli svårare, så EnemySpawner kommer att skapa fiender i en ökande takt som tiden fortskrider. När spelaren dör kommer vi att återställa EnemySpawner till sin första svårighet.

 statisk klass EnemySpawner static Random rand = ny slumpmässig (); statisk float inverseSpawnChance = 60; public static void Uppdatering () om (! 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 vektor2 GetSpawnPosition () Vector2 pos; gör pos = ny Vector2 (rand.Next ((int) GameRoot.ScreenSize.X), rand.Next ((int) GameRoot.ScreenSize.Y));  medan (Vector2.DistanceSquared (pos, PlayerShip.Instance.Position) < 250 * 250); return pos;  public static void Reset()  inverseSpawnChance = 60;  

Varje ram, det finns en in inverseSpawnChance att generera varje typ av fiende. Möjligheten att gyta en fiende ökar gradvis tills den når högst en på tjugo. Fiender skapas alltid minst 250 pixlar bort från spelaren.

Var försiktig med medan slingan är i GetSpawnPosition (). Det kommer att fungera effektivt så länge som området där fiender kan gräva är större än det område där de inte kan kasta. Men om du gör det förbudade området för stort får du en oändlig slinga.

Ring upp EnemySpawner.Update () från GameRoot.Update () och ringa EnemySpawner.Reset () när spelaren dödas.


Betyg och liv

I Shape Blaster börjar du med fyra liv och får ett extra liv varje 2000 poäng. Du får poäng för att förstöra fiender, med olika typer av fiender som är värda olika mängder poäng. Varje fiende som förstörs ökar också din poängmultiplikator med en. Om du inte dödar några fiender inom en kort tid kommer din multiplikator att återställas. Den totala poängen som erhållits från varje fiende du förstör är antalet poäng som fienden är värd multiplicerad med din nuvarande multiplikator. Om du förlorar alla dina liv är spelet över och du startar ett nytt spel med din poängåterställning till noll.

För att hantera allt detta kommer vi att göra en statisk klass som heter PlayerStatus.

 statisk klass PlayerStatus // hur mycket tid det tar, i sekunder, för en multiplikator att gå ut. privat const float multiplikatorExpiryTime = 0,8f; privat const int maxMultiplier = 20; offentliga statiska int Lever get; privat uppsättning  statisk statisk int Resultat get; privat uppsättning  statisk statisk int Multiplikator get; privat uppsättning  privat statisk float multiplicerTimeLeft; // tiden tills den aktuella multiplikatorn löper ut privata statiska int poängForExtraLife; // poäng som krävs för att få extra livslängd // Statisk konstruktor statisk PlayerStatus () Reset ();  statisk statisk tomgång Återställ () Resultat = 0; Multiplikator = 1; Bor = 4; scoreForExtraLife = 2000; multiplikatorTimeLeft = 0;  public static void Uppdatering () if (Multiplikator> 1) // uppdatera multiplikatorns timer om ((multiplicerTimeLeft - = (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; Lever ++;  statisk statisk tomt IncreaseMultiplier () om (PlayerShip.Instance.IsDead) returnerar; multiplicerTimeLeft = multiplikatorExpiryTime; om (Multiplikatorn < maxMultiplier) Multiplier++;  public static void ResetMultiplier()  Multiplier = 1;  public static void RemoveLife()  Lives--;  

Ring upp PlayerStatus.Update () från GameRoot.Update () när spelet inte är pausat.

Därefter vill vi visa din poäng, liv och multiplikator på skärmen. För att göra detta måste vi lägga till en SpriteFont i Innehåll projekt och en motsvarande variabel i Konst klass, som vi kommer att namnge Font. Ladda in teckensnittet i Art.Load () som vi gjorde med texturerna.

Notera: Det finns en font med namnet Nova Square som ingår i Shape Blaster-källfilerna som du kan använda. För att använda teckensnittet måste du först installera det och starta om Visual Studio om det var öppet. Du kan sedan ändra teckensnittsnamnet i sprite-fontfilen till "Nova Square". Demo-projektet använder inte denna typsnitt som standard eftersom det förhindrar att projektet kompilerar om teckensnittet inte är installerat.

Ändra slutet av GameRoot.Draw () där markören ritas som visas nedan.

 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); // rita anpassade muspekaren spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();

DrawRightAlignedString () är en hjälpmetod för att rita textjusterad på höger sida av skärmen. Lägg till den till GameRoot genom att lägga till koden nedan.

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

Nu ska dina liv, poäng och multiplikator visas på skärmen. Men vi behöver ändå ändra dessa värden som svar på spelhändelser. Lägg till en egendom som heter PointValue till Fiende klass.

 offentliga int PointValue get; privat uppsättning 

Ställ in poängvärdet för olika fiender till något du tycker är lämpligt. Jag gjorde de vandrande fienderna värd en poäng och de sökande fiender värda två poäng.

Lägg sedan till följande två rader till Enemy.WasShot () för att öka spelarens poäng och multiplikator:

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

Ring upp PlayerStatus.RemoveLife () i PlayerShip.Kill (). Om spelaren förlorar alla sina liv, ring PlayerStatus.Reset () att återställa sin poäng och bor i början av ett nytt spel.

Höga poäng

Låt oss lägga till möjligheten för spelet att spåra dina bästa poäng. Vi vill att den här poängen ska fortsätta över spel så vi sparar det till en fil. Vi kommer att hålla det väldigt enkelt och spara högsta poängen som ett enda vanligt textnummer i en fil i den aktuella arbetsmappen (det här kommer att vara samma katalog som innehåller spelets .exe fil).

Lägg till följande metoder till PlayerStatus:

 privat const sträng highScoreFilename = "highscore.txt"; privat statisk int LoadHighScore () // returnera sparade högpoäng om möjligt och returnera 0 annars int poäng returnera File.Exists (highScoreFilename) && int.TryParse (File.ReadAllText (highScoreFilename), out score)? poäng: 0;  privat statisk tomgång SaveHighScore (int poäng) File.WriteAllText (highScoreFilename, score.ToString ()); 

De LoadHighScore () Metoden kontrollerar först att högpoängsfilen existerar och kontrollerar att det innehåller ett giltigt heltal. Den andra kontrollen kommer sannolikt aldrig att misslyckas om inte användaren manuellt ändrar högpoängsfilen till något ogiltigt, men det är bra att vara försiktig.

Vi vill ladda hög poäng när spelet startar och spara det när spelaren får ett nytt högt poäng. Vi modifierar den statiska konstruktören och Återställa() metoder i PlayerStatus att göra så. Vi lägger också till en hjälparätt, IsGameOver som vi ska använda på ett ögonblick.

 offentlig statisk bool IsGameOver get return Lives == 0;  statisk PlayerStatus () HighScore = LoadHighScore (); Återställa();  statisk statisk tomgång Återställ () om (Resultat> HighScore) SaveHighScore (HighScore = Score); Betyg = 0; Multiplikator = 1; Bor = 4; scoreForExtraLife = 2000; multiplikatorTimeLeft = 0; 

Det tar hand om att spåra högsta poängen. Nu behöver vi visa den. Lägg till följande kod till GameRoot.Draw () i samma SpriteBatch blockera där den andra texten är ritad:

 om (PlayerStatus.IsGameOver) string text = "Spel över \ n" + "Din poäng:" + PlayerStatus.Score + "\ n" + "High Score:" + PlayerStatus.HighScore; Vector2 textSize = Art.Font.MeasureString (text); spriteBatch.DrawString (Art.Font, text, ScreenSize / 2 - textSize / 2, Color.White); 

Detta kommer att göra det visa din poäng och hög poäng på spelet över, centrerad på skärmen.

Som en sista justering ökar vi tiden innan skeppet respekterar spelet över för att ge spelaren tid att se sin poäng. Ändra PlayerShip.Kill () genom att ställa in responstid till 300 bildrutor (fem sekunder) om spelaren är borta.

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

Spelet är nu klart att spela. Det kanske inte ser ut som mycket, men det har alla grundläggande mekanismer implementerats. I framtida handledning kommer vi att lägga till ett blomfilter och partikeleffekter för att spice upp det. Men just nu, låt oss snabbt lägga till lite ljud och musik för att göra det mer intressant.


Ljud och musik

Att spela ljud och musik är lätt i XNA. Först lägger vi till våra ljudeffekter och musik till innehållsrörledningen. I Egenskaper ruta, se till att innehållsbehandlaren är inställd på Låt för musiken och Ljudeffekt för ljuden.

Därefter gör vi en statisk hjälparklass för ljuden.

 statisk klass Ljud public static Song Music get; privat uppsättning  privata statiska readonly Random rand = ny slumpmässig (); privata statiska SoundEffect [] explosioner; // returnera ett slumpmässigt explosionsljud offentligt statiskt SoundEffect Explosion get return explosions [rand.Next (explosions.Length)];  privata statiska SoundEffect [] -bilder; offentlig statisk SoundEffect Shot get return shots [rand.Next (shots.Length)];  privata statiska SoundEffect [] spawns; offentlig statisk SoundEffect Spawn get return spawns [rand.Next (spawns.Length)];  statisk statisk tomgång (ContentManager-innehåll) Music = content.Load( "Sound / musik"); // Dessa linq uttryck är bara ett fint sätt att ladda alla ljud i varje kategori i en array. explosioner = Enumerable.Range (1, 8) .Välj (x => content.Load("Ljud / explosion-0" + x)). ToArray (); shots = Enumerable.Range (1, 4) .Välj (x => content.Load("Ljud / skott-0" + x)). ToArray (); spawns = Enumerable.Range (1, 8) .Välj (x => content.Load("Ljud / spawn-0" + x)). ToArray (); 

Eftersom vi har flera varianter av varje ljud, är Explosion, Skott, och Rom egenskaper kommer att välja ett ljud slumpmässigt bland varianterna.

Ring upp Sound.load () i GameRoot.LoadContent (). För att spela upp musiken lägger du till följande två rader i slutet av GameRoot.Initialize ().

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

För att spela ljud i XNA kan du helt enkelt ringa Spela() metod på a Ljudeffekt. Denna metod ger också en överbelastning som låter dig justera volymen, tonhöjden och panelen av ljudet. Ett knep för att göra våra ljud mer varierade är att justera dessa kvantiteter på varje spel.

För att utlösa ljudeffekten för fotografering, lägg till följande rad PlayerShip.Update (), inuti if-uttalandet där kulorna skapas. Observera att vi slumpmässigt byter tonvikten upp eller ner, upp till en femtedel av en oktav, för att göra ljuden mindre repeterande.

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

På samma sätt utlösa en explosionsljudseffekt varje gång en fiende förstörs genom att lägga till följande Enemy.WasShot ().

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

Du har nu ljud och musik i ditt spel. Lätt, är det inte?


Slutsats

Det bryter upp den grundläggande spelmekaniken. I nästa handledning lägger vi till ett blomfilter för att neonljusen ska lysa.