Lag en Neon Vector Shooter i XNA Basic Gameplay

I denne serien av opplæringsprogrammer, viser jeg deg hvordan du lager en neon tvillingpinne shooter som Geometry Wars, som vi vil ringe Shape Blaster, 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.

Jeg oppfordrer deg til å utvide på og eksperimentere med koden som er gitt i disse veiledningene. Vi vil dekke disse emnene i serien:

  1. Sett opp grunnleggende spill, skape spillerens skip og håndtering, lyd og musikk.
  2. Fullfør implementeringen av spillmekanikken ved å legge til fiender, håndtere kollisjonsdeteksjon og spore spillernes score og liv.
  3. Legg til et blomstfilter, som er effekten som gir grafikken en neon glød.
  4. Legg til galte, over-the-top partikkel effekter.
  5. Legg til bakgrunnsrammen for krumning.

Her er hva vi får ved slutten av serien:

Advarsel: Høyt!

Og her er hva vi får ved slutten av denne første delen:

Advarsel: Høyt!

Musikken og lydeffekter du kan høre i disse videoene ble laget av RetroModular, og du kan lese om hvordan han gjorde det over på Audiotuts+.

De sprites er av Jacob Zinman-Jeanes, vår bosatt Tuts + designer. Alt kunstverket kan bli funnet i kildefilen last ned zip.

Skriften er Nova Square, av Wojciech Kalinowski.

La oss komme i gang.


Oversikt

I denne opplæringen vil vi lage en tvillingpinne shooter; spilleren vil styre skipet med tastaturet, tastaturet og musen eller de to tommelstikkene til en gamepad. 

Vi bruker en rekke klasser for å oppnå dette:

  • Entity: Baseklassen for fiender, kuler og spillerens skip. Enheter kan bevege seg og bli tegnet.
  • Kule og PlayerShip.
  • EntityManager: Holder styr på alle enheter i spillet og utfører kollisjonsdeteksjon.
  • Input: Hjelper med å administrere innspill fra tastatur, mus og gamepad.
  • Kunst: Laster og inneholder referanser til teksturen som trengs for spillet.
  • Lyd: Laster og holder referanser til lydene og musikken.
  • MathUtil og utvidelser: Inneholder noen nyttige statiske metoder og utvidelsesmetoder.
  • GameRoot: Kontrollerer hovedløkken i spillet. Dette er Spill1 klasse XNA genererer automatisk, omdøpt.

Koden i denne opplæringen tar sikte på å være enkel og lett å forstå. Det vil ikke ha alle funksjoner eller en komplisert arkitektur designet for å støtte alle mulige behov. Snarere vil det bare gjøre hva den trenger å gjøre. Å holde det enkelt gjør det lettere for deg å forstå konseptene, og deretter endre og utvide dem til ditt eget unike spill.


Enheter og spillerens skip

Lag et nytt XNA-prosjekt. Gi nytt navn til Spill1 klassen til noe mer egnet. Jeg ringte det GameRoot.

La oss nå begynne med å lage en grunnklasse for våre spill-enheter.

 abstrakt klasse Entitet beskyttet Texture2D image; // Tonet av bildet. Dette vil også gi oss mulighet til å endre gjennomsiktigheten. beskyttet Fargefarge = Color.White; offentlig Vector2 posisjon, hastighet; offentlig float Orientering; offentlig flyte Radius = 20; // brukt til sirkulær kollisjon gjenkjenning offentlig bool IsExpired; // true hvis enheten ble ødelagt og bør slettes. offentlig Vector2 Størrelse get return image == null? Vector2.Zero: Ny Vector2 (image.Width, image.Height);  offentlig abstrakt tomt oppdatering (); offentlig virtuell ugyldig tegning (SpriteBatch spriteBatch) spriteBatch.Draw (bilde, posisjon, null, farge, orientering, størrelse / 2f, 1f, 0, 0); 

Alle våre enheter (fiender, kuler og spillerens skip) har noen grunnleggende egenskaper som et bilde og en posisjon. IsExpired vil bli brukt til å indikere at enheten har blitt ødelagt og bør fjernes fra noen lister med referanse til den.

