In deze serie tutorials laat ik je zien hoe je een neon-tweepijps-schietspel maakt, zoals Geometry Wars, 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.
In de serie tot nu toe hebben we de gameplay-, bloei- en partikeleffecten gemaakt. In dit laatste deel maken we een dynamisch, kromtrekkend achtergrondraster.
Waarschuwing: Loud!Een van de coolste effecten in Geometry Wars is het kromme achtergrondraster. We zullen bekijken hoe je een vergelijkbaar effect kunt creëren in Shape Blaster. Het raster reageert op kogels, zwarte gaten en de speler die uit elkaar valt. Het is niet moeilijk om te maken en het ziet er geweldig uit.
We maken het rooster met behulp van een veersimulatie. Bij elke kruising van het raster plaatsen we een klein gewicht en bevestigen we aan weerszijden een veer. Deze veren zullen alleen trekken en nooit duwen, net als een rubberen band. Om het raster in positie te houden, worden de massa's aan de rand van het raster op hun plaats verankerd. Hieronder is een diagram van de lay-out.
We zullen een klasse aanmaken genaamd rooster
om dit effect te creëren. Voordat we echter aan het raster zelf werken, moeten we twee helperklassen maken: De lente
en PointMass
.
De PointMass
klasse vertegenwoordigt de massa's waaraan we de veren zullen bevestigen. Veren sluiten nooit rechtstreeks op andere veren aan. In plaats daarvan oefenen ze een kracht uit op de massa's die ze verbinden, die op hun beurt andere bronnen kunnen rekken.
private class PointMass public Vector3 Position; openbare Vector3 Velocity; openbare float InverseMass; privé Vector3-versnelling; privé vlotter demping = 0.98f; openbare PointMass (Vector3-positie, zwevende invMass) Position = position; InverseMass = invMass; openbare leegte ApplyForce (Vector3 force) acceleratie + = force * InverseMass; public void IncreaseDamping (float factor) demping * = factor; public void Update () Velocity + = acceleratie; Positie + = snelheid; versnelling = Vector3.Zero; if (Velocity.LengthSquared () < 0.001f * 0.001f) Velocity = Vector3.Zero; Velocity *= damping; damping = 0.98f;
Er zijn een paar interessante punten over deze klasse. Merk allereerst op dat het de omgekeerde van de massa, 1 / massa
. Dit is vaak een goed idee in natuurkundige simulaties omdat natuurkundige vergelijkingen de neiging hebben om vaker het omgekeerde van de massa te gebruiken, en omdat het ons een gemakkelijke manier geeft om oneindig zware, onverplaatsbare objecten weer te geven door de inverse massa op nul te zetten.
De klasse bevat ook een demping variabel. Dit wordt grofweg gebruikt als wrijving of luchtweerstand. Het vertraagt geleidelijk de massa. Dit zorgt ervoor dat het raster uiteindelijk tot rust komt en verhoogt ook de stabiliteit van de veersimulatie.
De Bijwerken()
methode verplaatst het werk van het verplaatsen van de punt elk frame. Het begint met symplectische Euler-integratie, wat betekent dat we de versnelling toevoegen aan de snelheid en vervolgens de bijgewerkte snelheid aan de positie toevoegen. Dit verschilt van de standaard Euler-integratie waarin we de snelheid zouden bijwerken na de positie bijwerken.
Tip: Symplectische Euler is beter voor de lentesimulaties omdat het energie bespaart. Als je gewone Euler-integratie gebruikt en veren zonder demping maakt, zullen ze de neiging hebben om verder te rekken en verder te stuiteren als ze energie krijgen, en uiteindelijk je simulatie te verbreken.
Na het bijwerken van de snelheid en de positie, controleren we of de snelheid erg klein is en zo ja stellen we het op nul. Dit kan van belang zijn voor de prestaties vanwege de aard van gedenormaliseerde drijvende-kommagetallen.
(Wanneer drijvende-kommagetallen erg klein worden, gebruiken ze een speciale representatie die een denormaal getal wordt genoemd. Dit heeft als voordeel dat float kleinere getallen vertegenwoordigt, maar het heeft een prijs. De meeste chipsets kunnen hun standaard rekenkundige bewerkingen niet gebruiken op gedenormaliseerde getallen en in plaats daarvan moeten ze worden geëmuleerd met behulp van een reeks stappen.Dit kan tientallen tot honderden keren langzamer zijn dan het uitvoeren van bewerkingen op genormaliseerde drijvende-kommagetallen.Want we onze snelheid vermenigvuldigen met onze dempingsfactor per frame, zal deze uiteindelijk erg klein worden We geven niet echt om zulke minuscule snelheden, dus we hebben het eenvoudig op nul gezet.)
De IncreaseDamping ()
methode wordt gebruikt om de hoeveelheid demping tijdelijk te verhogen. We zullen dit later gebruiken voor bepaalde effecten.
Een veer verbindt twee puntmassa's en oefent een kracht uit die over de natuurlijke lengte heen reikt. Springs volgen een aangepaste versie van Hooke's Law met demping:
\ [f = -kx - bv \]
De code voor de De lente
klasse is als volgt.
private struct Spring public PointMass End1; openbare PointMass End2; public float TargetLength; openbare vlotter Stijfheid; openbare vlotter Demping; openbare lente (PointMass end1, PointMass end2, vlotterstijfheid, vlotterdemping) End1 = end1; End2 = end2; Stijfheid = stijfheid; Demping = demping; TargetLength = Vector3.Distance (end1.Position, end2.Position) * 0.95f; public void Update () var x = End1.Position - End2.Position; vlotterlengte = x.Lengte (); // deze veren kunnen alleen trekken, niet duwen als (lengte <= 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);
Wanneer we een veer maken, stellen we de natuurlijke lengte van de veer in op iets minder dan de afstand tussen de twee eindpunten. Dit houdt het raster strak, zelfs in rust en verbetert het uiterlijk enigszins.
De Bijwerken()
methode controleert eerst of de veer uitgerekt is voorbij zijn natuurlijke lengte. Als het niet uitgerekt is, gebeurt er niets. Als dat zo is, gebruiken we de aangepaste wet van Hooke om de kracht van de veer te vinden en toe te passen op de twee verbonden massa's.
Nu we de benodigde geneste klassen hebben, zijn we klaar om het raster te maken. We beginnen met creëren PointMass
objecten op elk kruispunt op het raster. We creëren ook een onroerende anker PointMass
objecten om het raster op zijn plaats te houden. Vervolgens verbinden we de massa's met bronnen.
Veer [] veren; PointMass [,] punten; public Grid (Rectangle size, Vector2 spacing) var springList = new List (); int numColumns = (int) (size.Width / spacing.X) + 1; int numRows = (int) (size.Height / spacing.Y) + 1; punten = nieuwe PointMass [numColumns, numRows]; // deze vaste punten worden gebruikt om het raster te verankeren naar vaste posities op het scherm PointMass [,] fixedPoints = new PointMass [numColumns, numRows]; // maak de puntmassa's int kolom = 0, rij = 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 (nieuwe lente (punten [x - 1, y], punten [x, y], stijfheid, demping)); if (y> 0) springList.Add (nieuwe lente (punten [x, y - 1], punten [x, y], stijfheid, demping)); springs = springList.ToArray ();
De eerste voor
loop creëert zowel normale massa's als onroerende massa's op elk kruispunt van het raster. We zullen niet echt alle onwrikbare massa's gebruiken, en de ongebruikte massa's zullen gewoonweg vuilnis worden verzameld ergens nadat de aannemer eindigt. We zouden kunnen optimaliseren door onnodige objecten te creëren, maar omdat het raster meestal maar één keer wordt gemaakt, maakt het niet veel uit.
Naast het gebruik van ankerpuntmassa's rond de rand van het raster, gebruiken we ook enkele ankermassa's in het raster. Deze zullen worden gebruikt om heel voorzichtig te helpen het rooster terug te trekken naar zijn oorspronkelijke positie na te zijn vervormd.
Omdat de ankerpunten nooit bewegen, hoeven ze niet elk frame te worden bijgewerkt. We kunnen ze gewoon aansluiten op de bronnen en ze vergeten. Daarom hebben we geen ledenvariabele in de rooster
klasse voor deze massa's.
Er zijn een aantal waarden die u kunt aanpassen bij het maken van het raster. De belangrijkste zijn de stijfheid en demping van de veren. De stijfheid en demping van de grensankers en binnenankers worden onafhankelijk van de hoofdveren ingesteld. Hogere stijfheidswaarden zullen de veren sneller laten oscilleren, en hogere dempingswaarden zullen ervoor zorgen dat de veren sneller vertragen.
Om het raster te laten bewegen, moeten we het elk frame bijwerken. Dit is heel eenvoudig omdat we al het harde werk in de PointMass
en De lente
klassen.
openbare ongeldige Update () foreach (var spring in springs) spring.Update (); foreach (var massa in punten) massa.Update ();
Nu zullen we enkele methoden toevoegen die het raster manipuleren. U kunt methoden toevoegen voor elke vorm van manipulatie die u maar kunt bedenken. We zullen hier drie soorten manipulaties uitvoeren: een deel van het raster in een bepaalde richting duwen, het raster vanaf een bepaald punt naar buiten duwen en het raster naar een bepaald punt trekken. Alle drie hebben invloed op het raster binnen een bepaalde straal vanaf een bepaald doelpunt. Hieronder staan enkele afbeeldingen van deze manipulaties in actie.
openbare ongeldige ApplyDirectedForce (Vector3 force, Vector3 positie, zwevende straal) foreach (var massa in punten) if (Vector3.DistanceSquared (positie, massa. Positie) < 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);
We zullen alle drie deze methoden in Shape Blaster gebruiken voor verschillende effecten.
We tekenen het raster door lijnsegmenten tussen elk aangrenzend paar punten te tekenen. Eerst maken we een uitbreidingsmethode SpriteBatch
waarmee we lijnsegmenten kunnen tekenen door een textuur van een enkele pixel te nemen en deze uit te lijnen in een lijn.
Open de Kunst
klasse en declareer een textuur voor de pixel.
openbare statische Texture2D Pixel get; privé set;
U kunt de pixelstructuur op dezelfde manier instellen als de andere afbeeldingen, of u kunt gewoon de volgende twee regels toevoegen aan de Art.Load ()
methode.
Pixel = nieuwe Texture2D (Player.GraphicsDevice, 1, 1); Pixel.SetData (nieuw [] Color.White);
Dit maakt eenvoudig een nieuwe 1x1px-structuur en stelt de enige pixel in op wit. Voeg nu de volgende methode toe in de uitbreidingen
klasse.
openbare statische leegte DrawLine (deze SpriteBatch spriteBatch, Vector2 start, Vector2-end, Kleurkleur, zwevendikte = 2f) Vector2 delta = einde - start; spriteBatch.Draw (Art.Pixel, start, null, color, delta.ToAngle (), new Vector2 (0, 0.5f), new Vector2 (delta.Length (), thickness), SpriteEffects.None, 0f);
Deze methode rekt, roteert en tinten de pixeltextuur om de gewenste lijn te produceren.
Vervolgens hebben we een methode nodig om de 3D-rasterpunten op ons 2D-scherm te projecteren. Normaal gesproken kan dit worden gedaan met behulp van matrices, maar hier zullen we de coördinaten in plaats daarvan handmatig transformeren.
Voeg het volgende toe aan de rooster
klasse.
public Vector2 ToVec2 (Vector3 v) // een perspectiefprojectie-floatfactor = (v.Z + 2000) / 2000; return (nieuwe Vector2 (v.X, v.Y) - screenSize / 2f) * factor + screenSize / 2;
Deze transformatie geeft het raster een perspectiefbeeld waarbij ver weg gelegen punten dichter bij elkaar op het scherm verschijnen. Nu kunnen we het raster tekenen door door de rijen en kolommen te itereren en er lijnen tussen te tekenen.
public void Draw (SpriteBatch spriteBatch) int width = points.GetLength (0); int height = points.GetLength (1); Kleurkleur = nieuw Kleur (30, 30, 139, 85); // donkerblauw voor (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 (punten [x - 1, y]. Positie); vlotterdikte = y% 3 == 1? 3f: 1f; spriteBatch.DrawLine (links, p, kleur, dikte); if (y> 1) up = ToVec2 (punten [x, y - 1]. Positie); vlotterdikte = x% 3 == 1? 3f: 1f; spriteBatch.DrawLine (omhoog, p, kleur, dikte);
In de bovenstaande code, p
is ons huidige punt op het raster, links
is het punt direct links ervan en omhoog
is het punt er direct boven. We tekenen elke derde lijn zowel horizontaal als verticaal voor een visueel effect.
We kunnen het raster optimaliseren door de beeldkwaliteit voor een bepaald aantal veren te verbeteren zonder de prestatiekosten aanzienlijk te verhogen. We gaan twee van dergelijke optimalisaties doen.
We zullen het raster dichter maken door lijnsegmenten toe te voegen in de bestaande rastercellen. We doen dit door lijnen te tekenen vanaf het middelpunt van één kant van de cel naar het middelpunt van de andere kant. De afbeelding hieronder toont de nieuwe geïnterpoleerde lijnen in rood.
Het tekenen van de geïnterpoleerde lijnen is eenvoudig. Als je twee punten hebt, een
en b
, hun middelpunt is (a + b) / 2
. Dus, om de geïnterpoleerde lijnen te tekenen, voegen we de volgende code toe binnen de voor
loops van onze Trek()
methode.
if (x> 1 && y> 1) Vector2 upLeft = ToVec2 (punten [x - 1, y - 1]. Positie); spriteBatch.DrawLine (0.5f * (omhoogLinks + omhoog), 0.5f * (links + p), kleur, 1f); // verticale lijn spriteBatch.DrawLine (0.5f * (omhoogLinks + links), 0.5f * (omhoog + p), kleur, 1f); // horizontale lijn
De tweede verbetering is om interpolatie uit te voeren op onze rechte lijnsegmenten om ze in vloeiendere bochten te maken. XNA biedt het handige Vector2.CatmullRom ()
methode die Catmull-Rom-interpolatie uitvoert. Je geeft de methode vier opeenvolgende punten op een gebogen lijn door, en het geeft punten terug langs een vloeiende curve tussen de tweede en derde punten die je hebt opgegeven.
Het vijfde argument voor Vector2.CatmullRom ()
is een wegingsfactor die bepaalt welk punt op de geïnterpoleerde curve wordt geretourneerd. Een weegfactor van 0
of 1
zullen respectievelijk het door u opgegeven tweede of derde punt en een wegingsfactor van 0.5
zal het punt op de geïnterpoleerde curve halverwege tussen de twee punten retourneren. Door de wegingsfactor geleidelijk van nul naar één te verplaatsen en lijnen tussen de geretourneerde punten te tekenen, kunnen we een perfect vloeiende curve produceren. Om de prestatiekosten laag te houden, zullen we echter slechts één enkel geïnterpoleerd punt in overweging nemen, met een wegingsfactor van 0.5
. Vervolgens vervangen we de oorspronkelijke rechte lijn in het raster met twee lijnen die elkaar raken op het geïnterpoleerde punt.
Het onderstaande diagram toont het effect van deze interpolatie.
Omdat de lijnsegmenten in het raster al klein zijn, maakt het gebruik van meer dan één geïnterpoleerd punt over het algemeen geen merkbaar verschil.
Vaak zijn de lijnen in ons raster erg recht en hoeven ze niet te worden gladgemaakt. We kunnen dit controleren en voorkomen dat we twee lijnen moeten trekken in plaats van één. We controleren of de afstand tussen het geïnterpoleerde punt en het middelpunt van de rechte lijn groter is dan één pixel. Als dat zo is, nemen we aan dat de lijn gebogen is en we trekken twee lijnsegmenten. De wijziging aan onze Trek()
methode voor het toevoegen van Catmull-Rom-interpolatie voor de horizontale lijnen wordt hieronder weergegeven.
links = ToVec2 (punten [x - 1, y]. Positie); vlotterdikte = y% 3 == 1? 3f: 1f; // gebruik Catmull-Rom-interpolatie om bochten in het raster glad te maken in ingeklemdX = Math.Min (x + 1, width - 1); Vector2 mid = Vector2.CatmullRom (ToVec2 (punten [x - 2, y]. Positie), links, p, ToVec2 (punten [clampedX, y]. Positie), 0,5f); // Als het raster hier heel recht is, tekent u een enkele rechte lijn. Teken anders lijnen naar ons // nieuw geïnterpoleerd middelpunt als (Vector2.DistanceSquared (midden, (links + p) / 2)> 1) spriteBatch.DrawLine (links, midden, kleur, dikte); spriteBatch.DrawLine (midden, p, kleur, dikte); else spriteBatch.DrawLine (links, p, kleur, dikte);
De afbeelding hieronder toont de effecten van de afvlakking. Op elk geïnterpoleerd punt wordt een groene stip getekend om beter te illustreren waar de lijnen worden afgevlakt.
Nu is het tijd om het raster in onze game te gebruiken. We beginnen met het verklaren van een openbare, statische rooster
variabele in GameRoot
en het maken van het raster in de GameRoot.Initialize ()
methode. We zullen een raster maken met ongeveer 1600 punten zoals dat.
const int maxGridPoints = 1600; Vector2 gridSpacing = new Vector2 ((float) Math.Sqrt (Viewport.Width * Viewport.Height / maxGridPoints)); Grid = new Grid (Viewport.Bounds, gridSpacing);
Dan bellen we Grid.Update ()
en Grid.Draw ()
van de Bijwerken()
en Trek()
methoden in GameRoot
. Hierdoor kunnen we het raster zien wanneer we het spel uitvoeren. We moeten echter nog steeds verschillende game-objecten laten communiceren met het raster.
Kogels stoten het raster af. We hebben al een methode gemaakt om dit te doen genaamd ApplyExplosiveForce ()
. Voeg de volgende regel toe aan de Bullet.Update ()
methode.
GameRoot.Grid.ApplyExplosiveForce (0.5f * Velocity.Length (), Position, 80);
Dit zorgt ervoor dat kogels het raster proportioneel afstoten ten opzichte van hun snelheid. Dat was vrij eenvoudig.
Laten we nu werken aan zwarte gaten. Voeg deze regel toe aan BlackHole.Update ()
.
GameRoot.Grid.PlusImplosiveForce ((float) Math.Sin (sprayAngle / 2) * 10 + 20, positie, 200);
Dit zorgt ervoor dat het zwarte gat in het rooster zuigt met een variërende hoeveelheid kracht. Ik heb het opnieuw gebruikt sprayAngle
variabele, die ervoor zorgt dat de kracht op het rooster synchroon pulseert met de hoek waarin het deeltjes sproeit (hoewel met de helft van de frequentie als gevolg van de deling door twee). De doorgevoerde kracht zal sinusvormig variëren tussen 10 en 30.
Ten slotte zullen we een schokgolf in het net creëren wanneer het schip van de speler na de dood weer verschijnt. We doen dit door het rooster langs de z-as te trekken en vervolgens de kracht te laten propageren en door de veren te laten stuiteren. Nogmaals, dit vereist slechts een kleine aanpassing aan PlayerShip.Update ()
.
if (IsDead) if (--framesUntilRespawn == 0) GameRoot.Grid.ApplyDirectedForce (new Vector3 (0, 0, 5000), new Vector3 (Position, 0), 50); terug te keren;
We hebben de basis gameplay en effecten geïmplementeerd. Het is aan jou om er een compleet en gepolijst spel van te maken met je eigen smaak. Probeer wat interessante nieuwe mechanica, enkele coole nieuwe effecten of een uniek verhaal toe te voegen. Als je niet zeker weet waar je moet beginnen, zijn hier een paar suggesties.
Bedankt voor het lezen!