Gör en Neon Vector Shooter i XNA Basic Gameplay

I den här serien av handledningar visar jag dig hur man gör en neon tvillingskytt som Geometry Wars, som vi kommer att ringa Shape Blaster 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.

Jag uppmuntrar dig att expandera och experimentera med koden som ges i dessa handledning. Vi täcker dessa ämnen i serien:

  1. Ställ in den grundläggande gameplayen, skapa spelarens fartyg och hanteringsingång, ljud och musik.
  2. Avsluta implementeringen av spelmekaniken genom att lägga till fiender, hantera kollisionsdetektering och spåra spelarens poäng och liv.
  3. Lägg till ett blomfilter, vilket är den effekt som ger grafiken en neonglöd.
  4. Lägg till galen, över-the-top partikel effekter.
  5. Lägg till bakgrundsröret för vridning.

Här är vad vi får i slutet av serien:

Varning: Högt!

Och här är vad vi får vid slutet av den här första delen:

Varning: Högt!

Musik och ljud effekter du kan höra i dessa videoklipp skapades av RetroModular, och du kan läsa om hur han gjorde det över på Audiotuts+.

De sprites är av Jacob Zinman-Jeanes, vår invånare Tuts + designer. Allt konstverket kan hittas i källfilens nedladdningsladd.

Teckensnittet är Nova Square, av Wojciech Kalinowski.

Låt oss börja.


Översikt

I denna handledning skapar vi en tvillingstickskytte; Spelaren kommer att styra skeppet med tangentbordet, tangentbordet och musen eller de två tumpelarna i en gamepad. 

Vi använder ett antal klasser för att uppnå detta:

  • Entitet: Basklassen för fiender, kulor och spelarens fartyg. Entiteter kan flytta och dras.
  • Kula och PlayerShip.
  • EntityManager: Håller koll på alla enheter i spelet och utför kollisionsdetektering.
  • Inmatning: Hjälper hantera inmatning från tangentbord, mus och gamepad.
  • Konst: Laddar och innehåller referenser till de texturer som behövs för spelet.
  • Ljud: Laddar och innehåller referenser till ljud och musik.
  • MathUtil och Extensions: Innehåller några användbara statiska metoder och förlängningsmetoder.
  • GameRoot: Kontrollerar huvudslingan i spelet. Det här är game1 klass XNA genererar automatiskt, bytt namn.

Koden i denna handledning syftar till att vara enkel och lätt att förstå. Det kommer inte att ha alla funktioner eller en komplicerad arkitektur utformad för att stödja alla möjliga behov. Snarare gör det bara vad den behöver göra. Att hålla det enkelt gör det lättare för dig att förstå koncepten, och sedan ändra och expandera dem i ditt eget unika spel.


Enheter och spelarens fartyg

Skapa ett nytt XNA-projekt. Byt namn på game1 klass till något mer lämpligt. Jag ringde det GameRoot.

Låt oss börja med att skapa en bas klass för våra spel enheter.

 abstrakt klass Enhet skyddad Texture2D image; // Bildens färgton. Detta kommer också att göra det möjligt för oss att ändra insynen. skyddad Färgfärg = Color.White; allmän vektor2 position, hastighet; offentlig float Orientering; offentlig float Radius = 20; // används för cirkulär kollisionsdetektering offentlig bool IsExpired; // true om enheten var förstörd och bör raderas. allmän vektor2 storlek get return image == null? Vector2.Zero: Ny Vector2 (image.Width, image.Height);  offentlig abstrakt tomgångsuppdatering (); public virtual void Draw (SpriteBatch spriteBatch) spriteBatch.Draw (bild, Position, null, färg, Orientering, Storlek / 2f, 1f, 0, 0); 

Alla våra enheter (fiender, kulor och spelarens fartyg) har några grundläggande egenskaper som en bild och en position. IsExpired kommer att användas för att indikera att enheten har förstörts och bör tas bort från alla listor som innehåller en hänvisning till den.

