Gör en Neon Vector Shooter i XNA The Warping Grid

I den här serien av handledningar visar jag dig hur man gör en neon tvillingskytt, som Geometry Wars, i XNA. Målet med dessa handledningar är att inte lämna dig en exakt kopia av Geometry Wars, utan snarare att gå över de nödvändiga elementen som gör att du kan skapa din egen högkvalitativa variant.


Översikt

I serien hittills skapade vi gameplay, blom och partikel effekter. I den här sista delen kommer vi att skapa ett dynamiskt, krusande bakgrundsrör.

Varning: Högt!

En av de coolaste effekterna i Geometry Wars är klingningsbakgrundet. Vi undersöker hur du skapar en liknande effekt i Shape Blaster. Nätet kommer att reagera på kulor, svarta hål och spelare som respekterar. Det är inte svårt att göra och det ser fantastiskt ut.

Vi gör nätet med en fjädersimulering. Vid varje korsning av gallret lägger vi en liten vikt och bifogar en fjäder på varje sida. Dessa fjädrar kommer bara att dra och aldrig trycka, som ett gummiband. För att hålla nätet i läge, kommer massorna vid gridens gräns förankras på plats. Nedan är ett diagram över layouten.

Vi skapar en klass som heter Rutnät för att skapa denna effekt. Men innan vi arbetar på själva nätet måste vi göra två hjälparklasser: Vår och PointMass.

PointMass-klassen

De PointMass klassen representerar de massor som vi kommer att fästa fjädrarna på. Fjädrar kopplas aldrig direkt till andra fjädrar. I stället tillämpar de en kraft på de massor de ansluter, vilket i sin tur kan sträcka andra fjädrar.

 privata klassen PointMass public Vector3 Position; allmän vektor3 hastighet; offentliga float InverseMass; privat Vector3 acceleration; privat floatdämpning = 0,98f; allmän PointMass (Vector3 position, float invMass) Position = position; InverseMass = invMass;  public void ApplyForce (Vector3 force) acceleration + = force * InverseMass;  offentligt tomrum IncreaseDamping (floatfaktor) dempning * = faktor;  public void Update () Hastighet + = acceleration; Position + = Hastighet; acceleration = Vector3.Zero; om (Velocity.LengthSquared () < 0.001f * 0.001f) Velocity = Vector3.Zero; Velocity *= damping; damping = 0.98f;  

Det finns några intressanta punkter om den här klassen. Först märker du att det lagrar omvänd av massan, 1 / massa. Det här är ofta en bra idé i fysiksimuleringar, eftersom fysikekvationer tenderar att använda massens inversare oftare, och eftersom det ger oss ett enkelt sätt att representera oändligt tunga, obotliga föremål genom att ställa in den omvända massan till noll.

Klassen innehåller också en dämpning variabel. Detta används ungefär som friktion eller luftmotstånd. Det saktar gradvis massan nere. Detta bidrar till att nätet i slutändan kommer att vila och ökar också stabiliteten i vårens simulering.

De Uppdatering() Metoden gör arbetet med att flytta punktmassan varje ram. Det börjar med att göra en symplektisk Euler-integration, vilket bara innebär att vi lägger till accelerationen till hastigheten och sedan lägger till den uppdaterade hastigheten på positionen. Detta skiljer sig från standard Euler integration där vi skulle uppdatera hastigheten efter uppdatering av positionen.

Tips: Symplectic Euler är bättre för vårsimuleringar eftersom det sparar energi. Om du använder regelbunden Euler-integration och skapar fjädrar utan dämpning, kommer de att tendera att sträcka sig längre och mer varje studs då de får energi, så småningom bryter din simulering.

Efter uppdatering av hastighet och position, kontrollerar vi om hastigheten är väldigt liten, och om så sätter vi den till noll. Detta kan vara viktigt för prestanda på grund av arten av denormaliserade flytpunkten.