Deretter lager vi en EntityManager å spore våre enheter og å oppdatere og tegne dem.

 statisk klasse EntityManager statisk liste enheter = ny liste(); statisk bool er oppdatering; statisk liste addedEntities = ny liste(); offentlig statisk int Count get return entities.Count;  statisk statisk tomt Add (Entity entity) hvis (! erUpdating) entities.Add (enhet); ellers addedEntities.Add (enhet);  offentlig statisk ugyldig oppdatering () isUpdating = true; foreach (var enhet i enheter) entity.Update (); isUpdating = false; foreach (var enhet i addedEntities) entities.Add (enhet); addedEntities.Clear (); // fjerne eventuelle utgåtte enheter. enheter = entities.Where (x =>! x.IsExpired) .ToList ();  statisk statisk tomt tegning (SpriteBatch spriteBatch) foreach (var enhet i enheter) entity.raw (spriteBatch); 

Husk at hvis du endrer en liste mens den overhører det, får du et unntak. Ovennevnte kode tar vare på dette ved å kjøre opp noen enheter lagt til under oppdatering i en egen liste, og legge dem til etter at det er ferdig med å oppdatere eksisterende enheter.

Gjør dem synlige

Vi må laste inn noen teksturer hvis vi ønsker å tegne noe. Vi lager en statisk klasse for å holde referanser til alle våre teksturer.

 statisk klasse Art public static Texture2D Player get; privat sett;  offentlig statisk Texture2D søkeren get; privat sett;  offentlig statisk Texture2D Wanderer get; privat sett;  offentlig statisk Texture2D Bullet get; privat sett;  offentlig statisk Texture2D Pointer get; privat sett;  Statisk statisk tomt Load (ContentManager innhold) Player = content.Load("Spiller"); Søker = content.Load( "Seeker"); Wanderer = content.Load( "Vandrer"); Bullet = content.Load("Kule"); Pointer = content.Load( "Pointer"); 

Last inn kunsten ved å ringe Art.Load (Content) i GameRoot.LoadContent (). Et antall klasser må også kjenne skjermdimensjonene, så legg til følgende egenskaper til GameRoot:

 offentlig statisk GameRoot-instans get; privat sett;  offentlig statisk Viewport Viewport get return Instance.GraphicsDevice.Viewport;  offentlig statisk Vector2 ScreenSize få returner ny Vector2 (Viewport.Width, Viewport.Height); 

Og i GameRoot konstruktør, legg til:

 Instans = dette;

Nå begynner vi å skrive PlayerShip klasse.

 klasse PlayerShip: Entity privat statisk PlayerShip eksempel; offentlig statisk PlayerShip-instans få hvis (forekomst == null) instance = nytt PlayerShip (); returnere forekomst;  privat PlayerShip () image = Art.Player; Posisjon = GameRoot.ScreenSize / 2; Radius = 10;  Offentlig overstyring ugyldig Oppdatering () // skiplogikk går her

Vi lagde PlayerShip en singleton, sette bildet sitt og plasserte det i midten av skjermen.

Til slutt, la oss legge til spillerens skip til EntityManager og oppdater og tegne det. Legg til følgende kode i GameRoot:

 // i Initialize (), etter samtalen til base.Initialize () EntityManager.Add (PlayerShip.Instance); // i Update () EntityManager.Update (); // i Draw () GraphicsDevice.Clear (Color.Black); spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (spriteBatch); spriteBatch.End ();

Vi trekker sprites med additiv blanding, som er en del av det som vil gi dem deres neon utseende. Hvis du kjører spillet på dette punktet, bør du se skipet ditt i midten av skjermen. Det svarer imidlertid ikke til innspillingen ennå. La oss fikse det.


Input

For bevegelse kan spilleren bruke WASD på tastaturet eller venstre tommelstikk på en gamepad. For sikte kan de bruke piltastene, høyre tommelstikk eller musen. Vi vil ikke kreve at spilleren holder museknappen for å skyte fordi det er ubehagelig å holde knappen nede. Dette gir oss et lite problem: hvordan vet vi om spilleren satser med mus, tastatur eller gamepad?

Vi bruker følgende system: vi legger til tastatur og gamepad-inngang sammen. Hvis spilleren beveger musen, bytter vi til musens mål. Hvis spilleren trykker piltastene eller bruker riktig tommelstikk, slår vi av musens mål.

