Crea uno sparatutto al neon in XNA The Warping Grid

In questa serie di tutorial, ti mostrerò come realizzare uno sparatutto al neon con due gemelli, come Geometry Wars, in XNA. L'obiettivo di queste esercitazioni non è quello di lasciarti con una replica esatta di Geometry Wars, ma piuttosto di esaminare gli elementi necessari che ti permetteranno di creare la tua variante di alta qualità.


Panoramica

Nella serie finora abbiamo creato gli effetti di gameplay, bloom e particle. In questa parte finale creeremo una griglia di sfondo dinamica e deformante.

Avvertenza: forte!

Uno degli effetti più interessanti di Geometry Wars è la griglia di sfondo di warping. Esamineremo come creare un effetto simile in Shape Blaster. La griglia reagirà a proiettili, buchi neri e il giocatore che respawning. Non è difficile da fare e sembra fantastico.

Creeremo la griglia utilizzando una simulazione a molla. Ad ogni intersezione della griglia, metteremo un piccolo peso e attacceremo una molla su ciascun lato. Queste molle tirano e non spingono mai, proprio come un elastico. Per mantenere la griglia in posizione, le masse al bordo della griglia saranno ancorate al loro posto. Di seguito è riportato un diagramma del layout.

Creeremo una classe chiamata Griglia per creare questo effetto. Tuttavia, prima di lavorare sulla griglia stessa, dobbiamo creare due classi helper: Primavera e PointMass.

La classe PointMass

Il PointMass la classe rappresenta le masse alle quali legheremo le molle. Le molle non si collegano mai direttamente ad altre molle. Invece, applicano una forza alle masse che collegano, che a loro volta possono allungare altre molle.

 classe privata PointMass posizione pubblica Vector3; velocità Vector3 pubblica; galleggiante pubblico InverseMass; accelerazione privata Vector3; smorzamento del galleggiante privato = 0.98f; PointMass pubblico (posizione Vector3, invMass mobile) Position = position; InverseMass = invMass;  public void ApplyForce (forza Vector3) acceleration + = force * InverseMass;  public void IncreaseDamping (float factor) damping * = factor;  public void Update () Velocity + = acceleration; Posizione + = Velocità; accelerazione = Vector3.Zero; if (Velocity.LengthSquared () < 0.001f * 0.001f) Velocity = Vector3.Zero; Velocity *= damping; damping = 0.98f;  

Ci sono alcuni punti interessanti su questa classe. Innanzitutto, nota che memorizza il inverso della massa, 1 / massa. Questa è spesso una buona idea nelle simulazioni di fisica perché le equazioni fisiche tendono a utilizzare l'inverso della massa più spesso e perché ci offre un modo semplice per rappresentare oggetti infinitamente pesanti e immobili impostando la massa inversa a zero.

La classe contiene anche a smorzamento variabile. Questo è usato approssimativamente come attrito o resistenza all'aria. Rallenta gradualmente la massa. Questo aiuta a far riposare la griglia e aumenta anche la stabilità della simulazione della molla.

Il Aggiornare() metodo fa il lavoro di spostare il punto di massa ogni fotogramma. Inizia facendo un'integrazione Eulero simplettale, il che significa che aggiungiamo l'accelerazione alla velocità e quindi aggiungiamo la velocità aggiornata alla posizione. Questo differisce dall'integrazione standard di Eulero in cui aggiorneremmo la velocità dopo aggiornare la posizione.

Mancia: Eulero simplettico è migliore per le simulazioni di primavera perché risparmia energia. Se si utilizza l'integrazione Eulero regolare e si creano molle senza smorzamento, tenderanno ad allungarsi ulteriormente ogni volta rimbalzando man mano che guadagnano energia, rompendo alla fine la simulazione.

Dopo aver aggiornato la velocità e la posizione, controlliamo se la velocità è molto piccola, e in tal caso lo impostiamo a zero. Questo può essere importante per le prestazioni a causa della natura dei numeri in virgola mobile denormalizzati.