(När flytpunkterna blir väldigt små använder de en särskild representation som kallas ett denormalt tal. Detta har fördelen att float kan representera mindre antal, men det kommer till ett pris. De flesta chipset kan inte använda sina standardräkningstransaktioner på detormaliserade tal och istället måste emulera dem med en rad steg. Det kan vara tiotals gånger långsammare än att utföra operationer på normaliserade flytpunkter. Eftersom vi multiplicerar vår hastighet med vår dämpningsfaktor varje ram blir den så småningom väldigt liten . Vi bryr oss inte om sådana små hastigheter, så vi ställer det helt enkelt till noll.)

De IncreaseDamping () Metoden används för att tillfälligt öka mängden dämpning. Vi kommer att använda detta senare för vissa effekter.

Vårklassen

En fjäder ansluter två punktmassor och, om den sträcker sig över sin naturliga längd, applicerar en kraft som drar massorna samman. Springs följer en modifierad version av Hooke's Law med dämpning:

\ [f = -kx - bv \]

  • \ (f \) är kraften som produceras av våren.
  • \ (k \) är vårkonstanten eller fjäderns styvhet.
  • \ (x \) är avståndet fjädern sträcker sig bortom dess naturliga längd.
  • \ (b \) är dämpningsfaktorn.
  • \ (v \) är hastigheten.

Koden för Vår klassen är som följer.

 private struct Våren public PointMass End1; offentlig PointMass End2; offentlig float TargetLength; offentlig float Styvhet; offentlig float Damping; offentlig vår (PointMass end1, PointMass end2, float-styvhet, floatdämpning) End1 = end1; End2 = end2; Stiffhet = styvhet; Dämpning = dämpning; TargetLength = Vector3.Distance (end1.Position, end2.Position) * 0.95f;  public void Update () var x = End1.Position - End2.Position; floatlängd = x.Length (); // dessa fjädrar kan bara dra, inte trycka om (längd <= 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 skapar en fjäder sätter vi vårens naturliga längd på bara något mindre än avståndet mellan de två ändpunkterna. Detta håller gallret stramt även i vila och förbättrar utseendet något.

De Uppdatering() Metoden kontrollerar först om våren sträcker sig utöver sin naturliga längd. Om det inte sträcker sig, händer ingenting. Om det är så använder vi den modifierade Hooke's Law för att hitta kraften från våren och tillämpa den på de två anslutna massorna.

Skapa nätet

Nu när vi har nödvändiga kapslade klasser, är vi redo att skapa nätet. Vi börjar med att skapa PointMass objekt vid varje korsning på gallret. Vi skapar också ett antal fasta ankar PointMass föremål för att hålla nätet på plats. Vi kopplar sedan massorna med fjädrar.

 Våren [] fjädrar; PointMass [,] poäng; public grid (rektangel storlek, vektor2 avstånd) var springList = ny lista (); int numColumns = (int) (storlek.Width / spacing.X) + 1; int numRows = (int) (size.Height / spacing.Y) + 1; poäng = ny PointMass [numColumns, numRows]; // Dessa fasta punkter kommer att användas för att förankra gallret till fasta positioner på skärmen PointMass [,] fixedPoints = new PointMass [numColumns, numRows]; // skapa punktmassorna int kolumn = 0, rad = 0; för (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 (poäng [x - 1, y], poäng [x, y], styvhet, dämpning)); om (y> 0) springList.Add (ny vår (poäng [x, y - 1], poäng [x, y], styvhet, dämpning));  fjädrar = springList.ToArray (); 

Den första för slinga skapar både vanliga massor och obevekliga massor vid varje korsning av gallret. Vi kommer inte att använda alla de obevekliga massorna, och de oanvända massorna kommer helt enkelt att vara skräp samlas någon gång efter att konstruktören slutar. Vi kan optimera genom att undvika att skapa onödiga objekt, men eftersom rutnätet vanligtvis bara skapas en gång kommer det inte att göra stor skillnad.

Förutom att använda ankarpunktsmassor runt gridens gräns, kommer vi också att använda några ankermassor inuti gallret. Dessa kommer att användas för att försiktigt hjälpa till att dra tillbaka gallret till dess ursprungliga läge efter att deformeras.

Eftersom ankarpunkterna aldrig rör sig behöver de inte uppdateras varje ram. Vi kan helt enkelt koppla upp dem till fjädrarna och glömma dem. Därför har vi ingen medlemsvariabel i Rutnät klass för dessa massor.