En ting å merke seg: Hvis du trykker på en tommelstikk fremover, kommer du tilbake a positiv y verdi. I skjermkoordinater øker y-verdiene nedad. Vi vil omverve y-aksen på kontrolleren, slik at du trykker på tommelstikket vil sikte mot eller bevege oss mot toppen av skjermen.

Vi lager en statisk klasse for å holde oversikt over de ulike inngangsenhetene, og passe på å bytte mellom de forskjellige typer sikter.

 statisk klasse Input privat statisk tastaturState keyboardState, lastKeyboardState; privat statisk MouseState mouseState, lastMouseState; privat statisk GamePadState gamepadState, lastGamepadState; privat statisk bool isAimingWithMouse = false; offentlig statisk Vector2 MousePosition get returner ny Vector2 (mouseState.X, mouseState.Y);  offentlig statisk ugyldig oppdatering () lastKeyboardState = keyboardState; lastMouseState = mouseState; lastGamepadState = gamepadState; keyboardState = Keyboard.GetState (); mouseState = Mouse.GetState (); gamepadState = GamePad.GetState (PlayerIndex.One); // Hvis spilleren trykket på en av piltastene eller bruker en gamepad for å sikte, vil vi deaktivere musens mål. Ellers, // hvis spilleren beveger musen, aktiver musen med sikte. hvis (ny [] Keys.Left, Keys.Right, Keys.Up, KeysDown .Any (x => keyboardState.IsKeyDown (x)) || gamepadState.ThumbSticks.Right! = Vector2.Zero) isAimingWithMouse = falsk; ellers hvis (MousePosition! = ny Vector2 (lastMouseState.X, lastMouseState.Y)) isAimingWithMouse = true;  // Kontrollerer om en nøkkel bare ble trykket ned offentlig statisk bool WasKeyPressed (Keys key) return lastKeyboardState.IsKeyUp (key) && keyboardState.IsKeyDown (key);  offentlig statisk bool WasButtonPressed (Knapper-knappen) return lastGamepadState.IsButtonUp (knapp) && gamepadState.IsButtonDown (knapp);  statisk statisk Vector2 GetMovementDirection () Vector2 direction = gamepadState.ThumbSticks.Left; retning.Y * = -1; // omvendt y-aksen hvis (keyboardState.IsKeyDown (Keys.A)) retning.X - = 1; hvis (keyboardState.IsKeyDown (Keys.D)) retning.X + = 1; hvis (keyboardState.IsKeyDown (Keys.W)) retning.Y - = 1; hvis (keyboardState.IsKeyDown (Keys.S)) retning.Y + = 1; // Klem lengden på vektoren til maksimalt 1. hvis (direction.LengthSquared ()> 1) direction.Normalize (); returretning;  statisk statisk Vector2 GetAimDirection () hvis (isAimingWithMouse) returnerer GetMouseAimDirection (); Vector2 retning = gamepadState.ThumbSticks.Right; retning.Y * = -1; hvis (keyboardState.IsKeyDown (Keys.Left)) retning.X - = 1; hvis (keyboardState.IsKeyDown (Keys.Right)) retning.X + = 1; hvis (keyboardState.IsKeyDown (Keys.Up)) retning.Y - = 1; hvis (keyboardState.IsKeyDown (Keys.Down)) retning.Y + = 1; // Hvis det ikke er noen målinngang, returner null. Ellers normaliserer retningen å ha en lengde på 1. hvis (retning == Vector2.Zero) returnerer Vector2.Zero; ellers returnerer Vector2.Normalize (retning);  privat statisk Vector2 GetMouseAimDirection () Vector2 retning = MousePosition - PlayerShip.Instance.Position; hvis (retning == Vector2.Zero) returnere Vector2.Zero; ellers returnerer Vector2.Normalize (retning);  offentlig statisk bool WasBombButtonPressed () retur WasButtonPressed (Buttons.LeftTrigger) || WasButtonPressed (Buttons.RightTrigger) || WasKeyPressed (Keys.Space); 

Anrop Input.Update () i begynnelsen av GameRoot.Update () for inngangsklassen til arbeid.

Tips: Du kan legge merke til at jeg inkluderte en metode for bomber. Vi vil ikke implementere bomber nå, men den metoden er der for fremtidig bruk.

