În această serie de tutoriale, vă voi arăta cum să faceți un împușcătură cu stick-uri neon, cum ar fi Geometry Wars, pe care o vom numi Shape Blaster, în XNA. Scopul acestor tutoriale nu este să vă lase o replică exactă a războaielor Geometry, ci mai degrabă să treceți peste elementele necesare care vă vor permite să creați propria varianta de înaltă calitate.
Vă încurajez să vă extindeți și să experimentați codul dat în aceste tutoriale. Vom aborda aceste subiecte în întreaga serie:
Iată ce vom avea până la sfârșitul seriei:
Avertizare: Tare!Iată ce vom avea până la sfârșitul acestei prime părți:
Avertizare: Tare!Muzica și efectele sonore pe care le puteți auzi în aceste videoclipuri au fost create de RetroModular și puteți citi despre cum a făcut acest lucru la Audiotuts+.
Spritele sunt de Jacob Zinman-Jeanes, designerul nostru rezident Tuts +. Toate lucrările pot fi găsite în fișierul sursă de descărcare.
Fontul este Piața Nova, de Wojciech Kalinowski.Să începem.
În acest tutorial vom crea un shooter cu două gemuri; jucătorul va controla nava cu tastatura, tastatura și mouse-ul sau cele două degete ale unui gamepad.
Utilizăm un număr de clase pentru a realiza acest lucru:
Entitate
: Clasa de bază pentru dușmani, gloanțe și nava jucătorului. Entitățile se pot deplasa și pot fi trase.Glonţ
și PlayerShip
.EntityManager
: Ține evidența tuturor entităților din joc și efectuează detectarea coliziunilor.Intrare
: Ajută la gestionarea intrărilor de la tastatură, mouse și gamepad.Artă
: Încarcă și deține referințe la texturile necesare jocului.Sunet
: Încarcă și deține referințe la sunete și muzică.MathUtil
și Extensii
: Conține câteva metode statice și metode de extensie utile.GameRoot
: Controlează bucla principală a jocului. Acesta este Game1
clasa XNA generează automat, redenumită.Codul din acest tutorial își propune să fie simplu și ușor de înțeles. Nu va avea fiecare caracteristică sau o arhitectură complicată concepută pentru a suporta toate posibilele nevoi. Mai degrabă, va face doar ceea ce trebuie să facă. Ținând-o simplu, vă va fi mai ușor să înțelegeți conceptele și apoi să le modificați și să le extindeți în propriul joc unic.
Creați un nou proiect XNA. Redenumiți Game1
clasa la ceva mai potrivit. L-am sunat GameRoot
.
Acum, să începem prin crearea unei clase de bază pentru entitățile noastre de joc.
clasa abstractă Entitatea textură protejată Texture2D; Tonul imaginii. Acest lucru ne va permite, de asemenea, să schimbăm transparența. protejat Culoare culoare = Culoare albă; Poziția publică Vector2, Velocity; public float Orientare; plutonul public flood = 20; // folosit pentru detecția coliziunilor circulare bool public IsExpired; // true dacă entitatea a fost distrusă și ar trebui eliminată. public Vector2 Mărime get return image == null? Vector2.Zero: Vector2 nou (image.Width, image.Height); void public abstract Update (); public virtual void Draw (SpriteBatch spriteBatch) spriteBatch.Draw (imagine, poziție, null, culoare, orientare, dimensiune / 2f, 1f, 0, 0);
Toate entitățile noastre (dușmani, gloanțe și nava jucătorului) au câteva proprietăți de bază, cum ar fi o imagine și o poziție. IsExpired
va fi folosit pentru a indica faptul că entitatea a fost distrusă și ar trebui eliminată din orice listă care deține o referință la ea.
Apoi vom crea un EntityManager
pentru a urmări entitățile noastre și pentru a le actualiza și desena.
clasa statică EntityManager listă staticăentități = Listă nouă (); static bool isUpdating; Listă statică addedEntities = Listă nouă (); static public int Count get return entities.Count; public static void Add (entitate entitate) if (! isUpdating) entities.Add (entitate); altceva adăugatEntiții.Add (entitate); void static public Update () isUpdating = true; foreach (entitate var în entități) entity.Update (); isUpdating = false; foreach (entitate var în entități adăugate) entities.Add (entitate); addedEntities.Clear (); // eliminați toate entitățile expirate. entități = entități.În cazul în care (x =>! x.IsExpired) .ToList (); void static public Draw (SpriteBatch spriteBatch) foreach (entitate var în entități) entity.Draw (spriteBatch);
Rețineți că, dacă modificați o listă în timp ce iterați peste ea, veți obține o excepție. Codul de mai sus se ocupă de acest lucru prin așteptarea oricăror entități adăugate în timpul actualizării într-o listă separată și adăugarea acestora după finalizarea actualizării entităților existente.
Va trebui să încărcăm niște texturi dacă vrem să tragem ceva. Vom face o clasă statică care să dețină referințe la texturile noastre.
clasa statică Art public static Texture2D Player get; set privat; public static Texture2D Seeker get; set privat; public static Texture2D Wanderer get; set privat; public static Texture2D Bullet get; set privat; public static Texture2D Pointer get; set privat; Încărcare nevalidă statică publică (conținut ContentManager) Player = content.Load("Jucător"); Seeker = content.Load ( "Cautator"); Wanderer = content.Load ("Rătăcitor"); Bullet = content.Load ("Glonţ"); Pointer = content.Load ( "Pointer");
Încărcați arta apelând Art.Load (Conținut)
în GameRoot.LoadContent ()
. De asemenea, un număr de clase va trebui să cunoască dimensiunile ecranului, deci adăugați următoarele proprietăți GameRoot
:
statica publica GameRoot Instance get; set privat; static Viewport Viewport get return instanță.GraphicsDevice.Viewport; public static Vector2 ScreenSize get retur nou Vector2 (Viewport.Width, Viewport.Height);
Și în GameRoot
constructor, adăugați:
Instanță = aceasta;
Acum vom începe să scriem PlayerShip
clasă.
clasa PlayerShip: Entitatea instanță privată Static PlayerShip; statică statică a stației de jucător get if (instance == null) instance = new PlayerShip (); instanță retur; Playerul privat () image = Art.Player; Poziție = GameRoot.ScreenSize / 2; Radius = 10; suprascrie public void Actualizare () // logica navei merge aici
Am făcut PlayerShip
un singur ton, și-a stabilit imaginea și la plasat în centrul ecranului.
În cele din urmă, să adăugăm nava jucătorului la EntityManager
și să îl actualizați și să îl desenați. Adăugați următorul cod în GameRoot
:
// în Initialize (), după apelul către base.Initialize () EntityManager.Add (PlayerShip.Instance); // în Actualizare () EntityManager.Update (); // în Draw () GraphicsDevice.Clear (Color.Black); spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additiv); EntityManager.Draw (spriteBatch); spriteBatch.End ();
Tragem cu spritele amestecarea aditivilor, care face parte din ceea ce le va da aspectul lor neon. Dacă executați jocul în acest moment, ar trebui să vă vedeți nava în centrul ecranului. Cu toate acestea, acesta nu răspunde încă la intrare. Să rezolvăm asta.
Pentru mișcare, player-ul poate folosi WASD de pe tastatură sau stânga de pe un jocpad. Pentru direcționare, pot folosi tastele săgeți, butonul drept al mouse-ului sau mouse-ul. Nu vom cere ca jucătorul să țină apăsat butonul mouse-ului pentru a trage, deoarece este inconfortabil să țineți apăsat butonul. Acest lucru ne lasă o mică problemă: de unde știm dacă jucătorul urmărește mouse-ul, tastatura sau gamepad-ul?
Vom folosi următorul sistem: vom adăuga împreună tastatură și gamepad. Dacă jucătorul mișcă mouse-ul, trecem la țintirea mouse-ului. Dacă jucătorul apasă tastele săgeată sau folosește clichetul drept, oprim scopul mouse-ului.
Un lucru de remarcat: împingând un clic înapoi va reveni a pozitiv y valoare. În coordonatele ecranului, valorile y cresc în jos. Vrem să inversăm axa y pe controler astfel încât împingerea în sus să ne orienteze sau să ne deplaseze spre partea superioară a ecranului.
Vom face o clasă statică pentru a urmări diferitele dispozitive de intrare și pentru a avea grijă de comutarea între diferitele tipuri de direcționare.
statică statică Input private static KeyboardState tastaturăState, lastKeyboardState; mouse-ul static static MouseStateState, lastMouseState; static privat GamePadState gamepadState, lastGamepadState; bootul static privat esteAimingWithMouse = false; public static Vector2 MousePosition primi returnează vectorul Vector2 (mouseState.X, mouseState.Y); void static public Update () lastKeyboardState = keyboardState; lastMouseState = mouseState; lastGamepadState = gamepadState; tastaturăState = tastatură.GetState (); mouseState = Mouse.GetState (); gamepadState = GamePad.GetState (PlayerIndex.One); // Dacă jucătorul a apăsat una din tastele săgeată sau dacă folosește un gamepad pentru a viza, dorim să dezactivați direcționarea mouse-ului. În caz contrar, // dacă jucătorul mișcă mouse-ul, activați mișcarea mouse-ului. dacă (noi [] Keys.Left, Keys.Right, Keys.Up, Keys.Down .A orice (x => keyboardState.IsKeyDown (x)) || gamepadState.ThumbSticks.Right! = Vector2.Zero) isAimingWithMouse = fals; altfel dacă (MousePosition! = nou Vector2 (lastMouseState.X, lastMouseState.Y)) isAimingWithMouse = true; // Verifică dacă o tastă a fost doar apăsată pe starea de boot public WasKeyPressed (Chei chei) return lastKeyboardState.IsKeyUp (key) && keyboardState.IsKeyDown (cheie); boot static public WasButtonPressed (Buton buton) return lastGamepadState.IsButtonUp (buton) && gamepadState.IsButtonDown (buton); statică publică Vector2 GetMovementDirection () Vector2 direction = gamepadState.ThumbSticks.Left; direcția.Y * = -1; // inversează axa y dacă (keyboardState.IsKeyDown (Keys.A)) direction.X - = 1; dacă (keyboardState.IsKeyDown (Keys.D)) direcția.X + = 1; dacă (keyboardState.IsKeyDown (Keys.W)) direction.Y - = 1; dacă (keyboardState.IsKeyDown (Keys.S)) direcția.Y + = 1; // Strângeți lungimea vectorului la un maxim de 1. dacă (direction.LengthSquared ()> 1) direction.Normalize (); direcția de întoarcere; statică publică Vector2 GetAimDirection () if (isAimingWithMouse) returnează GetMouseAimDirection (); Vector2 direcție = gamepadState.ThumbSticks.Right; direcția.Y * = -1; dacă (keyboardState.IsKeyDown (Keys.Left)) direction.X - = 1; dacă (keyboardState.IsKeyDown (Keys.Right)) direcția.X + = 1; dacă (keyboardState.IsKeyDown (Keys.Up)) direcția.Y - = 1; dacă (keyboardState.IsKeyDown (Keys.Down)) direcția.Y + = 1; // Dacă nu există nicio intrare a scopului, reveniți la zero. Altfel, normalizați direcția pentru a avea o lungime de 1. dacă (direcția == Vector2.Zero) returnează Vector2.Zero; altul returnează Vector2.Normalize (direcție); privat static Vector2 GetMouseAimDirection () Vector2 direcție = MousePosition - PlayerShip.Instance.Position; dacă (direcția == Vector2.Zero) returnează Vector2.Zero; altul returnează Vector2.Normalize (direcție); boot static public WasBombButtonPressed () return WasButtonPressed (Buttons.LeftTrigger) || WasButtonPressed (Buttons.RightTrigger) || WasKeyPressed (Keys.Space);
Apel Input.Update ()
la inceputul GameRoot.Update ()
pentru ca clasa de intrare să funcționeze.
Bacsis: S-ar putea să observați că am inclus o metodă pentru bombe. Nu vom pune în aplicare acum bombe, dar acea metodă este acolo pentru utilizare ulterioară.
Puteți observa, de asemenea, în GetMovementDirection ()
Am scris direction.LengthSquared ()> 1
. Utilizarea LengthSquared ()
este o optimizare de performanță mică; calculând pătratul lungimii este un pic mai rapid decât calculul lungimii în sine pentru că evită funcționarea relativ lentă a rădăcinii pătrate. Veți vedea codul folosind pătratele de lungimi sau distanțe pe tot parcursul programului. În acest caz particular, diferența de performanță este neglijabilă, dar această optimizare poate avea o diferență atunci când este utilizată în bucle stricte.
Suntem gata să facem nava să se miște. Adăugați acest cod la PlayerShip.Update ()
metodă:
const viteza flotorului = 8; Viteza = viteza * Input.GetMovementDirection (); Poziția + = viteza; Poziție = Vector2.Clamp (Poziție, Dimensiune / 2, GameRoot.ScreenSize - Dimensiune / 2); dacă (Velocity.LengthSquared ()> 0) Orientation = Velocity.ToAngle ();
Aceasta va face ca nava să se deplaseze cu o viteză de până la opt pixeli pe cadru, să își fixeze poziția astfel încât să nu poată ieși din ecran și să rotească nava pentru a se îndrepta spre direcția în care se mișcă.
ToAngle ()
este o metodă de extensie simplă definită în versiunea noastră Extensii
clasa ca atare:
floare statică publică ToAngle (vector Vector2) return (float) Math.Atan2 (vector.Y, vector.X);
Dacă conduci jocul acum, ar trebui să poți zbura nava în jurul tău. Acum să facem să tragem.
În primul rând, avem nevoie de o clasă pentru gloanțe.
Clasa Bullet: Entitatea Bullet public (poziția Vector2, viteza Vector2) image = Art.Bullet; Poziție = poziție; Viteza = viteza; Orientare = Velocity.ToAngle (); Radius = 8; suprascrie public void Actualizare () if (Velocity.LengthSquared ()> 0) Orientation = Velocity.ToAngle (); Poziția + = viteza; // ștergeți gloanțele care vor ieși din ecran dacă (! GameRoot.Viewport.Bounds.Contains (Position.ToPoint ())) IsExpired = true;
Vrem o scurtă perioadă de cooldown între gloanțe, deci adăugați următoarele câmpuri la PlayerShip
clasă.
const int cooldownFrames = 6; int cooldownRemaining = 0; static Rand rand = nou Random ();
De asemenea, adăugați următorul cod la PlayerShip.Update ()
.
var țintă = Input.GetAimDirection (); dacă (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--;
Acest cod creează două gloanțe care călătoresc paralele între ele. Se adaugă o cantitate mică de aleatorie în direcție. Acest lucru face ca loviturile să se împrăștie puțin ca un mitralieră. Adăugăm două numere aleatorii împreună, deoarece acest lucru face probabil ca suma lor să fie centrat (în jurul valorii de zero) și este mai puțin probabil să trimită gloanțe la distanță. Utilizăm un quaternion pentru a roti poziția inițială a gloanțelor în direcția în care călătoresc.
De asemenea, am folosit două noi metode de ajutor:
Random.NextFloat ()
returnează un flotor între valoarea minimă și cea maximă.MathUtil.FromPolar ()
creează un Vector2
dintr-un unghi și amploare.// in Extensions float static public NextFloat (acest Random rand, float minValue, float maxValue) retur (float) rand.NextDouble () * (maxValue - minValue) + minValue; // în MathUtil static public Vector2 FromPolar (unghi de flotare, magnitudine flotantă) întoarcere magnitudine * new Vector2 (float) Math.Cos (unghi), (float) Math.Sin (unghi));
Mai avem încă un lucru pe care trebuie să-l facem acum Intrare
clasă. Să desenați un cursor personalizat pentru a face mai ușor să vedeți unde nava vizează. În GameRoot.Draw
, pur și simplu trageți Art.Pointer
la poziția mouse-ului.
spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additiv); EntityManager.Draw (spriteBatch); // trageți cursorul mouse-ului personalizat spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();
Dacă testați jocul acum, veți putea să mutați nava în jurul valorii de tastele WASD sau pe stânga și să direcționați fluxul continuu de gloanțe cu tastele săgeți, mouse-ul sau cu degetele drept.
În partea următoare, vom completa gameplay-ul adăugând dușmani și un scor.