Det finns ett antal värden du kan tweak i skapandet av rutnätet. De viktigaste är fjädrarnas styvhet och dämpning. Stivheten och dämpningen av gränsankarna och inre ankare ställs oberoende av huvudfjädrarna. Högre styvhetsvärden gör att fjädrarna svänger snabbare och högre dämpningsvärden leder till att fjädrarna saktar fortare.

Manipulerar nätet

För att nätet ska kunna flyttas måste vi uppdatera det varje ram. Det här är väldigt enkelt eftersom vi redan gjort allt det hårda arbetet i PointMass och Vår klasser.

 public void Update () foreach (var våren i fjädrar) spring.Update (); foreach (var massa i poäng) mass.Update (); 

Nu lägger vi till några metoder som manipulerar nätet. Du kan lägga till metoder för vilken typ av manipulation du kan tänka dig. Vi kommer att genomföra tre typer av manipuleringar här: trycka en del av gallret i en given riktning, trycka gallret utåt från en viss punkt och dra in gallret in i någon punkt. Alla tre kommer att påverka nätet inom en given radie från någon målpunkt. Nedan följer några bilder av dessa manipulationer i aktion.


Kulor avvisar gallret utåt.
Suger gallret inåt. Våg skapad genom att trycka gallret längs z-axeln.
 public void ApplyDirectedForce (Vector3 kraft, Vector3 position, floatradie) foreach (var massa i poäng) om (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);   

Vi kommer att använda alla tre av dessa metoder i Shape Blaster för olika effekter.

Rendering av nätet

Vi kommer att rita rutnätet genom att rita linjesegment mellan varje angränsande par punkter. Först gör vi en förlängningsmetod på SpriteBatch som tillåter oss att rita linjesegment genom att ta en textur av en enda pixel och sträcka den i en linje.

Öppna Konst klass och deklarera en textur för pixeln.

 statisk statisk Texture2D Pixel get; privat uppsättning 

Du kan ställa in pixeltexturen på samma sätt som vi ställer in de andra bilderna, eller du kan helt enkelt lägga till följande två rader till Art.Load () metod.

 Pixel = ny Texture2D (Player.GraphicsDevice, 1, 1); Pixel.SetData (ny [] Color.White);

Detta skapar helt enkelt en ny 1x1px konsistens och sätter den enda pixeln till vit. Lägg nu till följande metod i Extensions klass.

 Statisk statisk tomt DrawLine (denna SpriteBatch spriteBatch, Vector2 start, Vector2-änden, Färgfärg, flyttjocklek = 2f) Vector2 delta = endstart; spriteBatch.Draw (Art.Pixel, start, null, färg, delta.ToAngle (), ny Vector2 (0, 0.5f), ny Vector2 (delta.Length (), tjocklek), SpriteEffects.None, 0f); 

Denna metod sträcker, roterar och tonar pixeltexturen för att producera den linje vi önskar.

Därefter behöver vi en metod för att projicera 3D-gridpunkterna på vår 2D-skärm. Normalt kan detta göras med matriser, men här omvandlar vi koordinaterna manuellt istället.

Lägg till följande i Rutnät klass.

 offentliga Vector2 ToVec2 (Vector3 v) // gör en perspektivprojektion float factor = (v.Z + 2000) / 2000; returnera (ny vektor2 (v.x, v.y) - screenSize / 2f) * faktor + screenSize / 2; 

Denna transformation ger grid en perspektivvy där långt borta punkter dyker närmare varandra på skärmen. Nu kan vi rita gallret genom att iterera genom raderna och kolumnerna och ritningslinjerna mellan dem.

 public void Draw (SpriteBatch spriteBatch) int bredd = points.GetLength (0); int höjd = points.GetLength (1); Färgfärg = ny färg (30, 30, 139, 85); // mörkblå för (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) vänster = ToVec2 (poäng [x - 1, y]. Position); float tjocklek = y% 3 == 1? 3f: 1f; spriteBatch.DrawLine (vänster, p, färg, tjocklek);  om (y> 1) upp = ToVec2 (poäng [x, y - 1] .position); float tjocklek = x% 3 == 1? 3f: 1f; spriteBatch.DrawLine (upp, p, färg, tjocklek); 