Nästa skapar vi en EntityManager att spåra våra enheter och att uppdatera och rita dem.

 statisk klass EntityManager statisk lista enheter = ny lista(); statisk bool uppdaterar statisk lista addedEntities = ny lista(); offentlig statisk inträkning get return entities.Count;  statisk statisk tomt Lägg till (Entity entity) om (! isUpdating) entities.Add (enhet); Annat addedEntities.Add (enhet);  offentlig statisk tomgång Uppdatering () isUpdating = true; foreach (var enhet i enheter) entity.Update (); isUpdating = false; foreach (var enhet i addedEntities) entities.Add (enhet); addedEntities.Clear (); // Ta bort alla utgått enheter. enheter = enheter.Var (x =>! x.IsExpired) .ToList ();  statisk statisk tomt teckning (SpriteBatch spriteBatch) foreach (var enhet i enheter) entity.raw (spriteBatch); 

Kom ihåg att om du ändrar en lista medan du tar över det, får du ett undantag. Ovanstående kod tar hand om detta genom att köra upp alla enheter som läggs till under uppdatering i en separat lista och lägger till dem efter att det har slutförts uppdatering av befintliga enheter.

Göra dem synliga

Vi måste ladda lite texturer om vi vill rita något. Vi gör en statisk klass för att hålla referenser till alla våra texturer.

 statisk klass Art public static Texture2D Player get; privat uppsättning  statisk statisk Texture2D-sökare get; privat uppsättning  statisk statisk Texture2D Wanderer get; privat uppsättning  statisk statisk Texture2D Bullet get; privat uppsättning  statisk statisk Texture2D Pointer get; privat uppsättning  statisk statisk tomgång (ContentManager-innehåll) Player = content.Load("Spelare"); Sökare = content.Load( "Sökare"); Wanderer = content.Load("Vandrare"); Bullet = content.Load("Kula"); Pointer = content.Load("Pekare"); 

Ladda konsten genom att ringa Art.Load (innehåll) i GameRoot.LoadContent (). Dessutom måste ett antal klasser känna till skärmdimensionerna, så lägg till följande egenskaper till GameRoot:

 offentlig statisk GameRoot Instance get; privat uppsättning  statisk statisk Viewport Viewport get returnera Instance.GraphicsDevice.Viewport;  statisk statisk vektor2 ScreenSize get returnera ny Vector2 (Viewport.Width, Viewport.Height); 

Och i GameRoot konstruktör, lägg till:

 Instans = detta;

Nu börjar vi skriva PlayerShip klass.

 klass PlayerShip: Entity privat statisk PlayerShip instans; offentlig statisk PlayerShip Instance få om (instance == null) instance = new PlayerShip (); returnera instans  privat PlayerShip () image = Art.Player; Position = GameRoot.ScreenSize / 2; Radius = 10;  offentlig åsidosätt annullering Uppdatering () // skeppslogik går här

Vi gjorde PlayerShip en singleton, sätta sin bild och placera den i mitten av skärmen.

Slutligen, låt oss lägga till spelarskeppet till EntityManager och uppdatera och rita den. Lägg till följande kod i GameRoot:

 // i Initialize (), efter samtalet till bas.Initiera () EntityManager.Add (PlayerShip.Instance); // i Uppdatering () EntityManager.Update (); // i Draw () GraphicsDevice.Clear (Color.Black); spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (spriteBatch); spriteBatch.End ();

Vi drar spritesna med tillsatsblandning, vilket är en del av vad som kommer att ge dem sitt neon-utseende. Om du kör spelet vid denna tidpunkt ska du se ditt skepp i mitten av skärmen. Det svarar dock inte på inmatningen. Låt oss fixa det.


Inmatning

För rörelse kan spelaren använda WASD på tangentbordet eller den vänstra tumstickan på en gamepad. För syftet kan de använda piltangenterna, höger tumblick eller musen. Vi behöver inte att spelaren håller musknappen intryckt eftersom det är obekväma att hålla knappen intryckt. Detta leder oss till ett litet problem: hur vet vi om spelaren syftar med mus, tangentbord eller gamepad?

Vi använder följande system: vi lägger till tangentbord och gamepad-inmatning tillsammans. Om spelaren flyttar musen växlar vi till musens sikte. Om spelaren trycker på piltangenterna eller använder rätt tumsticka, stänger du av musens sikte.

En sak att notera: att trycka på en tumstick framåt kommer att returnera a positiv y värde. I skärmkoordinater ökar y-värdena nedåt. Vi vill invertera y-axeln på regulatorn så att trycka upp tummen uppåt kommer att sikta mot oss eller flytta oss mot toppen av skärmen.

