Faire un vecteur de tir néon dans XNA The Warping Grid

Dans cette série de didacticiels, je vais vous montrer comment créer un jeu de tir au néon, tel que Geometry Wars, dans XNA. Le but de ces tutoriels n'est pas de vous laisser avec une réplique exacte de Geometry Wars, mais plutôt de passer en revue les éléments nécessaires qui vous permettront de créer votre propre variante de haute qualité..


Vue d'ensemble

Jusqu'à présent, dans la série, nous avons créé les effets de gameplay, de floraison et de particules. Dans cette dernière partie, nous allons créer une grille d’arrière-plan dynamique.

Avertissement: fort!

L'un des effets les plus intéressants de Geometry Wars est la grille d'arrière-plan qui déforme. Nous examinerons comment créer un effet similaire dans Shape Blaster. La grille réagira aux balles, aux trous noirs et au joueur qui se réveille. Ce n'est pas difficile à faire et ça a l'air génial.

Nous allons faire la grille en utilisant une simulation de printemps. À chaque intersection de la grille, nous allons mettre un petit poids et attacher un ressort de chaque côté. Ces ressorts ne feront que tirer et ne jamais pousser, un peu comme un élastique. Pour maintenir la grille en position, les masses à la limite de la grille seront ancrées sur place. Ci-dessous un schéma de la mise en page.

Nous allons créer une classe appelée la grille pour créer cet effet. Cependant, avant de travailler sur la grille elle-même, nous devons créer deux classes d'assistance: Printemps et PointMass.

La classe PointMass

le PointMass classe représente les masses auxquelles nous attacherons les ressorts. Les sources ne se connectent jamais directement aux autres sources. Au lieu de cela, ils appliquent une force aux masses qu’ils connectent, ce qui peut étirer d’autres sources..

 classe privée PointMass public Vector3 Position; public Vector3 Velocity; public float InverseMass; accélération privée de Vector3; amortissement des flotteurs privés = 0,98f; public PointMass (position Vector3, floM invMass) Position = position; InverseMass = invMass;  public vide ApplyForce (force Vector3) accélération + = force * InverseMass;  public void IncreaseDamping (facteur de flottement) amortissement * = facteur;  public void Update () Velocity + = accélération; Position + = vitesse; accélération = Vector3.Zero; if (Velocity.LengthSquared () < 0.001f * 0.001f) Velocity = Vector3.Zero; Velocity *= damping; damping = 0.98f;  

Il y a quelques points intéressants à propos de cette classe. Tout d'abord, notez qu'il stocke le inverse de la masse, 1 / masse. C'est souvent une bonne idée dans les simulations de physique, car les équations de physique ont tendance à utiliser l'inverse de la masse plus souvent, et parce qu'elles nous permettent de représenter facilement des objets immuables et infiniment lourds en réglant la masse inverse à zéro..

La classe contient également un amortissement variable. Ceci est utilisé à peu près comme friction ou résistance à l'air. Il ralentit progressivement la masse. Cela permet de calmer la grille et augmente également la stabilité de la simulation de printemps.

le Mettre à jour() méthode effectue le travail de déplacement de la masse ponctuelle de chaque image. Cela commence par l'intégration symplectique d'Euler, ce qui signifie simplement que nous ajoutons l'accélération à la vélocité, puis la vélocité mise à jour à la position. Ceci diffère de l'intégration standard d'Euler dans laquelle nous voudrions mettre à jour la vélocité après mise à jour de la position.

Pointe: Symplectic Euler est préférable pour les simulations printanières car il conserve de l’énergie. Si vous utilisez une intégration Euler régulière et créez des ressorts sans amortissement, ils auront tendance à s'étirer de plus en plus à chaque rebond à mesure qu'ils gagnent de l'énergie, ce qui finira par interrompre votre simulation..

Après avoir mis à jour la vélocité et la position, nous vérifions si la vélocité est très petite et, le cas échéant, nous la mettons à zéro. Cela peut être important pour les performances en raison de la nature des nombres à virgule flottante dénormalisés.

