I denne serien av opplæringsprogrammer, viser jeg deg hvordan du lager en neon tvillingpinne shooter, som Geometry Wars, 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.
I serien så langt har vi laget spill, blomst og partikkeleffekter. I denne siste delen vil vi skape et dynamisk, krøllende bakgrunnsnett.
Advarsel: Høyt!En av de kuleste effektene i Geometry Wars er det klingende bakgrunnsnettet. Vi undersøker hvordan du lager en lignende effekt i Shape Blaster. Gitteret vil reagere på kuler, svarte hull og spilleren respekterer. Det er ikke vanskelig å lage og det ser fantastisk ut.
Vi lager nettverket ved hjelp av en vårsimulering. Ved hvert skjæringspunkt av nettverket legger vi en liten vekt og legger en fjær på hver side. Disse fjærene vil bare trekke og aldri skyve, som et gummibånd. For å holde rutenettet på plass, vil massene ved grensen til ruten bli forankret på plass. Nedenfor er et diagram over oppsettet.
Vi lager en klasse som heter Nett
å skape denne effekten. Men før vi jobber på nettet selv, må vi lage to hjelperklasser: Vår
og PointMass
.
De PointMass
klassen representerer massene som vi vil knytte fjærene til. Fjærer forbinder aldri direkte med andre fjærer. I stedet bruker de en kraft til massene de forbinder, som igjen kan strekke andre fjærer.
privat klasse PointMass offentlig Vector3 posisjon; offentlig Vector3 Velocity; offentlig float InverseMass; privat Vector3 akselerasjon; privat flottørdemping = 0,98f; offentlig PointMass (Vector3 posisjon, float invMass) Posisjon = posisjon; InverseMass = invMass; offentlig ugyldig ApplyForce (Vector3 force) akselerasjon + = kraft * InverseMass; offentlig tomgang IncreaseDamping (float factor) demping * = faktor; offentlig ugyldig oppdatering () Velocity + = akselerasjon; Posisjon + = Hastighet; akselerasjon = Vector3.Zero; hvis (Velocity.LengthSquared () < 0.001f * 0.001f) Velocity = Vector3.Zero; Velocity *= damping; damping = 0.98f;
Det er noen interessante poeng om denne klassen. Først legg merke til at det lagrer omvendt av massen, 1 / masse
. Dette er ofte en god ide i fysikk simuleringer fordi fysikk likninger pleier å bruke den inverse av massen oftere, og fordi det gir oss en enkel måte å representere uendelig tunge, faste objekter ved å sette den inverse massen til null.
Klassen inneholder også a demping variabel. Dette brukes omtrent som friksjon eller luftmotstand. Det setter gradvis ned massen. Dette bidrar til at gridet til slutt kommer til hvile og øker også stabiliteten til vårens simulering.
De Oppdater()
Metoden gjør arbeidet med å flytte punktmassen hver ramme. Det begynner med å simulere Euler-integrasjon, som bare betyr at vi legger til akselerasjonen til hastigheten og deretter legger til den oppdaterte hastigheten til stillingen. Dette er forskjellig fra standard Euler-integrasjon der vi vil oppdatere hastigheten etter oppdatering av stillingen.
Tips: Symplegisk Euler er bedre for vårsimuleringer fordi den sparer energi. Hvis du bruker vanlig Euler-integrasjon og lager fjærer uten demping, vil de strekke seg lenger og lenger hver sprette etter hvert som de får energi, til slutt bryter simuleringen din.
Etter å ha oppdatert hastigheten og posisjonen, kontrollerer vi om hastigheten er svært liten, og hvis så setter vi den til null. Dette kan være viktig for ytelsen på grunn av arten av denormaliserte flytende punktnumre.
(Når flytende poeng blir svært små, bruker de en spesiell representasjon kalt et denormal nummer. Dette har fordelen av at float kan representere mindre tall, men det kommer til en pris. De fleste brikkesettene kan ikke bruke sine vanlige aritmetiske operasjoner på deormaliserte tall og i stedet må etterligne dem ved hjelp av en rekke trinn. Dette kan være titalls hundrevis tregere enn å utføre operasjoner på normaliserte flytpunktstall. Da vi multipliserer vår hastighet med vår demperfaktor hver ramme, vil den etter hvert bli svært liten . Vi bryr oss egentlig ikke om slike små hastigheter, så vi stiller det til null.)
De IncreaseDamping ()
Metoden brukes til midlertidig å øke mengden demping. Vi vil bruke dette senere for visse effekter.
En fjær knytter to punktmasser, og hvis den strekkes forbi dens naturlige lengde, gjelder en kraft som trekker massene sammen. Fjærer følger en modifisert versjon av Hooke's Law med demping:
\ [f = -kx - bv \]
Koden for Vår
klassen er som følger.
private struct Våren offentlig PointMass End1; offentlig PointMass End2; offentlig float TargetLength; offentlig float Stivhet; offentlig float Damping; offentlig vår (PointMass end1, PointMass end2, float stivhet, float demping) End1 = end1; End2 = end2; Stivhet = stivhet; Damping = demping TargetLength = Vector3.Distance (end1.Position, end2.Position) * 0.95f; offentlig ugyldig oppdatering () var x = End1.Position - End2.Position; flytlengde = x.Length (); // disse fjærene kan bare trekke, ikke trykk hvis (lengde <= 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);
Når vi lager en fjær, setter vi vårens naturlige lengde til bare litt mindre enn avstanden mellom de to endepunktene. Dette holder rutenettet stramt selv når du er i ro og forbedrer utseendet noe.
De Oppdater()
Metoden kontrollerer først om våren strekkes utover sin naturlige lengde. Hvis den ikke strekkes, skjer ingenting. Hvis det er, bruker vi den modifiserte Hooke's Law til å finne kraften fra våren og bruke den til de to forbundne massene.
Nå som vi har de nødvendige nestede klassene, er vi klare til å lage nettverket. Vi begynner med å lage PointMass
gjenstander ved hvert skjæringspunkt på rutenettet. Vi lager også noen fast anker PointMass
gjenstander for å holde rutenettet på plass. Vi knytter sammen massene med fjærer.
Våren fjærer; PointMass [,] poeng; offentlig grid (rektangelstørrelse, vektor2 mellomrom) var springList = ny liste (); int numColumns = (int) (size.Width / spacing.X) + 1; int numRows = (int) (size.Height / spacing.Y) + 1; poeng = nytt PointMass [numColumns, numRows]; // disse faste punktene vil bli brukt til å forankre rutenettet til faste posisjoner på skjermen PointMass [,] fixedPoints = new PointMass [numColumns, numRows]; // lag poengmassene int kolonne = 0, rad = 0; for (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 (ny vår (poeng [x - 1, y], poeng [x, y], stivhet, demping)); hvis (y> 0) springList.Add (ny vår (poeng [x, y - 1], poeng [x, y], stivhet, demping)); fjærer = springList.ToArray ();
Den første til
sløyfe skaper både vanlige masser og faste masser ved hvert skjæringspunkt av rutenettet. Vi vil ikke faktisk bruke alle de faste massene, og de ubrukte massene vil rett og slett bli søppel samlet etter at konstruktøren ender. Vi kan optimalisere ved å unngå å skape unødvendige objekter, men siden rutenettet vanligvis bare opprettes en gang, vil det ikke gjøre mye forskjell.
I tillegg til å bruke ankerpunktsmasser rundt grensen til rutenettet, vil vi også bruke noen ankermasser inne i rutenettet. Disse vil bli brukt til å forsiktig trekke rutenettet tilbake til sin opprinnelige posisjon etter å ha blitt deformert.
Siden ankerpunktene aldri beveger seg, trenger de ikke å bli oppdatert hver ramme. Vi kan bare koble dem opp til fjærene og glemme dem. Derfor har vi ikke en medlemsvariabel i Nett
klasse for disse massene.
Det finnes en rekke verdier du kan finjustere i etableringen av nettet. De viktigste er stivhet og demping av fjærene. Stivheten og dempingen av grenseankrene og indre ankre settes uavhengig av hovedfjærene. Høyere stivhetsverdier vil gjøre fjærene oscillere raskere, og høyere dempingsverdier vil føre til at fjærene senkes raskere.
For at nettverket skal bevege seg, må vi oppdatere det hver ramme. Dette er veldig enkelt som vi allerede gjorde alt hardt arbeid i PointMass
og Vår
klasser.
offentlig ugyldig oppdatering () foreach (var våren i fjærer) spring.Update (); foreach (var masse i poeng) masse.Update ();
Nå vil vi legge til noen metoder som manipulerer rutenettet. Du kan legge til metoder for enhver form for manipulasjon du kan tenke på. Vi vil implementere tre typer manipulasjoner her: skyve en del av rutenettet i en gitt retning, skyve rutenettet utover fra et tidspunkt og trekke ruten inn mot et punkt. Alle tre vil påvirke rutenettet innenfor en gitt radius fra noen målpunkt. Nedenfor er noen bilder av disse manipulasjonene i aksjon.
offentlig ugyldig ApplyDirectedForce (Vector3 force, Vector3 posisjon, floatradius) foreach (var masse i poeng) hvis (Vector3.DistanceSquared (posisjon, masse.Posisjon) < 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);
Vi bruker alle tre metodene i Shape Blaster til forskjellige effekter.
Vi skal tegne rutenett ved å tegne linjesegmenter mellom hvert nabopunkt. Først skal vi lage en utvidelsesmetode på SpriteBatch
som tillater oss å tegne linjesegmenter ved å ta en tekstur av en enkelt piksel og strekke den inn i en linje.
Åpne Kunst
klasse og erklære en tekstur for pikselet.
offentlig statisk Texture2D Pixel get; privat sett;
Du kan sette pikseltekstur på samme måte som vi konfigurerer de andre bildene, eller du kan ganske enkelt legge til følgende to linjer til Art.Load ()
metode.
Pixel = ny Texture2D (Player.GraphicsDevice, 1, 1); Pixel.SetData (ny [] Color.White);
Dette skaper bare en ny 1x1px tekstur og setter den eneste piksel til hvit. Legg nå følgende metode i utvidelser
klasse.
Statisk statisk tomt DrawLine (denne SpriteBatch spriteBatch, Vector2 start, Vector2 end, Fargefarge, flyte tykkelse = 2f) Vector2 delta = endstart; SpriteBatch.Draw (Art.Pixel, start, null, farge, delta.ToAngle (), ny Vector2 (0, 0.5f), ny Vector2 (delta.Length (), tykkelse), SpriteEffects.None, 0f);
Denne metoden strekker seg, roterer og toner pikseltekstur for å produsere linjen vi ønsker.
Deretter trenger vi en metode for å projisere 3D-gridpunktene på vår 2D-skjerm. Normalt kan dette gjøres ved hjelp av matriser, men her forvandler vi koordinatene manuelt i stedet.
Legg til følgende i Nett
klasse.
offentlig Vector2 ToVec2 (Vector3 v) // gjør et perspektivprojeksjon float factor = (v.Z + 2000) / 2000; returnere (ny Vector2 (v.X, v.Y) - screenSize / 2f) * faktor + skjermstørrelse / 2;
Denne transformasjonen vil gi grid et perspektivvinkel hvor langt unna punkter vises nærmere sammen på skjermen. Nå kan vi tegne rutenettet ved å iterere gjennom rader og kolonner og tegningslinjer mellom dem.
Offentlig tomrom Draw (SpriteBatch spriteBatch) int bredde = poeng.GetLength (0); int høyde = poeng.Getlengde (1); Fargefarge = Ny Farge (30, 30, 139, 85); // mørk blå for (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) left = ToVec2 (poeng [x - 1, y] .posisjon); flyte tykkelse = y% 3 == 1? 3f: 1f; SpriteBatch.DrawLine (venstre, p, farge, tykkelse); hvis (y> 1) opp = ToVec2 (poeng [x, y - 1] .posisjon); flyte tykkelse = x% 3 == 1? 3f: 1f; SpriteBatch.DrawLine (opp, p, farge, tykkelse);
I ovennevnte kode, p
er vårt nåværende punkt på rutenettet, venstre
er poenget direkte til venstre og opp
er poenget rett over det. Vi trekker hver tredje linje tykkere både horisontalt og vertikalt for visuell effekt.
Vi kan optimalisere rutenettet ved å forbedre visuell kvalitet for et gitt antall fjærer uten å øke ytelseskostnaden betydelig. Vi skal gjøre to slike optimaliseringer.
Vi vil gjøre grid tettere ved å legge til linjesegmenter inne i eksisterende gridceller. Vi gjør det ved å tegne linjer fra midtpunktet til den ene siden av cellen til midtpunktet på motsatt side. Bildet under viser de nye interpolerte linjene i rødt.
Tegning av de interpolerte linjene er rett frem. Hvis du har to poeng, en
og b
, deres midtpunkt er (a + b) / 2
. Så, for å tegne de interpolerte linjene, legger vi til følgende kode inne i til
løkker av vår Tegne()
metode.
hvis (x> 1 && y> 1) Vector2 upLeft = ToVec2 (poeng [x - 1, y - 1] .posisjon); spriteBatch.DrawLine (0.5f * (upLeft + opp), 0.5f * (venstre + p), farge, 1f); // vertikal linje spriteBatch.DrawLine (0.5f * (upLeft + left), 0.5f * (opp + p), farge, 1f); // horisontal linje
Den andre forbedringen er å utføre interpolering på våre rettlinjesegmenter for å gjøre dem til jevnere kurver. XNA gir den praktiske Vector2.CatmullRom ()
metode som utfører Catmull-Rom interpolering. Du overfører metoden fire sekvensielle punkter på en buet linje, og den vil returnere poeng langs en jevn kurve mellom det andre og det tredje poenget du angav.
Det femte argumentet til Vector2.CatmullRom ()
er en vektningsfaktor som bestemmer hvilket punkt på den interpolerte kurven den returnerer. En vektningsfaktor på 0
eller 1
Vil henholdsvis returnere det andre eller tredje poenget du oppgav, og en vektningsfaktor på 0.5
vil returnere punktet på den interpolerte kurven halvveis mellom de to punktene. Ved å bevege vektvektoren gradvis fra null til en og tegne linjer mellom de returnerte punktene, kan vi produsere en perfekt jevn kurve. For å holde ytelseskostnadene lave, tar vi kun et enkelt interpolert punkt i betraktning, med en vektningsfaktor på 0.5
. Vi erstatter deretter den opprinnelige rette linjen i rutenettet med to linjer som møtes ved det interpolerte punktet.
Diagrammet nedenfor viser effekten av denne interpoleringen.
Siden linjesegmentene i nettet er allerede små, bruker det ikke mer enn ett interpolert punkt generelt en merkbar forskjell.
Ofte vil linjene i rutenettet være veldig rett og vil ikke kreve noen utjevning. Vi kan se etter dette og unngå å trekke to linjer i stedet for en. Vi kontrollerer om avstanden mellom det interpolerte punktet og midtpunktet til den rette linjen er større enn en piksel. Hvis det er, antar vi at linjen er buet og vi tegner to linjesegmenter. Modifikasjonen til vår Tegne()
Metoden for å legge til Catmull-Rom interpolering for de horisontale linjene er vist nedenfor.
venstre = ToVec2 (poeng [x - 1, y] .posisjon); flyte tykkelse = y% 3 == 1? 3f: 1f; // bruk Catmull-Rom interpolering for å hjelpe glatte bøyninger i rutenettet int clampedX = Math.Min (x + 1, bredde - 1); Vector2 mid = Vector2.CatmullRom (ToVec2 (poeng [x - 2, y] .posisjon), venstre, p, ToVec2 (poeng [clampedX, y] .posisjon), 0,5f); // Hvis rutenettet er veldig rett her, tegne en enkelt rett linje. Ellers tegner du linjer til vårt // nye interpolerte midtpunkt hvis (Vector2.DistanceSquared (midten, (venstre + p) / 2)> 1) spriteBatch.DrawLine (venstre, midt, farge, tykkelse); SpriteBatch.DrawLine (midt, p, farge, tykkelse); ellers spriteBatch.DrawLine (venstre, p, farge, tykkelse);
Bildet under viser effekten av utjevningen. En grønn prikk tegnes på hvert interpolert punkt for bedre å illustrere hvor linjene er glattet.
Nå er det på tide å bruke rutenettet i spillet vårt. Vi begynner med å erklære en offentlig, statisk Nett
variabel i GameRoot
og opprette rutenettet i GameRoot.Initialize ()
metode. Vi lager et rutenett med omtrent 1600 poeng som det.
const int maxGridPoints = 1600; Vector2 gridSpacing = ny Vector2 ((float) Math.Sqrt (Viewport.Width * Viewport.Height / maxGridPoints)); Rutenett = nytt rutenett (Viewport.Bounds, gridSpacing);
Da kaller vi Grid.Update ()
og Grid.Draw ()
fra Oppdater()
og Tegne()
metoder i GameRoot
. Dette vil tillate oss å se nettet når vi kjører spillet. Imidlertid må vi fortsatt gjøre ulike spillobjekter interaksjon med rutenettet.
Kuler vil avvise nettverket. Vi har allerede laget en metode for å gjøre dette kalt ApplyExplosiveForce ()
. Legg til følgende linje i Bullet.Update ()
metode.
GameRoot.Grid.ApplyExplosiveForce (0.5f * Velocity.Length (), posisjon, 80);
Dette vil gjøre kuler støte rutenettet proporsjonalt til deres hastighet. Det var ganske enkelt.
La oss nå jobbe på sorte hull. Legg til denne linjen til BlackHole.Update ()
.
GameRoot.Grid.ApplyImplosiveForce ((float) Math.Sin (sprayAngle / 2) * 10 + 20, posisjon, 200);
Dette gjør det svarte hullet suge i rutenett med varierende mengde kraft. Jeg gjenbrukte sprayAngle
variabel, noe som vil føre til at kraften på rutenettet pulserer i synkronisering med vinkelen det sprays partikler (selv om det er halvparten av frekvensen som skyldes divisjonen med to). Kraften som går inn, vil variere sinusformet mellom 10 og 30.
Til slutt vil vi skape en shockwave i nettet når spillerens skip responderer etter døden. Vi vil gjøre det ved å trekke rutenettet langs z-aksen og deretter la kraften forplante seg og sprette gjennom fjærene. Igjen, dette krever bare en liten modifikasjon til PlayerShip.Update ()
.
hvis (IsDead) if (--framesUntilRespawn == 0) GameRoot.Grid.ApplyDirectedForce (ny Vector3 (0, 0, 5000), ny Vector3 (Posisjon, 0), 50); komme tilbake;
Vi har grunnleggende spill og effekter implementert. Det er opp til deg å gjøre det til et komplett og polert spill med din egen smak. Prøv å legge til noen interessante nye mekanikere, noen kule nye effekter eller en unik historie. Hvis du ikke er sikker på hvor du skal begynne, er det noen forslag.
Takk for at du leste!