Vi gör en statisk klass för att hålla reda på de olika inmatningsenheterna och ta hand om växling mellan de olika typerna av sikten.

 statisk klass Input privat statisk KeyboardState keyboardState, lastKeyboardState; privat statisk MouseState mouseState, lastMouseState; privat statisk GamePadState gamepadState, lastGamepadState; privat statisk bool isAimingWithMouse = false; offentliga statiska Vector2 MousePosition get returnera nya Vector2 (mouseState.X, mouseState.Y);  offentlig statisk tomgång Uppdatering () lastKeyboardState = keyboardState; lastMouseState = mouseState; lastGamepadState = gamepadState; keyboardState = Keyboard.GetState (); mouseState = Mouse.GetState (); gamepadState = GamePad.GetState (PlayerIndex.One); // Om spelaren trycker på en av piltangenterna eller använder en gamepad för att sikta, vill vi inaktivera musriktning. Annars, // om spelaren flyttar musen, aktivera musen siktar. om (ny [] Keys.Left, Keys.Right, Keys.Up, Keys.Down .Any (x => keyboardState.IsKeyDown (x)) || gamepadState.ThumbSticks.Right! = Vector2.Zero) isAimingWithMouse = falsk; annars om (MousePosition! = ny Vector2 (lastMouseState.X, lastMouseState.Y)) isAimingWithMouse = true;  // Kontrollerar om en knapp bara trycktes ned offentliga statiska boolen WasKeyPressed (Keys key) returnera LastKeyboardState.IsKeyUp (key) && keyboardState.IsKeyDown (key);  statisk statisk bool WasButtonPressed (Knappar) returnera lastGamepadState.IsButtonUp (knapp) && gamepadState.IsButtonDown (knapp);  statisk statisk vektor2 GetMovementDirection () Vector2 direction = gamepadState.ThumbSticks.Left; riktning.Y * = -1; // invertera y-axeln om (keyboardState.IsKeyDown (Keys.A)) direction.X - = 1; om (keyboardState.IsKeyDown (Keys.D)) direction.X + = 1; om (keyboardState.IsKeyDown (Keys.W)) direction.Y - = 1; om (keyboardState.IsKeyDown (Keys.S)) direction.Y + = 1; // Kläm längden på vektorn till högst 1. om (direction.LengthSquared ()> 1) direction.Normalize (); returriktning;  statisk static2 GetAimDirection () om (isAimingWithMouse) returnerar GetMouseAimDirection (); Vector2 direction = gamepadState.ThumbSticks.Right; riktning.Y * = -1; om (keyboardState.IsKeyDown (Keys.Left)) riktning.X - = 1; om (keyboardState.IsKeyDown (Keys.Right)) direction.X + = 1; om (keyboardState.IsKeyDown (Keys.Up)) direction.Y - = 1; om (keyboardState.IsKeyDown (Keys.Down)) direction.Y + = 1; // Om det inte finns någon inmatningsinmatning, returnera noll. Annars normaliserar du riktningen för att ha en längd på 1. om (riktning == Vector2.Zero) returnera Vector2.Zero; annars återvänder Vector2.Normalize (direction);  privat statisk vektor2 GetMouseAimDirection () Vector2 direction = MousePosition - PlayerShip.Instance.Position; om (riktning == Vector2.Zero) returnera Vector2.Zero; annars återvänder Vector2.Normalize (direction);  statisk statisk bool WasBombButtonPressed () returnera WasButtonPressed (Buttons.LeftTrigger) || WasButtonPressed (Buttons.RightTrigger) || WasKeyPressed (Keys.Space); 

Ring upp Input.Update () i början av GameRoot.Update () för inmatningsklassen att arbeta.

Tips: Du kanske märker att jag inkluderade en metod för bomber. Vi kommer inte att genomföra bomber nu men den här metoden finns för framtida bruk.

Du kanske också märker i GetMovementDirection () jag skrev direction.LengthSquared ()> 1. Använder sig av LengthSquared () är en liten prestationsoptimering; Att beräkna kvadraten av längden är lite snabbare än att beräkna längden själv eftersom den undviker den relativt långsamma kvadratrotsoperationen. Du ser kod med fyrkanter av längder eller avstånd i hela programmet. I det här fallet är prestandafördelningen försumbar, men den här optimeringen kan göra skillnad när den används i snäva slingor.

Rör på sig