(Quando i numeri in virgola mobile diventano molto piccoli, usano una rappresentazione speciale chiamata numero denormale.Questo ha il vantaggio di consentire a float di rappresentare numeri più piccoli, ma ha un prezzo. La maggior parte dei chipset non può usare le loro operazioni aritmetiche standard su numeri denormalizzati e invece devono emularli usando una serie di passaggi.Questo può essere da decine a centinaia di volte più lento di eseguire operazioni su numeri in virgola mobile normalizzati. Poiché moltiplichiamo la nostra velocità per il nostro fattore di smorzamento ogni fotogramma, alla fine diventerà molto piccolo In realtà non ci interessano queste velocità così piccole, quindi lo impostiamo su zero.)

Il IncreaseDamping () il metodo è usato per aumentare temporaneamente la quantità di smorzamento. Lo useremo più tardi per determinati effetti.

La lezione di primavera

Una molla collega due masse puntiformi e, se allungata oltre la sua lunghezza naturale, applica una forza che unisce le masse. Le molle seguono una versione modificata della legge di Hooke con smorzamento:

\ [f = -kx - bv \]

  • \ (f \) è la forza prodotta dalla molla.
  • \ (k \) è la costante della molla, o la rigidità della molla.
  • \ (x \) è la distanza in cui la molla è allungata oltre la sua lunghezza naturale.
  • \ (b \) è il fattore di smorzamento.
  • \ (v \) è la velocità.

Il codice per il Primavera la classe è la seguente.

 struct private Spring public PointMass End1; PointMass End2 pubblico; float pubblico TargetLength; rigidità del float pubblico; smorzamento del galleggiante pubblico; public Spring (PointMass end1, PointMass end2, float rigidezza, float damping) End1 = end1; End2 = end2; Rigidità = rigidità; Smorzamento = smorzamento; TargetLength = Vector3.Distance (end1.Position, end2.Position) * 0.95f;  public void Update () var x = End1.Position - End2.Position; float length = x.Length (); // queste molle possono solo tirare, non spingere se (lunghezza <= 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);  

Quando creiamo una molla, impostiamo la lunghezza naturale della molla leggermente inferiore alla distanza tra i due punti finali. Ciò mantiene la griglia ben tesa anche a riposo e migliora l'aspetto.

Il Aggiornare() il metodo controlla innanzitutto se la molla è allungata oltre la sua lunghezza naturale. Se non è allungato, non succede nulla. Se lo è, usiamo la legge di Hooke modificata per trovare la forza dalla molla e applicarla alle due masse connesse.

Creare la griglia

Ora che abbiamo le classi nidificate necessarie, siamo pronti a creare la griglia. Iniziamo creando PointMass oggetti ad ogni intersezione sulla griglia. Creiamo anche un'ancora immobile PointMass oggetti per mantenere la griglia in posizione. Quindi colleghiamo le masse con le molle.

 Molla [] molle; PointMass [,] punti; Griglia pubblica (Dimensione rettangolo, Distanza vettore2) var springList = new List (); int numColumns = (int) (size.Width / spacing.X) + 1; int numRows = (int) (size.Height / spacing.Y) + 1; points = new PointMass [numColumns, numRows]; // questi punti fissi saranno usati per ancorare la griglia a posizioni fisse sullo schermo PointMass [,] fixedPoints = new PointMass [numColumns, numRows]; // crea il punto masses int column = 0, row = 0; per (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 (new Spring (punti [x - 1, y], punti [x, y], rigidità, smorzamento)); se (y> 0) springList.Add (new Spring (punti [x, y - 1], punti [x, y], rigidità, smorzamento));  springs = springList.ToArray (); 

Il primo per loop crea sia masse regolari che masse immobili a ogni intersezione della griglia. In realtà non useremo tutte le masse inamovibili e le masse inutilizzate saranno semplicemente raccolte dalla spazzatura qualche tempo dopo la fine del costruttore. Potremmo ottimizzare evitando di creare oggetti non necessari, ma dato che la griglia di solito viene creata solo una volta, non farà molta differenza.

Oltre a utilizzare le masse del punto di ancoraggio attorno al bordo della griglia, utilizzeremo anche alcune masse di ancoraggio all'interno della griglia. Questi saranno usati per aiutare molto delicatamente a riportare la griglia nella sua posizione originale dopo essere stata deformata.