I ovanstående kod, p är vår nuvarande punkt på rutnätet, vänster är punkten direkt till vänster och upp är punkten direkt ovanför den. Vi drar varje tredje linje tjockare både horisontellt och vertikalt för visuell effekt.

Interpolation

Vi kan optimera nätet genom att förbättra den visuella kvaliteten för ett visst antal fjädrar utan att avsevärt öka prestandakostnaden. Vi ska göra två sådana optimeringar.

Vi kommer att göra nätet tätare genom att lägga till linjesegment i de befintliga rutorna. Vi gör det genom att rita linjer från mittpunkten på den ena sidan av cellen till mittpunkten på motsatt sida. Bilden nedan visar de nya interpolerade linjerna i rött.


Rutnät med interpolerade linjer som visas i rött

Att rita de interpolerade linjerna är rakt framåt. Om du har två poäng, en och b, deras mittpunkt är (a + b) / 2. Så, för att rita de interpolerade linjerna lägger vi till följande kod inuti för slingor av vår Dra() metod.

 om (x> 1 && y> 1) Vector2 upLeft = ToVec2 (poäng [x - 1, y - 1] .Position); spriteBatch.DrawLine (0.5f * (upLeft + upp), 0.5f * (vänster + p), färg, 1f); // vertikal linje spriteBatch.DrawLine (0.5f * (upLeft + vänster), 0.5f * (upp + p), färg, 1f); // vågrät linje 

Den andra förbättringen är att utföra interpolering på våra raka segment för att göra dem till jämnare kurvor. XNA ger den praktiska Vector2.CatmullRom () metod som utför Catmull-Rom interpolation. Du överför metoden fyra sekventiella punkter på en krökt linje, och den kommer att returnera punkter längs en jämn kurva mellan andra och tredje poängen du angav.

Det femte argumentet till Vector2.CatmullRom () är en viktningsfaktor som bestämmer vilken punkt på den interpolerade kurvan som den återvänder. En viktningsfaktor av 0 eller 1 kommer att återge den andra eller tredje punkten du angav och en viktningsfaktor på 0,5 kommer att returnera punkten på den interpolerade kurvan halvvägs mellan de två punkterna. Genom att gradvis flytta viktningsfaktorn från noll till en och rita linjer mellan de återvändande punkterna, kan vi producera en perfekt jämn kurva. För att hålla prestandokostnaden låg, tar vi emellertid endast en enda interpolerad punkt i beaktande, med en viktningsfaktor på 0,5. Vi ersätter sedan den ursprungliga raka linjen i rutnätet med två linjer som möts vid den interpolerade punkten.

Diagrammet nedan visar effekten av denna interpolering.

Eftersom linjesegmenten i rutnätet redan är små gör det inte en märkbar skillnad att använda mer än en interpolerad punkt.

Ofta kommer linjerna i vårt nät vara väldigt raka och kräver ingen utjämning. Vi kan kolla på detta och undvika att rita två linjer istället för en. Vi kontrollerar om avståndet mellan den interpolerade punkten och mittlinjen på den raka linjen är större än en pixel. Om det är, antar vi att linjen är krökt och vi ritar två linjesegment. Modifieringen till vår Dra() Metod för att lägga till Catmull-Rom-interpolering för de horisontella linjerna visas nedan.

 left = ToVec2 (poäng [x - 1, y]. Position); float tjocklek = y% 3 == 1? 3f: 1f; // Använd Catmull-Rom interpolering för att hjälpa smidiga böjningar i rutan int clampedX = Math.Min (x + 1, bredd - 1); Vector2 mid = Vector2.CatmullRom (ToVec2 (poäng [x - 2, y] .Position), vänster, p, ToVec2 (poäng [clampedX, y]. Position), 0.5f); // Om nätet är väldigt rakt här, rita en enda rak linje. Annars rita linjer till vår // nya interpolerade mittpunkt om (Vector2.DistanceSquared (mitt, (vänster + p) / 2)> 1) spriteBatch.DrawLine (vänster, mitt, färg, tjocklek); spriteBatch.DrawLine (mitten, p, färg, tjocklek);  annat spriteBatch.DrawLine (vänster, p, färg, tjocklek);