Vi är nu redo att få skeppet att röra sig. Lägg till den här koden i PlayerShip.Update () metod:

 const float hastighet = 8; Hastighet = hastighet * Input.GetMovementDirection (); Position + = Hastighet; Position = Vector2.Clamp (Position, Storlek / 2, GameRoot.ScreenSize - Storlek / 2); om (Velocity.LengthSquared ()> 0) Orientering = Velocity.ToAngle ();

Detta kommer att göra skeppet att flytta med en hastighet upp till åtta pixlar per ram, klämma fast sin position så att den inte kan gå utanför skärmen och rotera skeppet mot den riktning det rör sig.

ToAngle () är en enkel förlängningsmetod som definieras i vår Extensions klass som så:

 offentlig statisk flottör ToAngle (denna Vector2 vektor) returnera (float) Math.Atan2 (vector.Y, vector.X); 

Skytte

Om du kör spelet nu ska du kunna flyga skeppet runt. Nu får vi göra det skjuta.

Först behöver vi en klass för kulor.

 klass Bullet: Entity offentliga Bullet (Vector2 position, Vector2 hastighet) image = Art.Bullet; Position = position; Hastighet = hastighet; Orientering = Velocity.ToAngle (); Radius = 8;  offentlig åsidosätt annullering Uppdatering () om (Velocity.LengthSquared ()> 0) Orientering = Velocity.ToAngle (); Position + = Hastighet; // radera kulor som går utanför skärmen om (! GameRoot.Viewport.Bounds.Contains (Position.ToPoint ())) IsExpired = true; 

Vi vill ha en kort nedkylningsperiod mellan kulor, så lägg till följande fält till PlayerShip klass.

 const int cooldownFrames = 6; int coolingdownRemaining = 0; statisk Random rand = ny slumpmässig ();

Lägg även till följande kod till PlayerShip.Update ().

 var aim = Input.GetAimDirection (); om (aim.LengthSquared ()> 0 && cooldownRemaining <= 0)  cooldownRemaining = cooldownFrames; float aimAngle = aim.ToAngle(); Quaternion aimQuat = Quaternion.CreateFromYawPitchRoll(0, 0, aimAngle); float randomSpread = rand.NextFloat(-0.04f, 0.04f) + rand.NextFloat(-0.04f, 0.04f); Vector2 vel = MathUtil.FromPolar(aimAngle + randomSpread, 11f); Vector2 offset = Vector2.Transform(new Vector2(25, -8), aimQuat); EntityManager.Add(new Bullet(Position + offset, vel)); offset = Vector2.Transform(new Vector2(25, 8), aimQuat); EntityManager.Add(new Bullet(Position + offset, vel));  if (cooldownRemaining > 0) CooldownRemaining--;

Denna kod skapar två kulor som reser parallellt med varandra. Det lägger till en liten mängd slumpmässighet i riktningen. Detta gör att skotten sprids ut lite som en maskingevär. Vi lägger till två slumptal tillsammans eftersom det gör att deras summa sannolikt kommer att vara centrerad (runt noll) och mindre sannolikt att skicka kulor långt borta. Vi använder en kvaternion för att rotera kollens ursprungliga läge i den riktning de reser.

Vi använde också två nya hjälpar metoder:

  • Random.NextFloat () returnerar en float mellan ett minimi- och maximivärde.
  • MathUtil.FromPolar () skapar en Vector2 från en vinkel och storleksordning.
 // i Extensions offentlig statisk float NextFloat (denna slumpmässiga rand, float minValue, float maxValue) return (float) rand.NextDouble () * (maxValue - minValue) + minValue;  // i MathUtil offentliga statiska Vector2 FromPolar (floatvinkel, float magnitude) returformat * ny Vector2 ((float) Math.Cos (vinkel), (float) Math.Sin (vinkel)); 

Anpassad markör

Det är en sak vi borde göra nu när vi har Inmatning klass. Låt oss rita en anpassad muspekare för att göra det enklare att se var fartyget syftar. I GameRoot.Draw, helt enkelt rita Art.Pointer vid musens position.

 spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (spriteBatch); // rita anpassade muspekaren spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();

Slutsats

Om du testar spelet nu kan du flytta skeppet runt med WASD-tangenterna eller eller vänstra tumstickan och rikta den kontinuerliga strömmen av kulor med piltangenterna, musen eller höger fingertopp.

I nästa del kommer vi att slutföra spelet genom att lägga till fiender och en poäng.