Poiché i punti di ancoraggio non si spostano mai, non è necessario aggiornarli ogni fotogramma. Possiamo semplicemente collegarli alle sorgenti e dimenticarli. Pertanto, non abbiamo una variabile membro in Griglia classe per queste masse.

Ci sono un certo numero di valori che puoi modificare nella creazione della griglia. I più importanti sono la rigidità e lo smorzamento delle molle. La rigidità e lo smorzamento delle ancore del bordo e degli ancoraggi interni sono regolati indipendentemente dalle molle principali. Valori di rigidità più alti faranno oscillare le molle più velocemente, e valori di smorzamento più alti faranno rallentare le molle più velocemente.

Manipolare la griglia

Affinché la griglia si muova, dobbiamo aggiornarla ogni fotogramma. Questo è molto semplice dato che abbiamo già fatto tutto il duro lavoro nel PointMass e Primavera classi.

 public void Update () foreach (var spring in springs) spring.Update (); foreach (var massa in punti) mass.Update (); 

Ora aggiungeremo alcuni metodi che manipolano la griglia. Puoi aggiungere metodi per qualsiasi tipo di manipolazione che puoi immaginare. Implementeremo tre tipi di manipolazioni qui: spingendo parte della griglia in una data direzione, spingendo la griglia verso l'esterno da qualche punto e tirando la griglia verso un certo punto. Tutti e tre influenzeranno la griglia all'interno di un dato raggio da qualche punto bersaglio. Di seguito sono riportate alcune immagini di queste manipolazioni in azione.


Pallottole che respingono la griglia verso l'esterno.
Succhiare la griglia verso l'interno. Onda creata spingendo la griglia lungo l'asse z.
 vuoto pubblico ApplyDirectedForce (forza Vector3, posizione Vector3, raggio float) foreach (massa var in punti) if (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);   

Useremo tutti e tre questi metodi in Shape Blaster per effetti diversi.

Rendering della griglia

Disegneremo la griglia disegnando segmenti di linea tra ciascuna coppia di punti adiacenti. Innanzitutto, creeremo un metodo di estensione SpriteBatch questo ci consente di disegnare segmenti di linea prendendo una texture di un singolo pixel e allungandola in una linea.

Apri il Arte classificare e dichiarare una trama per il pixel.

 pubblico statico Texture2D Pixel get; set privato; 

Puoi impostare la trama dei pixel nello stesso modo in cui configuriamo le altre immagini, oppure puoi semplicemente aggiungere le seguenti due linee a Art.Load () metodo.

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

Ciò crea semplicemente una nuova trama 1x1px e imposta il solo pixel su bianco. Ora aggiungi il seguente metodo nel estensioni classe.

 public static void DrawLine (questo spriteBatch spriteBatch, Vector2 start, Vector2 end, Color color, float thickness = 2f) Vector2 delta = end - start; spriteBatch.Draw (Art.Pixel, start, null, color, delta.ToAngle (), new Vector2 (0, 0.5f), new Vector2 (delta.Length (), thickness), SpriteEffects.None, 0f); 

Questo metodo allunga, ruota e tinge la trama dei pixel per produrre la linea che desideriamo.

Successivamente, abbiamo bisogno di un metodo per proiettare i punti della griglia 3D sul nostro schermo 2D. Normalmente questo può essere fatto usando le matrici, ma qui trasformeremo le coordinate manualmente.

Aggiungi il seguente al Griglia classe.

 public Vector ToVec2 (Vector3 v) // fa una proiezione prospettica factor = (v.Z + 2000) / 2000; return (new Vector2 (v.X, v.Y) - screenSize / 2f) * factor + screenSize / 2; 

Questa trasformazione darà alla griglia una vista prospettica in cui punti lontani appaiono più vicini sullo schermo. Ora possiamo disegnare la griglia ripetendo le righe e le colonne e disegnando le linee tra di loro.

 public public Draw (SpriteBatch spriteBatch) int width = points.GetLength (0); int height = points.GetLength (1); Colore colore = nuovo Colore (30, 30, 139, 85); // blu scuro per (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 (points [x - 1, y] .Position); Spessore float = y% 3 == 1? 3f: 1f; spriteBatch.DrawLine (sinistra, p, colore, spessore);  if (y> 1) up = ToVec2 (punti [x, y - 1] .Position); float thickness = x% 3 == 1? 3f: 1f; spriteBatch.DrawLine (up, p, colore, spessore); 