Du kan også legge merke til i GetMovementDirection () Jeg skrev direction.LengthSquared ()> 1. Ved hjelp av LengthSquared () er en liten ytelsesoptimalisering; beregning av torget av lengden er litt raskere enn å beregne lengden selv fordi den unngår den relativt sakte kvadratrotsoperasjonen. Du ser kode ved å bruke firkantene av lengder eller avstander i hele programmet. I dette tilfellet er ytelsesforskjellen ubetydelig, men denne optimaliseringen kan gjøre en forskjell når den brukes i stramme løkker.

flytting

Vi er nå klar til å gjøre skipet flytte. Legg til denne koden til PlayerShip.Update () metode:

 const float speed = 8; Hastighet = hastighet * Input.GetMovementDirection (); Posisjon + = Hastighet; Posisjon = Vector2.Clamp (Posisjon, Størrelse / 2, GameRoot.ScreenSize - Størrelse / 2); hvis (Velocity.LengthSquared ()> 0) Orientering = Velocity.ToAngle ();

Dette vil gjøre skipet flytte med en hastighet på opptil åtte piksler per ramme, klemme sin posisjon slik at den ikke kan gå utenom skjermen, og rotere skipet for å møte retningen det beveger seg.

ToAngle () er en enkel utvidelsesmetode som er definert i vår utvidelser klasse slik:

 offentlig statisk flyte ToAngle (denne Vector2 vektoren) return (float) Math.Atan2 (vector.Y, vector.X); 

skyting

Hvis du kjører spillet nå, bør du kunne fly skipet rundt. La oss få det til å skyte.

Først trenger vi en klasse for kuler.

 klasse Bullet: Entity offentlig Bullet (Vector2 posisjon, Vector2 hastighet) image = Art.Bullet; Posisjon = posisjon; Hastighet = hastighet; Orientering = Velocity.ToAngle (); Radius = 8;  Offentlig overstyring ugyldig Oppdatering () hvis (Velocity.LengthSquared ()> 0) Orientering = Velocity.ToAngle (); Posisjon + = Hastighet; // slette kuler som går utenfor skjermen hvis (! GameRoot.Viewport.Bounds.Contains (Position.ToPoint ())) IsExpired = true; 

Vi ønsker en kort nedkjølingsperiode mellom kuler, så legg til følgende felt i PlayerShip klasse.

 const int cooldownFrames = 6; int cooldownRemaining = 0; statisk Random rand = ny tilfeldig ();

Legg også til følgende kode til PlayerShip.Update ().

 var aim = Input.GetAimDirection (); hvis (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--;

Denne koden oppretter to kuler som reiser parallelt med hverandre. Det legger til en liten mengde tilfeldighet i retningen. Dette gjør at skuddene sprer seg ut som en maskinpistol. Vi legger til to tilfeldige tall sammen, fordi dette gjør at summen er mer sannsynlig å være sentrert (rundt null) og mindre sannsynlig å sende kuler langt unna. Vi bruker en kvaternion for å rotere den opprinnelige posisjonen til kulene i den retningen de reiser.

Vi brukte også to nye hjelpemetoder:

  • Random.NextFloat () returnerer en flyt mellom minimum og maksimum verdi.
  • MathUtil.FromPolar () skaper en Vektor2 fra en vinkel og størrelse.
 // i Extensions offentlig statisk float NextFloat (denne tilfeldig rand, float minValue, float maxValue) return (float) rand.NextDouble () * (maxValue - minValue) + minValue;  // i MathUtil statisk statisk Vector2 FromPolar (flytvinkel, flytestørrelse) returstørrelse * ny Vector2 ((float) Math.Cos (vinkel), (flyt) Math.Sin (vinkel)); 

Tilpasset markør

Det er en ting vi skal gjøre nå, da vi har Input klasse. La oss tegne en egendefinert musemarkør for å gjøre det lettere å se hvor skipet sikter. I GameRoot.Draw, bare tegne Art.Pointer på musens posisjon.

 spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (spriteBatch); // tegne egendefinerte musepekeren spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();

Konklusjon

Hvis du tester spillet nå, kan du flytte skipet rundt med WASD-tastene eller venstre tommelstikk, og sikte den kontinuerlige strømmen av kuler med piltastene, musen eller høyre tommelstikk.

I neste del, vil vi fullføre gameplayet ved å legge til fiender og en poengsum.