Bilden nedan visar effekterna av utjämningen. En grön punkt ritas vid varje interpolerad punkt för att bättre illustrera var linjerna slätas.

Använda gallret i Shape Blaster

Nu är det dags att använda gallret i vårt spel. Vi börjar med att förklara en offentlig, statisk Rutnät variabel i GameRoot och skapa nätet i GameRoot.Initialize () metod. Vi skapar ett rutnät med ungefär 1600 poäng som så.

 const int maxGridPoints = 1600; Vector2 gridSpacing = ny Vector2 ((float) Math.Sqrt (Viewport.Width * Viewport.Height / maxGridPoints)); Rutnät = nytt rutnät (Viewport.Bounds, gridSpacing);

Då ringer vi Grid.Update () och Grid.Draw () från Uppdatering() och Dra() metoder i GameRoot. Detta gör det möjligt för oss att se rutnätet när vi kör spelet. Vi måste emellertid fortfarande göra olika spelobjekt interagera med rutnätet.

Kulor kommer att avvisa gallret. Vi har redan gjort en metod för att göra detta kallat ApplyExplosiveForce (). Lägg till följande rad i Bullet.Update () metod.

 GameRoot.Grid.ApplyExplosiveForce (0.5f * Velocity.Length (), Position, 80);

Detta kommer att göra kulor avstänger gallret proportionellt till deras hastighet. Det var ganska enkelt.

Låt oss nu arbeta på svarta hål. Lägg till den här raden till BlackHole.Update ().

 GameRoot.Grid.ApplyImplosiveForce ((float) Math.Sin (sprayAngle / 2) * 10 + 20, Position, 200);

Detta gör att det svarta hålet suger i gallret med en varierande mängd kraft. Jag återanvända sprayAngle variabel, vilket kommer att orsaka kraften på gallret att pulsera i synk med vinkeln som sprutar partiklar (även om den är halva frekvensen beroende på uppdelningen med två). Kraften som passerar in varierar sinusformigt mellan 10 och 30.

Slutligen skapar vi en shockwave i nätet när spelarens skepp respekterar efter döden. Vi kommer att göra det genom att dra gallret längs z-axeln och sedan låta kraften sprida sig och studsa genom fjädrarna. Återigen kräver detta bara en liten ändring till PlayerShip.Update ().

 om (IsDead) if (--framesUntilRespawn == 0) GameRoot.Grid.ApplyDirectedForce (ny Vector3 (0, 0, 5000), ny Vector3 (Position, 0), 50); lämna tillbaka; 

Vad kommer härnäst?

Vi har de grundläggande spel och effekter som genomförts. Det är upp till dig att göra det till ett komplett och polerat spel med din egen smak. Prova att lägga till några intressanta nya mekaniker, några coola nya effekter eller en unik historia. Om du inte är säker på var du ska börja, här är några förslag.

  • Skapa nya fiendetyper som ormar eller exploderande fiender.
  • Skapa nya vapentyper som att söka efter missiler eller en blixtpistol.
  • Lägg till en titelskärm och huvudmeny.
  • Lägg till ett högt betygstabell.
  • Lägg till några powerups som en sköld eller bomber. För bonuspoäng, bli kreativ med dina powerups. Du kan göra powerups som manipulerar gravitation, förändra tid eller växa som organismer. Du kan fästa en jätte, fysikbaserad slypboll till skeppet för att krossa fiender. Experimentera med att hitta powerups som är roliga och hjälpa ditt spel att sticka ut.
  • Skapa flera nivåer. Hårdare nivåer kan införa tuffare fiender och mer avancerade vapen och powerups.
  • Tillåt en andra spelare att gå med i en gamepad.
  • Låt arenan rulla så att den kan vara större än spelfönstret.
  • Lägg till miljöfaror som lasrar.
  • Lägg till en butik eller nivelleringssystem och låta spelaren vinna uppgraderingar.

Tack för att du läser!