In deze serie tutorials laat ik je zien hoe je een Neon Twin Stick-shooter zoals Geometry Wars maakt, die we Shape Blaster zullen noemen, 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.
Ik moedig u aan om uit te breiden en te experimenteren met de code in deze tutorials. We behandelen deze onderwerpen in de serie:
Dit is wat we zullen hebben aan het einde van de serie:
Waarschuwing: Loud!En hier is wat we zullen hebben aan het einde van dit eerste deel:
Waarschuwing: Loud!De muziek en geluidseffecten die je in deze video's kunt horen, zijn gemaakt door RetroModular en je kunt lezen hoe hij dat deed bij Audiotuts+.
De sprites zijn van Jacob Zinman-Jeanes, onze residente Tuts + -ontwerper. Alle illustraties zijn te vinden in de zip-download van het bronbestand.
Het lettertype is Nova Square, door Wojciech Kalinowski.Laten we beginnen.
In deze tutorial zullen we een tweepijps shooter creëren; de speler bestuurt het schip met het toetsenbord, het toetsenbord en de muis, of met de twee thumbsticks van een gamepad.
We gebruiken een aantal klassen om dit te bereiken:
Entiteit
: De basisklasse voor vijanden, kogels en het schip van de speler. Entiteiten kunnen bewegen en getekend worden.Kogel
en PlayerShip
.EntityManager
: Houdt alle entiteiten in het spel bij en voert botsingsdetectie uit.Invoer
: Helpt de invoer van toetsenbord, muis en gamepad te beheren.Kunst
: Laadt en bevat verwijzingen naar de texturen die nodig zijn voor het spel.Geluid
: Laadt en bevat verwijzingen naar de geluiden en muziek.MathUtil
en uitbreidingen
: Bevat enkele nuttige statische methoden en uitbreidingsmethoden.GameRoot
: Regelt de hoofdlus van het spel. Dit is de game1
klasse XNA genereert automatisch, hernoemd.De code in deze zelfstudie is bedoeld om eenvoudig en gemakkelijk te begrijpen te zijn. Het zal niet beschikken over alle functies of een ingewikkelde architectuur die is ontworpen om elke mogelijke behoefte te ondersteunen. Integendeel, het zal alleen doen wat het moet doen. Door het eenvoudig te houden, wordt het voor u eenvoudiger om de concepten te begrijpen en vervolgens te wijzigen en uit te breiden naar uw eigen unieke spel.
Maak een nieuw XNA-project. Hernoem de game1
naar iets dat meer geschikt is. Ik heb het gebeld GameRoot
.
Laten we nu beginnen met het maken van een basisklasse voor onze game-entiteiten.
abstracte klasse Entity protected Texture2D image; // De tint van de afbeelding. Dit zal ons ook in staat stellen om de transparantie te veranderen. beschermd Kleur kleur = Kleur.Wit; openbare Vector2-positie, Velocity; openbare zwevende oriëntatie; openbare vlotter Straal = 20; // gebruikt voor circulaire botsingherkenning public bool IsExpired; // true als de entiteit vernietigd is en moet worden verwijderd. public Vector2 Size krijg return image == null? Vector2.Zero: new Vector2 (image.Width, image.Height); public abstract void Update (); public virtual void Draw (SpriteBatch spriteBatch) spriteBatch.Draw (afbeelding, Positie, null, kleur, Orientatie, Grootte / 2f, 1f, 0, 0);
Al onze entiteiten (vijanden, kogels en het schip van de speler) hebben enkele basiseigenschappen zoals een afbeelding en een positie. Is verlopen
wordt gebruikt om aan te geven dat de entiteit vernietigd is en moet worden verwijderd van lijsten die er een verwijzing naar bevatten.
Vervolgens maken we een EntityManager
om onze entiteiten te volgen en ze bij te werken en te tekenen.
static class EntityManager static Listentiteiten = nieuwe lijst (); static bool isUpdating; statische lijst addedEntities = nieuwe lijst (); public static int Count krijg return entities.Count; public static void Add (Entity entity) if (! isUpdating) entities.Add (entity); else addedEntities.Add (entity); public static void Update () isUpdating = true; foreach (var entity in entities) entity.Update (); isUpdating = false; foreach (var entity in added Entities) entities.Add (entity); addedEntities.Clear (); // verwijder vervallen entiteiten. entities = entities.Where (x =>! x.IsExpired) .ToList (); public static void Draw (SpriteBatch spriteBatch) foreach (var entity in entities) entity.Draw (spriteBatch);
Let op: als u een lijst wijzigt terwijl u er overheen werkt, krijgt u een uitzondering. De bovenstaande code zorgt ervoor door alle entiteiten die tijdens het bijwerken zijn toegevoegd in een aparte lijst in de wachtrij te plaatsen en ze toe te voegen nadat het bijwerken van de bestaande entiteiten is voltooid.
We zullen wat texturen moeten laden als we iets willen tekenen. We zullen een statische klasse maken om verwijzingen naar al onze texturen te houden.
statische klasse Art public static Texture2D Player get; privé set; public static Texture2D Seeker get; privé set; public static Texture2D Wanderer get; privé set; public static Texture2D Bullet get; privé set; public static Texture2D Pointer get; privé set; public static void Load (inhoud ContentManager) Player = content.Load("Speler"); Seeker = content.Load ( "Zoeker"); Wanderer = content.Load ("Zwerver"); Bullet = content.Load ("Kogel"); Pointer = content.Load ("Wijzer");
Laad de kunst door te bellen Art.Load (Content)
in GameRoot.LoadContent ()
. Ook moet een aantal klassen de schermafmetingen weten, dus voeg de volgende eigenschappen toe aan GameRoot
:
openbare statische GameRoot-instantie get; privé set; public static Viewport Viewport krijg return Instance.GraphicsDevice.Viewport; public static Vector2 ScreenSize krijg return new Vector2 (Viewport.Width, Viewport.Height);
En in de GameRoot
constructor, voeg toe:
Instance = this;
Nu gaan we beginnen met het schrijven van de PlayerShip
klasse.
class PlayerShip: Entity private static PlayerShip-instantie; public static PlayerShip Instance get if (instance == null) instance = nieuw PlayerShip (); terugkeer instantie; private PlayerShip () image = Art.Player; Positie = GameRoot.ScreenSize / 2; Straal = 10; openbare overschrijving ongeldig Update () // scheepslogica gaat hier
We maakten PlayerShip
een singleton, stel het beeld in en plaatste het in het midden van het scherm.
Laten we tot slot het spelersschip toevoegen aan de EntityManager
en werk het bij en teken het. Voeg de volgende code toe in GameRoot
:
// in Initialize (), na de aanroep naar base.Initialize () EntityManager.Add (PlayerShip.Instance); // in Update () EntityManager.Update (); // in Draw () GraphicsDevice.Clear (Color.Black); spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (spriteBatch); spriteBatch.End ();
We tekenen de sprites met additief mengen, wat deel uitmaakt van wat hen hun neon-look zal geven. Als je het spel nu uitvoert, zou je je schip in het midden van het scherm moeten zien. Het reageert echter nog niet op invoer. Laten we dat oplossen.
Voor beweging kan de speler WASD gebruiken op het toetsenbord of de linker thumbstick op een gamepad. Voor het richten kunnen ze de pijltjestoetsen, de rechter thumbstick of de muis gebruiken. We zullen niet vereisen dat de speler de muisknop ingedrukt houdt om te schieten, omdat het ongemakkelijk is om de knop voortdurend ingedrukt te houden. Dit laat ons een klein probleem: hoe weten we of de speler mikt met de muis, het toetsenbord of de gamepad?
We gebruiken het volgende systeem: we voegen toetsenbord- en gamepadinvoer samen toe. Als de speler de muis beweegt, schakelen we over naar richten van de muis. Als de speler op de pijltoetsen drukt of de rechter thumbstick gebruikt, schakelen we het richten van de muis uit.
Een ding om op te merken: als u een thumbstick naar voren duwt, keert u terug positief y-waarde. In schermcoördinaten nemen de y-waarden toe naar beneden. We willen de y-as op de controller omdraaien zodat het omhoog duwen van de thumbstick ons zal richten of ons naar de bovenkant van het scherm zal brengen.
We zullen een statische klasse maken om de verschillende invoerapparaten bij te houden en zorgen voor het schakelen tussen de verschillende soorten richten.
static class Input private static KeyboardState keyboardState, lastKeyboardState; privé statisch MuisState mouseState, lastMouseState; privé statische GamePadState gamepadState, lastGamepadState; private static bool isAimingWithMouse = false; public static Vector2 MousePosition krijg return new Vector2 (mouseState.X, mouseState.Y); public static void Update () lastKeyboardState = keyboardState; lastMouseState = mouseState; lastGamepadState = gamepadState; keyboardState = Keyboard.GetState (); mouseState = Mouse.GetState (); gamepadState = GamePad.GetState (PlayerIndex.One); // Als de speler op een van de pijltoetsen heeft gedrukt of een gamepad gebruikt om te richten, willen we het richten van de muis uitschakelen. Anders, // als de speler de muis beweegt, schakelt u het richten van de muis in. if (nieuw [] Keys.Left, Keys.Right, Keys.Up, Keys.Down .Any (x => keyboardState.IsKeyDown (x)) || gamepadState.ThumbSticks.Right! = Vector2.Zero) isAimingWithMouse = false; else if (MousePosition! = new Vector2 (lastMouseState.X, lastMouseState.Y)) isAingingWithMouse = true; // Controleert of een sleutel zojuist is ingedrukt. Public static bool WasKeyPressed (Keys-toets) return lastKeyboardState.IsKeyUp (key) && keyboardState.IsKeyDown (key); public static bool WasButtonPressed (knop Knoppen) retourneer laatsteGamepadState.IsButtonUp (knop) && gamepadState.IsButtonDown (knop); public static Vector2 GetMovementDirection () Vector2 direction = gamepadState.ThumbSticks.Left; direction.Y * = -1; // draai de y-as om als (keyboardState.IsKeyDown (Keys.A)) direction.X - = 1; if (keyboardState.IsKeyDown (Keys.D)) direction.X + = 1; if (keyboardState.IsKeyDown (Keys.W)) direction.Y - = 1; if (keyboardState.IsKeyDown (Keys.S)) direction.Y + = 1; // Klem de lengte van de vector in tot maximaal 1. if (direction.LengthSquared ()> 1) direction.Normalize (); terugkeer richting; public static Vector2 GetAimDirection () if (isAimingWithMouse) retourneer GetMouseAimDirection (); Vector2 richting = gamepadState.ThumbSticks.Right; direction.Y * = -1; if (keyboardState.IsKeyDown (Keys.Left)) direction.X - = 1; if (keyboardState.IsKeyDown (Keys.Right)) direction.X + = 1; if (keyboardState.IsKeyDown (Keys.Up)) direction.Y - = 1; if (keyboardState.IsKeyDown (Keys.Down)) direction.Y + = 1; // Als er geen doelinvoer is, geeft u nul terug. Anders normaliseer de richting om een lengte van 1 te hebben als if (direction == Vector2.Zero) Vector2.Zero retourneert; anders retourneer Vector2.Normalize (richting); private static Vector2 GetMouseAimDirection () Vector2 direction = MousePosition - PlayerShip.Instance.Position; als (richting == Vector2.Zero) Vector2.Zero retourneert; anders retourneer Vector2.Normalize (richting); public static bool WasBombButtonPressed () keer terug WasButtonPressed (Buttons.LeftTrigger) || WasButtonPressed (Buttons.RightTrigger) || WasKeyPressed (Keys.Space);
telefoontje Input.Update ()
aan het begin van GameRoot.Update ()
om de invoerklasse te laten werken.
Tip: Je merkt misschien dat ik een methode voor bommen heb opgenomen. We zullen nu geen bommen implementeren, maar die methode is er voor toekomstig gebruik.
Je merkt het misschien ook in GetMovementDirection ()
ik schreef direction.LengthSquared ()> 1
. Gebruik makend van LengthSquared ()
is een kleine prestatie-optimalisatie; het berekenen van het kwadraat van de lengte is een beetje sneller dan het berekenen van de lengte zelf, omdat het de relatief langzame vierkantswortelbewerking vermijdt. Je ziet code met de vierkanten van lengtes of afstanden in het hele programma. In dit specifieke geval is het prestatieverschil te verwaarlozen, maar deze optimalisatie kan een verschil maken wanneer gebruikt in strakke lussen.
We zijn nu klaar om het schip te laten bewegen. Voeg deze code toe aan de PlayerShip.Update ()
methode:
const float speed = 8; Snelheid = snelheid * Input.GetMovementDirection (); Positie + = snelheid; Positie = Vector2.Clamp (Positie, Grootte / 2, GameRoot.ScreenSize - Grootte / 2); if (Velocity.LengthSquared ()> 0) Orientation = Velocity.ToAngle ();
Hierdoor zal het schip met een snelheid van maximaal acht pixels per frame bewegen, zijn positie vastzetten zodat het niet van het scherm kan gaan en het schip draaien in de richting waarin het beweegt.
ToAngle ()
is een eenvoudige uitbreidingsmethode die is gedefinieerd in onze uitbreidingen
klasse zoals zo:
public static float ToAngle (deze vector2 vector) return (float) Math.Atan2 (vector.Y, vector.X);
Als je het spel nu uitvoert, zou je in staat moeten zijn om het schip rond te vliegen. Laten we het nu laten schieten.
Ten eerste hebben we een klasse nodig voor kogels.
class Bullet: Entity public Bullet (Vector2 position, Vector2 velocity) image = Art.Bullet; Positie = positie; Velocity = velocity; Oriëntatie = Velocity.ToAngle (); Straal = 8; openbare overschrijving ongeldig Update () if (Velocity.LengthSquared ()> 0) Orientation = Velocity.ToAngle (); Positie + = snelheid; // verwijder kogels die van het scherm gaan als (! GameRoot.Viewport.Bounds.Contains (Position.ToPoint ())) IsExpired = true;
We willen een korte afkoelperiode tussen kogels, dus voeg de volgende velden toe aan de PlayerShip
klasse.
const int cooldownFrames = 6; int cooldownRemaining = 0; statisch Willekeurige rand = nieuw Willekeurig ();
Voeg ook de volgende code toe aan PlayerShip.Update ()
.
var aim = Input.GetAimDirection (); if (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--;
Deze code maakt twee kogels die parallel aan elkaar lopen. Het voegt een kleine hoeveelheid willekeurigheid toe aan de richting. Dit zorgt ervoor dat de schoten zich een beetje verspreiden als een machinegeweer. We voegen twee willekeurige getallen bij elkaar, omdat dit hun som waarschijnlijk gecentreerd maakt (rond nul) en minder waarschijnlijk kogels ver weg stuurt. We gebruiken een quaternion om de beginpositie van de kogels in de richting waarin ze reizen te roteren.
We hebben ook twee nieuwe hulpmethoden gebruikt:
Random.NextFloat ()
retourneert een float tussen een minimum- en een maximumwaarde.MathUtil.FromPolar ()
creëert een Vector2
vanuit een hoek en magnitude.// in uitbreidingen public static float NextFloat (deze willekeurige rand, minValue zweven, maxValue zweven) return (float) rand.NextDouble () * (maxValue - minValue) + minValue; // in MathUtil public static Vector2 FromPolar (float angle, float magnitude) return magnitude * new Vector2 ((float) Math.Cos (angle), (float) Math.Sin (angle));
Er is nog een ding dat we nu moeten doen, dat we de Invoer
klasse. Laten we een aangepaste muiscursor tekenen zodat u gemakkelijker kunt zien waar het schip naartoe gaat. In GameRoot.Draw
, gewoon tekenen Art.Pointer
op de positie van de muis.
spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (spriteBatch); // teken de aangepaste muiscursor spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();
Als je het spel nu test, kun je het schip verplaatsen met de WASD-toetsen of de linker thumbstick en de continue stroom van kogels richten met de pijltjestoetsen, de muis of de rechter thumbstick.
In het volgende deel voltooien we het spel door vijanden en een score toe te voegen.