Nel codice sopra, p è il nostro punto attuale sulla griglia, sinistra è il punto direttamente alla sua sinistra e su è il punto direttamente sopra di esso. Disegniamo ogni terza linea più spesso sia orizzontalmente che verticalmente per ottenere un effetto visivo.

interpolazione

Possiamo ottimizzare la griglia migliorando la qualità visiva per un dato numero di molle senza aumentare significativamente il costo delle prestazioni. Faremo due di questi ottimizzazioni.

Renderemo la griglia più densa aggiungendo segmenti di linea all'interno delle celle della griglia esistenti. Lo facciamo disegnando linee dal punto medio di un lato della cella al punto medio del lato opposto. L'immagine sotto mostra le nuove linee interpolate in rosso.


Griglia con linee interpolate mostrate in rosso

Disegnare le linee interpolate è semplice. Se hai due punti, un e B, il loro punto medio è (a + b) / 2. Quindi, per disegnare le linee interpolate, aggiungiamo il seguente codice all'interno di per loop dei nostri Disegnare() metodo.

 if (x> 1 && y> 1) Vector2 upLeft = ToVec2 (punti [x - 1, y - 1] .Position); spriteBatch.DrawLine (0.5f * (upLeft + su), 0.5f * (left + p), color, 1f); // vertical line spriteBatch.DrawLine (0.5f * (upLeft + left), 0.5f * (up + p), color, 1f); // linea orizzontale 

Il secondo miglioramento consiste nell'eseguire l'interpolazione sui nostri segmenti rettilinei per renderli più morbidi. XNA fornisce il pratico Vector2.CatmullRom () metodo che esegue l'interpolazione Catmull-Rom. Passi il metodo con quattro punti sequenziali su una linea curva e restituirà punti lungo una curva uniforme tra il secondo e il terzo punto che hai fornito.

Il quinto argomento di Vector2.CatmullRom () è un fattore di ponderazione che determina quale punto della curva interpolata restituisce. Un fattore di ponderazione di 0 o 1 restituirà rispettivamente il secondo o il terzo punto che hai fornito e un fattore di ponderazione di 0.5 restituirà il punto sulla curva interpolata a metà strada tra i due punti. Spostando gradualmente il fattore di ponderazione da zero a uno e disegnando le linee tra i punti restituiti, possiamo produrre una curva perfettamente liscia. Tuttavia, per mantenere bassi i costi delle prestazioni, prenderemo in considerazione un solo punto interpolato, con un fattore di ponderazione di 0.5. Sostituiamo quindi la linea retta originale nella griglia con due linee che si incontrano nel punto interpolato.

Lo schema seguente mostra l'effetto di questa interpolazione.

Poiché i segmenti di linea nella griglia sono già piccoli, l'uso di più di un punto interpolato generalmente non fa una differenza evidente.

Spesso, le linee nella nostra griglia saranno molto dritte e non richiedono alcuna levigatura. Possiamo controllare questo ed evitare di dover disegnare due linee invece di una. Controlliamo se la distanza tra il punto interpolato e il punto medio della retta è maggiore di un pixel. Se lo è, assumiamo che la linea sia curva e disegniamo due segmenti di linea. La modifica al nostro Disegnare() Il metodo per aggiungere l'interpolazione Catmull-Rom per le linee orizzontali è mostrato sotto.

 left = ToVec2 (points [x - 1, y] .Position); Spessore float = y% 3 == 1? 3f: 1f; // usa l'interpolazione Catmull-Rom per facilitare le curve nella griglia int clampedX = Math.Min (x + 1, width - 1); Vector2 mid = Vector2.CatmullRom (ToVec2 (punti [x - 2, y] .Position), sinistra, p, ToVec2 (punti [clampedX, y] .Position), 0,5f); // Se la griglia è molto diritta, traccia una singola linea retta. Altrimenti, traccia le linee sul nostro // nuovo punto medio interpolato se (Vector2.DistanceSquared (metà, (sinistra + p) / 2)> 1) spriteBatch.DrawLine (sinistra, metà, colore, spessore); spriteBatch.DrawLine (metà, p, colore, spessore);  else spriteBatch.DrawLine (left, p, color, thickness);