(Lorsque les nombres à virgule flottante deviennent très petits, ils utilisent une représentation spéciale appelée nombre dénormal. Cela présente l'avantage de permettre à float de représenter des nombres plus petits, mais cela a un prix. La plupart des chipsets ne peuvent pas utiliser leurs opérations arithmétiques standard. les nombres dénormalisés et les imite à l’aide d’une série d’étapes, ce qui peut être des dizaines à des centaines de fois plus lent que les opérations sur les nombres à virgule flottante normalisée. En multipliant notre vitesse par notre facteur d’amortissement, elle finira par devenir très petite. Nous ne nous intéressons pas vraiment à de si petites vitesses, nous le mettons donc à zéro.)

le Augmentation de l’amortissement () Cette méthode est utilisée pour augmenter temporairement l’amortissement. Nous l'utiliserons plus tard pour certains effets.

La classe de printemps

Un ressort relie deux masses ponctuelles et, s’il est tendu au-delà de sa longueur naturelle, applique une force qui tire les masses ensemble. Les ressorts suivent une version modifiée de la loi de Hooke avec amortissement:

\ [f = -kx - bv \]

  • \ (f \) est la force produite par le ressort.
  • \ (k \) est la constante du ressort ou la raideur du ressort.
  • \ (x \) est la distance à laquelle le ressort est tendu au-delà de sa longueur naturelle.
  • \ (b \) est le facteur d'amortissement.
  • \ (v \) est la vitesse.

Le code pour le Printemps la classe est comme suit.

 structure privée Spring public PointMass End1; public PointMass End2; public float TargetLength; flotteur public Rigidité; flottement public Amortissement; Ressort public (PointMass end1, PointMass end2, rigidité de flottement, amortissement de flottement) End1 = end1; End2 = end2; Raideur = raideur; Amortissement = amortissement; TargetLength = Vector3.Distance (end1.Position, end2.Position) * 0.95f;  public void Update () var x = End1.Position - End2.Position; float length = x.Length (); // ces ressorts ne peuvent que tirer, pas pousser si (longueur <= 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);  

Lorsque nous créons un ressort, nous fixons sa longueur naturelle à un peu moins de la distance entre les deux points d'extrémité. Cela maintient la grille tendue même au repos et améliore quelque peu l'apparence.

le Mettre à jour() méthode vérifie d’abord si le ressort est étiré au-delà de sa longueur naturelle. Si ce n'est pas étiré, rien ne se passe. Si c'est le cas, nous utilisons la loi de Hooke modifiée pour trouver la force du ressort et l'appliquer aux deux masses connectées..

Création de la grille

Maintenant que nous avons les classes imbriquées nécessaires, nous sommes prêts à créer la grille. Nous commençons par créer PointMass objets à chaque intersection sur la grille. Nous créons également des ancres inamovibles PointMass objets pour maintenir la grille en place. Nous relions ensuite les masses avec des ressorts.

 Printemps [] printemps; PointMass [,] points; grille publique (taille du rectangle, espacement Vector2) var springList = new List (); int numColumns = (int) (size.Width / spacing.X) + 1; int numRows = (int) (taille.Hauteur / espacement.Y) + 1; points = new PointMass [numColumns, numRows]; // ces points fixes seront utilisés pour ancrer la grille à des positions fixes sur l'écran PointMass [,] fixedPoints = new PointMass [numColumns, numRows]; // crée les masses ponctuelles int column = 0, row = 0; pour (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 (nouveau Spring (points [x - 1, y], points [x, y], rigidité, amortissement)); si (y> 0) springList.Add (nouveau ressort (points [x, y - 1], points [x, y], rigidité, amortissement));  springs = springList.ToArray (); 

La première pour La boucle crée des masses régulières et des masses fixes à chaque intersection de la grille. Nous n'utiliserons pas réellement toutes les masses immobiles, et les masses non utilisées seront simplement des ordures ramassées quelque temps après la fin du constructeur. Nous pourrions optimiser en évitant de créer des objets inutiles, mais comme la grille n'est généralement créée qu'une fois, cela ne fera pas beaucoup de différence.

En plus d'utiliser des masses de points d'ancrage autour de la bordure de la grille, nous utiliserons également certaines masses d'ancrage à l'intérieur de la grille. Ceux-ci seront utilisés pour aider très doucement à ramener la grille dans sa position initiale après avoir été déformée.

Comme les points d'ancrage ne bougent jamais, il n'est pas nécessaire de mettre à jour chaque image. Nous pouvons simplement les brancher aux sources et les oublier. Par conséquent, nous n'avons pas de variable membre dans le la grille classe pour ces masses.

Vous pouvez modifier un certain nombre de valeurs lors de la création de la grille. Les plus importants sont la rigidité et l’amortissement des ressorts. La rigidité et l’amortissement des ancrages de bordure et des ancrages intérieurs sont fixés indépendamment des ressorts principaux. Des valeurs de rigidité plus élevées accélèrent l'oscillation des ressorts, tandis que des valeurs d'amortissement plus élevées entraînent un ralentissement plus rapide des ressorts..

Manipulation de la grille

Pour que la grille se déplace, nous devons la mettre à jour à chaque image. C’est très simple, car nous avons déjà fait tout le travail difficile dans le PointMass et Printemps Des classes.

 public void Update () foreach (var printemps dans ressorts) spring.Update (); foreach (var masse en points) mass.Update (); 

Maintenant, nous allons ajouter quelques méthodes qui manipulent la grille. Vous pouvez ajouter des méthodes pour tout type de manipulation à laquelle vous pouvez penser. Nous allons implémenter ici trois types de manipulations: pousser une partie de la grille dans une direction donnée, pousser la grille vers l’extérieur à partir d’un point donné et tirer la grille vers un certain point. Les trois affecteront la grille dans un rayon donné à partir d'un point cible. Ci-dessous quelques images de ces manipulations en action.


Balles repoussant la grille vers l'extérieur.
Sucer la grille vers l'intérieur. Vague créée en poussant la grille le long de l'axe z.
 ApplyDirectedForce (force Vector3, position Vector3, rayon de flottement) foreach (masse variable en points) si (Vector3.DistanceSquared (position, masse.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);   

Nous utiliserons ces trois méthodes dans Shape Blaster pour différents effets.

Rendre la grille

Nous allons dessiner la grille en traçant des segments de ligne entre chaque paire de points voisins. Tout d'abord, nous allons faire une méthode d'extension sur SpriteBatch cela nous permet de dessiner des segments de ligne en prenant une texture d'un seul pixel et en l'étirant dans une ligne.

Ouvrez le Art classe et déclare une texture pour le pixel.

 statique public Texture2D Pixel get; ensemble privé; 

Vous pouvez définir la texture en pixels de la même manière que les autres images, ou vous pouvez simplement ajouter les deux lignes suivantes à la Art.Load () méthode.

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

Cela crée simplement une nouvelle texture 1x1px et définit le pixel unique en blanc. Maintenant, ajoutez la méthode suivante dans le Les extensions classe.

 public static void DrawLine (spriteBatch, spriteBatch, début de Vector2, fin de Vector2, couleur couleur, épaisseur de flottement = 2f) Delta2 de Vector2 = fin - début; spriteBatch.Draw (Art.Pixel, début, null, couleur, delta.ToAngle (), nouveau Vector2 (0, 0.5f), nouveau Vector2 (delta.Length (), épaisseur), SpriteEffects.None, 0f); 

Cette méthode étire, fait pivoter et teinte la texture en pixels pour produire la ligne souhaitée..

Ensuite, nous avons besoin d’une méthode pour projeter les points de grille 3D sur notre écran 2D. Normalement, cela pourrait être fait en utilisant des matrices, mais ici nous allons transformer les coordonnées manuellement à la place.

Ajouter ce qui suit au la grille classe.

 public Vector2 ToVec2 (Vector3 v) // effectue un facteur flottant de projection en perspective = (v.Z + 2000) / 2000; return (nouveau Vector2 (v.X, v.Y) - screenSize / 2f) * factor + screenSize / 2; 

Cette transformation donnera à la grille une vue en perspective où les points les plus éloignés apparaîtront plus rapprochés sur l'écran. Maintenant, nous pouvons dessiner la grille en parcourant les lignes et les colonnes et en traçant des lignes entre elles..

 public void Draw (SpriteBatch spriteBatch) int largeur = points.GetLength (0); int height = points.GetLength (1); Couleur couleur = nouvelle couleur (30, 30, 139, 85); // bleu foncé pour (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); épaisseur du flotteur = y% 3 == 1? 3f: 1f; spriteBatch.DrawLine (gauche, p, couleur, épaisseur);  if (y> 1) up = ToVec2 (points [x, y - 1] .Position); épaisseur du flotteur = x% 3 == 1? 3f: 1f; spriteBatch.DrawLine (haut, p, couleur, épaisseur); 

Dans le code ci-dessus, p est notre point actuel sur la grille, la gauche est le point directement à sa gauche et en haut est le point directement au-dessus de lui. Nous dessinons chaque troisième ligne plus épaisse à la fois horizontalement et verticalement pour un effet visuel.

Interpolation

Nous pouvons optimiser le réseau en améliorant la qualité visuelle d'un nombre donné de ressorts sans augmenter de manière significative le coût des performances. Nous allons faire deux de ces optimisations.

Nous allons densifier la grille en ajoutant des segments de ligne à l'intérieur des cellules de grille existantes. Nous le faisons en traçant des lignes à partir du centre d'un côté de la cellule jusqu'au milieu du côté opposé. L'image ci-dessous montre les nouvelles lignes interpolées en rouge.


Grille avec des lignes interpolées en rouge

Dessiner les lignes interpolées est simple. Si vous avez deux points, une et b, leur point médian est (a + b) / 2. Donc, pour dessiner les lignes interpolées, nous ajoutons le code suivant dans le pour boucles de notre Dessiner() méthode.

 if (x> 1 && y> 1) Vector2 upLeft = ToVec2 (points [x - 1, y - 1] .Position); spriteBatch.DrawLine (0.5f * (upLeft + up), 0.5f * (left + p), color, 1f); // ligne verticale spriteBatch.DrawLine (0.5f * (upLeft + left), 0.5f * (up + p), color, 1f); // ligne horizontale 

La deuxième amélioration consiste à effectuer une interpolation sur nos segments de droite pour les transformer en courbes plus lisses. XNA fournit le pratique Vector2.CatmullRom () méthode qui effectue une interpolation Catmull-Rom. Vous passez la méthode quatre points séquentiels sur une ligne courbe, et il retournera des points le long d'une courbe lisse entre les deuxième et troisième points que vous avez fournis.

Le cinquième argument à Vector2.CatmullRom () est un facteur de pondération qui détermine le point sur la courbe interpolée qu’elle renvoie. Un facteur de pondération de 0 ou 1 retournera respectivement le deuxième ou troisième point que vous avez fourni, et un facteur de pondération de 0.5 renverra le point sur la courbe interpolée à mi-chemin entre les deux points. En déplaçant progressivement le facteur de pondération de zéro à un et en traçant des lignes entre les points renvoyés, nous pouvons produire une courbe parfaitement lisse. Cependant, pour que le coût de performance reste faible, nous ne prendrons en compte qu'un seul point interpolé, avec un facteur de pondération de 0.5. Nous remplaçons ensuite la ligne droite originale dans la grille par deux lignes qui se rejoignent au point interpolé.

Le diagramme ci-dessous montre l'effet de cette interpolation.

Comme les segments de ligne dans la grille sont déjà petits, l’utilisation de plusieurs points interpolés ne fait généralement pas une différence notable..

Souvent, les lignes de notre grille seront très droites et ne nécessiteront aucun lissage. Nous pouvons vérifier cela et éviter de tracer deux lignes au lieu d'une. Nous vérifions si la distance entre le point interpolé et le milieu de la droite est supérieure à un pixel. Si c'est le cas, nous supposons que la ligne est courbe et nous dessinons deux segments. La modification de notre Dessiner() La méthode d'ajout d'une interpolation Catmull-Rom pour les lignes horizontales est présentée ci-dessous..

 left = ToVec2 (points [x - 1, y] .Position); épaisseur du flotteur = y% 3 == 1? 3f: 1f; // utilise l'interpolation Catmull-Rom pour aider à lisser les courbures de la grille int clampedX = Math.Min (x + 1, width - 1); Vector2 mid = Vector2.CatmullRom (ToVec2 (points [x - 2, y] .Position), gauche, p, ToVec2 (points [clampedX, y] .Position), 0,5f); // Si la grille est très droite ici, tracez une seule ligne droite. Sinon, tracez des lignes jusqu'à notre // nouveau milieu interpolé si ((Vector2.DistanceSquared (milieu, (gauche + p) / 2)> 1) 1 spriteBatch.DrawLine (gauche, milieu, couleur, épaisseur); spriteBatch.DrawLine (milieu, p, couleur, épaisseur);  else spriteBatch.DrawLine (gauche, p, couleur, épaisseur);

L'image ci-dessous montre les effets du lissage. Un point vert est dessiné à chaque point interpolé pour mieux illustrer où les lignes sont lissées.

Utilisation de la grille dans Shape Blaster

Il est maintenant temps d'utiliser la grille dans notre jeu. Nous commençons par déclarer un public, statique la grille variable dans GameRoot et créer la grille dans le GameRoot.Initialize () méthode. Nous allons créer une grille d'environ 1600 points comme si.

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

Puis on appelle Grid.Update () et Grid.Draw () du Mettre à jour() et Dessiner() méthodes en GameRoot. Cela nous permettra de voir la grille lorsque nous lancerons le jeu. Cependant, il reste encore à faire interagir divers objets du jeu avec la grille.

Les balles vont repousser la grille. Nous avons déjà fait une méthode pour le faire appelée ApplyExplosiveForce (). Ajouter la ligne suivante au Bullet.Update () méthode.

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

Cela fera en sorte que les balles repoussent la grille proportionnellement à leur vitesse. C'était assez facile.

Maintenant, travaillons sur les trous noirs. Ajouter cette ligne à BlackHole.Update ().

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

Cela fait que le trou noir aspire la grille avec une force variable. J'ai réutilisé le sprayAngle variable, ce qui provoque la pulsation de la force sur la grille en fonction de l’angle auquel elle pulvérise les particules (mais à la moitié de la fréquence en raison de la division par deux). La force transmise variera de manière sinusoïdale entre 10 et 30.

Enfin, nous créerons une onde de choc dans la grille lorsque le vaisseau du joueur réapparaîtra après sa mort. Nous allons le faire en tirant la grille le long de l'axe z, puis en laissant la force se propager et rebondir à travers les ressorts. Encore une fois, cela nécessite seulement une petite modification de PlayerShip.Update ().

 if (IsDead) if (--framesUntilRespawn == 0) GameRoot.Grid.ApplyDirectedForce (nouveau Vector3 (0, 0, 5000), nouveau Vector3 (Position, 0), 50); revenir; 

Et après?

Nous avons mis en place le gameplay et les effets de base. C'est à vous de le transformer en un jeu complet et raffiné à votre goût. Essayez d’ajouter de nouveaux mécanismes intéressants, de nouveaux effets sympas ou une histoire unique. Si vous ne savez pas par où commencer, voici quelques suggestions..

  • Créer de nouveaux types d'ennemis tels que des serpents ou des ennemis qui explosent.
  • Créer de nouveaux types d'arme tels que des missiles à la recherche ou un pistolet éclair.
  • Ajouter un écran de titre et un menu principal.
  • Ajouter une table des meilleurs scores.
  • Ajoutez des bonus comme un bouclier ou des bombes. Pour des points bonus, soyez créatif avec vos bonus. Vous pouvez créer des bonus qui manipulent la gravité, modifient le temps ou se développent comme des organismes. Vous pouvez attacher une boule de démolition géante basée sur la physique au navire pour écraser les ennemis. Expérimentez pour trouver des bonus amusants et aider votre jeu à se démarquer.
  • Créez plusieurs niveaux. Des niveaux plus difficiles peuvent introduire des ennemis plus durs et des armes et des bonus plus avancés.
  • Autoriser un second joueur à rejoindre un gamepad.
  • Permettre à l'arène de défiler afin qu'elle puisse être plus grande que la fenêtre de jeu.
  • Ajouter des dangers environnementaux tels que les lasers.
  • Ajouter une boutique ou un système de mise à niveau et permettre au joueur de gagner des améliorations.

Merci d'avoir lu!