Î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, î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.
În seria de până acum am creat efecte de gameplay, bloom și particule. În această ultimă parte vom crea o grilă dinamică de bază.
Avertizare: Tare!Unul dintre cele mai bune efecte în Geometry Wars este grila de fond de bază. Vom examina cum să creăm un efect similar în Shape Blaster. Grila va reacționa la gloanțe, găuri negre și player respawning. Nu este greu de făcut și arată minunat.
Vom face grila folosind o simulare de primăvară. La fiecare intersecție a grilajului, vom pune o greutate mică și vom atașa un arc pe fiecare parte. Aceste izvoare vor trage și nu se vor împinge niciodată, la fel ca o bandă de cauciuc. Pentru a menține grila în poziție, masele de la marginea grila vor fi ancorate în poziție. Mai jos este o diagramă a aspectului.
Vom crea o clasă numită Grilă
pentru a crea acest efect. Cu toate acestea, înainte de a lucra la grila în sine, trebuie să facem două clase de ajutor: Primăvară
și PointMass
.
PointMass
clasa reprezintă masele la care vom atașa izvoarele. Arcurile nu se conectează direct la alte izvoare. În schimb, aplică o forță maselor pe care le conectează, care, la rândul lor, pot întinde alte izvoare.
clasa privată PointMass public Vector3 Position; public Vector3 Velocity; public float InverseMass; accelerația privată Vector3; amortizare privată a flotorului = 0,98 f; public PointMass (pozitia Vector3, float invMass) Position = position; InverseMass = invMass; void public ApplyForce (forța Vector3) accelerare + = forță * InverseMass; void publice IncreaseDamping (factor float) amortizare * = factor; public void Actualizare () Velocity + = accelerare; Poziția + = viteza; accelerare = Vector3.Zero; dacă (Velocity.LengthSquared () < 0.001f * 0.001f) Velocity = Vector3.Zero; Velocity *= damping; damping = 0.98f;
Există câteva puncte interesante despre această clasă. Mai întâi, observați că stochează invers din masa, 1 / masă
. Aceasta este adesea o idee bună în simulările fizicii deoarece ecuațiile fizicii tind să folosească mai des inversul masei și pentru că ne oferă o modalitate ușoară de a reprezenta obiecte infinit de grele și imobile prin stabilirea masei inverse la zero.
Clasa conține, de asemenea, a amortizare variabil. Aceasta este folosită aproximativ drept rezistență la frecare sau la aer. Acesta încetinește treptat masa în jos. Acest lucru face ca grila să ajungă în cele din urmă să se odihnească și, de asemenea, crește stabilitatea simulării primăverii.
Actualizați()
metoda face munca de a muta masa punctului fiecare cadru. Aceasta începe prin a face integrarea simulctică Euler, ceea ce înseamnă doar că adăugăm accelerația la viteză și apoi adăugăm viteza actualizată la poziție. Aceasta diferă de integrarea standard Euler în care am fi actualizat viteza după actualizarea poziției.
Bacsis: Simulare Euler este mai bine pentru simulările de primăvară pentru că conservă energia. Dacă utilizați integrarea obișnuită a Euler și creați izvoare fără amortizare, acestea vor avea tendința să se extindă mai mult și mai mult, fiecare săriți pe măsură ce câștigă energie, eventual rupându-vă simularea.
După actualizarea vitezei și a poziției, verificăm dacă viteza este foarte mică și, dacă este așa, vom seta la zero. Acest lucru poate fi important pentru performanță datorită naturii numerelor cu puncte plutitoare denormalizate.
(Atunci când numerele cu puncte de plutire devin foarte mici, ele folosesc o reprezentare specială numită denormal, ceea ce are avantajul de a permite floatului să reprezinte numere mai mici, dar vine la un preț, majoritatea chipset-urilor nu pot folosi operațiunile aritmetice standard pe denormalizate numere și în loc să le emulează folosind o serie de pași.Aceasta poate fi de zeci la sute de ori mai lent decât efectuarea operațiunilor cu numere de virgulă normalizate.În timp ce ne înmulțim viteza de factorul nostru de amortizare fiecare cadru, va deveni în cele din urmă foarte mici Nu ne pasă de viteze atât de mici, așa că l-am setat pur și simplu la zero.)
IncreaseDamping ()
metoda este utilizată pentru a mări temporar cantitatea de amortizare. Vom folosi acest lucru mai târziu pentru anumite efecte.
Un arc conectează două mase punctuale și, dacă este întins peste lungimea sa naturală, aplică o forță care trage masele împreună. Arcurile urmăresc o versiune modificată a legii lui Hooke cu amortizare:
\ [f = -kx-bv \]
Codul pentru Primăvară
clasa este după cum urmează.
private struct Spring public PointMass End1; public PointMass End2; public float TargetLength; flotarea publicului Rigiditate; amortizarea publicului plutitor; publicul primar (punctul1, end point1, end2 de tip PointMass, rigiditatea flotorului, amortizarea flotorului) End1 = end1; End2 = end2; Rigiditate = rigiditate; Amortizare = amortizare; TargetLength = Vector3.Distance (end1.Position, end2.Position) * 0.95f; public void Actualizare () var x = End1.Position - End2.Position; lungimea plutitorului = x.Lungime (); // aceste izvoare pot trage numai, nu pot împinge dacă (lungime <= TargetLength) return; x = (x / length) * (length - TargetLength); var dv = End2.Velocity - End1.Velocity; var force = Stiffness * x - dv * Damping; End1.ApplyForce(-force); End2.ApplyForce(force);
Când creăm un arc, setăm lungimea naturală a arcului să fie doar puțin mai mică decât distanța dintre cele două puncte de capăt. Acest lucru menține grilă întinsă chiar și atunci când este în repaus și îmbunătățește aspectul într-o oarecare măsură.
Actualizați()
metoda verifică mai întâi dacă arcul se întinde dincolo de lungimea sa naturală. Dacă nu este întins, nimic nu se întâmplă. Dacă este cazul, vom folosi Legea modificată a lui Hooke pentru a găsi forța din primăvară și ao aplica celor două mase conectate.
Acum că avem clasele necesare imbricate, suntem gata să creăm grila. Începem prin a crea PointMass
obiecte la fiecare intersecție pe grila. De asemenea, creăm niște ancore imobile PointMass
obiecte pentru a ține grila în poziție. Apoi, legăm masele cu izvoare.
Arcuri de primăvară; PointMass [,] puncte; grilă publică (dimensiunea dreptunghiului, spațierea Vector2) var springList = listă nouă (); int numColumns = (int) (dimensiune.Lățime / spațiu.X) + 1; int numRows = (int) (size.Height / spațiere.Y) + 1; puncte = punct de masă nou [numColumns, numRows]; // aceste puncte fixe vor fi utilizate pentru a ancora grila în poziții fixe pe ecran PointMass [,] fixedPoints = new PointMass [numColumns, numRows]; // creați masele punct int coloană = 0, rând = 0; pentru (float y = size.Top; y <= size.Bottom; y += spacing.Y) for (float x = size.Left; x <= size.Right; x += spacing.X) points[column, row] = new PointMass(new Vector3(x, y, 0), 1); fixedPoints[column, row] = new PointMass(new Vector3(x, y, 0), 0); column++; row++; column = 0; // link the point masses with springs for (int y = 0; y < numRows; y++) for (int x = 0; x < numColumns; x++) if (x == 0 || y == 0 || x == numColumns - 1 || y == numRows - 1) // anchor the border of the grid springList.Add(new Spring(fixedPoints[x, y], points[x, y], 0.1f, 0.1f)); else if (x % 3 == 0 && y % 3 == 0) // loosely anchor 1/9th of the point masses springList.Add(new Spring(fixedPoints[x, y], points[x, y], 0.002f, 0.02f)); const float stiffness = 0.28f; const float damping = 0.06f; if (x > 0) springList.Add (noua primăvară (puncte [x - 1, y], puncte [x, y], rigiditate, amortizare)); dacă (y> 0) springList.Add (noua primăvară (puncte [x, y - 1], puncte [x, y], rigiditate, amortizare)); springs = springList.ToArray ();
Primul pentru
buclă creează atât mase regulate cât și mase imobile la fiecare intersecție a rețelei. Nu vom folosi de fapt toate masele imobile, iar masele neutilizate vor fi colectate pur și simplu după ce constructorul se va termina. Am putea optimiza evitând crearea obiectelor inutile, dar din moment ce grila este de obicei creată doar o singură dată, nu va face prea multă diferență.
În plus față de utilizarea masei punctului de ancorare în jurul marginii rețelei, vom folosi și câteva mase de ancore în interiorul rețelei. Acestea vor fi utilizate pentru a ajuta foarte ușor să trageți grila înapoi în poziția inițială după ce ați fost deformate.
Întrucât punctele de ancorare nu se mișcă niciodată, nu trebuie să fie actualizate fiecare cadru. Putem pur și simplu să le prindem până la izvoare și să uităm de ele. Prin urmare, nu avem o variabilă membru în Grilă
pentru aceste mase.
Există o serie de valori pe care le puteți modifica în crearea rețelei. Cele mai importante sunt rigiditatea și amortizarea izvoarelor. Rigiditatea și amortizarea ancorelor de frontieră și a ancorelor interioare sunt stabilite independent de izvoarele principale. Valorile de rigiditate mai mari vor face ca arcurile să oscileze mai repede, iar valorile de amortizare mai mari vor determina încetinirea arcurilor mai repede.
Pentru ca grila să se miște, trebuie să o actualizăm în fiecare cadru. Acest lucru este foarte simplu, deoarece am făcut deja toată munca grea în PointMass
și Primăvară
clase.
public void Actualizare () foreach (var izvor în izvoare) spring.Update (); foreach (masa var în puncte) mass.Update ();
Acum vom adăuga câteva metode care manipulează grila. Puteți adăuga metode pentru orice fel de manipulare pe care vă puteți gândi. Vom implementa trei tipuri de manipulări aici: împingând o parte a grilei într-o anumită direcție, împingând grila spre exterior dintr-un anumit punct și trăgând grila spre un anumit punct. Toate cele trei vor afecta grila într-o anumită rază de la un punct țintă. Mai jos sunt câteva imagini ale acestor manipulări în acțiune.
void public ApplyDirectedForce (forța vector3, poziția Vector3, raza flotorului) foreach (masa var în puncte) dacă (Vector3.DistanceSquared (position, mass.Position) < radius * radius) mass.ApplyForce(10 * force / (10 + Vector3.Distance(position, mass.Position))); public void ApplyImplosiveForce(float force, Vector3 position, float radius) foreach (var mass in points) float dist2 = Vector3.DistanceSquared(position, mass.Position); if (dist2 < radius * radius) mass.ApplyForce(10 * force * (position - mass.Position) / (100 + dist2)); mass.IncreaseDamping(0.6f); public void ApplyExplosiveForce(float force, Vector3 position, float radius) foreach (var mass in points) float dist2 = Vector3.DistanceSquared(position, mass.Position); if (dist2 < radius * radius) mass.ApplyForce(100 * force * (mass.Position - position) / (10000 + dist2)); mass.IncreaseDamping(0.6f);
Vom folosi toate cele trei metode în Shape Blaster pentru diferite efecte.
Vom desena grila prin trasarea segmentelor de linie intre fiecare pereche de puncte invecinate. În primul rând, vom face o metodă de extensie SpriteBatch
care ne permite să desenăm segmente de linie luând o textură a unui singur pixel și întinzându-l într-o linie.
Deschide Artă
clasă și să declare o textură pentru pixel.
statică publică Texture2D Pixel get; set privat;
Puteți seta textura pixelilor în același mod în care am setat celelalte imagini sau puteți adăuga pur și simplu următoarele două linii la Art.Load ()
metodă.
Pixel = Texture2D nou (Player.GraphicsDevice, 1, 1); Pixel.SetData (nou [] Color.White);
Acest lucru creează pur și simplu o nouă textura de 1x1px și stabilește un singur pixel alb. Acum adăugați următoarea metodă în Extensii
clasă.
public void static DrawLine (acest SpriteBatch spriteBatch, Start Vector2, End Vector2, Culoare culoare, grosime float = 2f) Vector2 delta = end-start; spriteBatch.Draw (Art.Pixel, start, null, culoare, Delta.ToAngle (), Vector2 nou (0, 0.5f), Vector2 nou (delta.Length (), grosime), SpriteEffects.None, 0f);
Această metodă se întinde, rotește și dă textura pixelilor pentru a produce linia dorită.
Apoi, avem nevoie de o metodă de proiectare a punctelor rețelei 3D pe ecranul 2D. În mod normal, acest lucru se poate face folosind matrice, dar aici vom transforma manual coordonatele.
Adăugați următoarele la Grilă
clasă.
public Vector2 ToVec2 (Vector3 v) // face un factor de proiecție în perspectivă float = (v.Z + 2000) / 2000; retur (nou Vector2 (v.X, v.Y) - screenSize / 2f) * factor + screenSize / 2;
Această transformare va oferi rețelei o vedere în perspectivă în cazul în care punctele îndepărtate apar mai aproape împreună pe ecran. Acum putem trage grila prin iterarea prin rânduri și coloane și trasarea liniilor între ele.
public void Desenați (SpriteBatch spriteBatch) int width = points.GetLength (0); int înălțime = puncte.GetLength (1); Culoare culoare = culoare nou (30, 30, 139, 85); // albastru închis pentru (int y = 1; y < height; y++) for (int x = 1; x < width; x++) Vector2 left = new Vector2(), up = new Vector2(); Vector2 p = ToVec2(points[x, y].Position); if (x > 1) stânga = ToVec2 (puncte [x - 1, y]. Poziție); float grosime = y% 3 == 1? 3f: 1f; spriteBatch.DrawLine (stânga, p, culoare, grosime); dacă (y> 1) up = ToVec2 (puncte [x, y - 1]. Poziție); grosimea flotorului = x% 3 == 1? 3f: 1f; spriteBatch.DrawLine (sus, p, culoare, grosime);
În codul de mai sus, p
este punctul nostru actual pe grilă, stânga
este punctul direct în stânga și sus
este punctul direct deasupra lui. Desenăm fiecare al treilea rând mai gros atât pe orizontală cât și pe verticală pentru efect vizual.
Putem optimiza grila prin îmbunătățirea calității vizuale pentru un anumit număr de arcuri fără a crește în mod semnificativ costul de performanță. Vom face două astfel de optimizări.
Vom face grilarea mai densă adăugând segmente de linie în interiorul celulelor rețelei existente. Facem acest lucru prin trasarea liniilor de la mijlocul unei părți a celulei până la mijlocul părții opuse. Imaginea de mai jos prezintă liniile noi interpolate în roșu.
Desenarea liniilor interpolate este simplă. Dacă aveți două puncte, A
și b
, punctul lor de mijloc este (a + b) / 2
. Deci, pentru a desena linii interpolate, adaugam urmatorul cod in interiorul lui pentru
buclele noastre A desena()
metodă.
dacă (x> 1 && y> 1) Vector2 upLeft = ToVec2 (puncte [x - 1, y - 1]. Poziție); spriteBatch.DrawLine (0.5f * (upLeft + sus), 0.5f * (stânga + p), culoare, 1f); // linie verticală spriteBatch.DrawLine (0.5f * (upLeft + stânga), 0.5f * (până + p), culoare, 1f); // linie orizontală
Cea de-a doua îmbunătățire este de a efectua interpolarea pe segmentele noastre liniare pentru a le face în curbe mai netede. XNA oferă utilitatea Vector2.CatmullRom ()
care efectuează interpolarea Catmull-Rom. Transmiteți metoda patru puncte secvențiale pe o linie curbă și veți întoarce punctele de-a lungul unei curbe netede între al doilea și al treilea punct pe care l-ați furnizat.
Al cincilea argument pentru Vector2.CatmullRom ()
este un factor de ponderare care determină ce punct pe curba interpolată se întoarce. Factor de ponderare 0
sau 1
va reveni respectiv la al doilea sau al treilea punct pe care l-ați furnizat și la un factor de ponderare de 0.5
va întoarce punctul pe curba interpolată la jumătatea distanței dintre cele două puncte. Prin mutarea treptată a factorului de ponderare de la zero la unul și trasarea liniilor între punctele returnate, putem produce o curbă perfect netedă. Cu toate acestea, pentru a menține costul scăzut al performanței, vom lua în considerare doar un singur punct interpolat, cu un factor de ponderare de 0.5
. Apoi înlocuim linia dreaptă originală în rețea cu două linii care se întâlnesc la punctul interpolat.
Diagrama de mai jos prezintă efectul acestei interpolări.
Deoarece segmentele de linie din rețea sunt deja mici, utilizarea mai multor puncte interpolate, în general, nu face o diferență notabilă.
Adesea, liniile din grila noastră vor fi foarte drepte și nu vor necesita nici o netezire. Putem verifica acest lucru și evităm să tragem două linii în loc de una. Verificăm dacă distanța dintre punctul interpolat și punctul de mijloc al liniei drepte este mai mare decât un pixel. Dacă este, presupunem că linia este curbată și desenăm două segmente de linie. Modificarea la adresa noastră A desena()
metoda de adăugare a interpolației Catmull-Rom pentru liniile orizontale este prezentată mai jos.
stânga = ToVec2 (puncte [x - 1, y]. Poziție); float grosime = y% 3 == 1? 3f: 1f; // folosiți interpolarea Catmull-Rom pentru a ajuta la curbele netede în grilă int clampedX = Math.Min (x + 1, lățime - 1); Vector2 mijloc = Vector2.CatmullRom (ToVec2 (puncte [x - 2, y] .Position), stânga, p, ToVec2 (puncte [clampedX, y] .Position), 0.5f); // Dacă grila este foarte dreaptă aici, trageți o singură linie dreaptă. În caz contrar, trasați linii la // noi punct intermediar interpolat dacă (Vector2.DistanceSquared (mijloc, (stânga + p) / 2)> 1) spriteBatch.DrawLine (stânga, mijlocul, culoarea, grosimea); spriteBatch.DrawLine (mediu, p, culoare, grosime); altfel spriteBatch.DrawLine (stânga, p, culoare, grosime);
Imaginea de mai jos prezintă efectele de netezire. Un punct verde este desenat la fiecare punct interpolat pentru a ilustra mai bine unde liniile sunt netezite.
Acum este timpul să folosim grila în jocul nostru. Începem prin a declara un public, static Grilă
variabilă în GameRoot
și crearea rețelei în GameRoot.Initialize ()
metodă. Vom crea o rețea cu aproximativ 1600 de puncte.
const int maxGridPoints = 1600; Vector2 gridSpacing = Vector2 nou (float) Math.Sqrt (Viewport.Width * Viewport.Height / maxGridPoints)); Grila = noua Grilă (Viewport.Bounds, gridSpacing);
Apoi sunăm Grid.Update ()
și Grid.Draw ()
de la Actualizați()
și A desena()
metode în GameRoot
. Acest lucru ne va permite să vedem grila când conducem jocul. Totuși, trebuie să facem ca obiectele jocului să interacționeze cu grila.
Gloanțele vor respinge grila. Am făcut deja o metodă pentru a face acest lucru numit ApplyExplosiveForce ()
. Adăugați următoarea linie la Bullet.Update ()
metodă.
GameRoot.Grid.ApplyExplosiveForce (0.5f * Velocitate.Length (), Poziție, 80);
Acest lucru va face ca gloanțele să respingă grila proporțional cu viteza lor. A fost destul de ușor.
Acum să lucrăm la găurile negre. Adăugați această linie la BlackHole.Update ()
.
GameRoot.Grid.ApplyImplosiveForce (float) Math.Sin (sprayAngle / 2) * 10 + 20, Poziție, 200);
Acest lucru face gaura neagra suge in grila cu o forta variata de forta. Eu refolosesc sprayAngle
variabilă, ceea ce va determina forța pe rețea să pulseze în sincronizare cu unghiul pe care îl pulverizează particulele (deși la jumătate din frecvența datorată împărțirii cu două). Forța care a trecut va varia sinusoidal între 10 și 30.
În cele din urmă, vom crea o undă de șoc în rețea atunci când nava jucătorului respawns după moarte. Vom face acest lucru trăgând grilajul de-a lungul axei z și apoi permițând forței să se propagate și să sări prin arcuri. Din nou, aceasta necesită doar o mică modificare PlayerShip.Update ()
.
dacă (IsDead) if (--framesUntilRespawn == 0) GameRoot.Grid.ApplyDirectedForce (Vector3 nou (0, 0, 5000), Vector3 nou (Poziție, 0), 50); întoarcere;
Avem jocul de bază și efectele implementate. Depinde de tine sa il transformi intr-un joc complet si lustruit cu propria ta aroma. Încearcă să adaugi niște mecanisme noi interesante, câteva efecte reci noi sau o poveste unică. În cazul în care nu sunteți sigur de unde să începeți, iată câteva sugestii.
Vă mulțumim pentru lectură!