L'immagine sotto mostra gli effetti della levigatura. Un punto verde viene disegnato in corrispondenza di ciascun punto interpolato per illustrare meglio dove vengono smussate le linee.

Utilizzando la griglia in Shape Blaster

Ora è il momento di usare la griglia nel nostro gioco. Iniziamo dichiarando pubblico, statico Griglia variabile in GameRoot e creando la griglia nel GameRoot.Initialize () metodo. Creeremo una griglia con circa 1600 punti in questo modo.

 const int maxGridPoints = 1600; Vector2 gridSpacing = new Vector2 ((float) Math.Sqrt (Viewport.Width * Viewport.Height / maxGridPoints)); Grid = new Grid (Viewport.Bounds, gridSpacing);

Quindi chiamiamo Grid.Update () e Grid.Draw () dal Aggiornare() e Disegnare() metodi in GameRoot. Questo ci permetterà di vedere la griglia quando eseguiremo il gioco. Tuttavia, abbiamo ancora bisogno di far interagire vari oggetti di gioco con la griglia.

I proiettili respingono la griglia. Abbiamo già creato un metodo per farlo ApplyExplosiveForce (). Aggiungi la seguente riga al Bullet.Update () metodo.

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

In questo modo i proiettili respingono la griglia proporzionalmente alla loro velocità. E 'stato abbastanza facile.

Ora lavoriamo sui buchi neri. Aggiungi questa linea a BlackHole.Update ().

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

Questo fa sì che il buco nero succhi nella griglia con una quantità variabile di forza. Ho riutilizzato il sprayAngle variabile, che farà sì che la forza sulla griglia pulsi in sincronia con l'angolo che irradia particelle (sebbene a metà della frequenza dovuta alla divisione per due). La forza passata varierà sinusoidalmente tra 10 e 30.

Infine, creeremo un'onda d'urto nella griglia quando la nave del giocatore respawn dopo la morte. Lo faremo tirando la griglia lungo l'asse z e permettendo alla forza di propagarsi e rimbalzare attraverso le molle. Di nuovo, questo richiede solo una piccola modifica a PlayerShip.Update ().

 if (IsDead) if (--framesUntilRespawn == 0) GameRoot.Grid.ApplyDirectedForce (nuovo Vector3 (0, 0, 5000), nuovo Vector3 (Posizione, 0), 50); ritorno; 

Qual'è il prossimo?

Abbiamo il gameplay di base e gli effetti implementati. Spetta a te trasformarlo in un gioco completo e raffinato con il tuo gusto. Prova ad aggiungere alcune interessanti nuove meccaniche, alcuni fantastici nuovi effetti o una storia unica. Nel caso in cui non sei sicuro di dove cominciare, ecco alcuni suggerimenti.

  • Crea nuovi tipi di nemici come serpenti o nemici che esplodono.
  • Crea nuovi tipi di armi come cercare missili o un fulmine.
  • Aggiungi una schermata del titolo e il menu principale.
  • Aggiungi una tabella dei punteggi più alti.
  • Aggiungi alcuni potenziamenti come uno scudo o delle bombe. Per i punti bonus, diventa creativo con i tuoi potenziamenti. Puoi fare potenziamenti che manipolano la gravità, alterano il tempo o crescono come organismi. Puoi attaccare una gigantesca palla demolitrice basata sulla fisica alla nave per distruggere i nemici. Sperimenta per trovare potenziamenti che siano divertenti e che aiutino il tuo gioco a distinguersi.
  • Crea più livelli. Livelli più duri possono introdurre nemici più duri e armi e potenziamenti più avanzati.
  • Permetti a un secondo giocatore di unirsi a un gamepad.
  • Permetti all'arena di scorrere in modo che possa essere più grande della finestra di gioco.
  • Aggiungi rischi ambientali come i laser.
  • Aggiungi un negozio o un sistema di livellamento e consenti al giocatore di guadagnare potenziamenti.

Grazie per aver letto!