Faire un vecteur Neon Shooter avec jME Warping Grid

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


Vue d'ensemble

Cette vidéo montre la grille en action:


Nous allons faire la grille en utilisant une simulation de ressorts: à chaque intersection de la grille, nous placerons un petit poids (une masse ponctuelle), et nous relierons ces poids à l'aide de ressorts. Ces ressorts ne feront que tirer et ne jamais pousser, un peu comme un élastique. Pour maintenir la grille en position, les masses autour 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 publique PointMass position privée Vector3f; privé Vector3f vélocité = Vector3f.ZERO; private float inverseMass; Accélération Vector3f privée = Vector3f.ZERO; amortissement des flotteurs privés = 0,98f; public PointMass (position Vector3f, float inverseMass) this.position = position; this.inverseMass = inverseMass;  public void applyForce (force Vector3f) acceleration.addLocal (force.mult (inverseMass));  public void augmentationDamping (facteur de flottement) amortissement * = facteur;  mise à jour publique void (float tpf) velocity.addLocal (acceleration.mult (1f)); position.addLocal (velocity.mult (0.6f)); accélération = Vector3f.ZERO.clone (); if (velocity.lengthSquared () < 0.0001f)  velocity = Vector3f.ZERO.clone();  velocity.multLocal(damping); damping = 0.98f; damping = 0.8f; position.z *= 0.9f; if (position.z < 0.01) position.z = 0;  public Vector3f getPosition()  return position;  public Vector3f getVelocity()  return velocity;  

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, qui agit pour ralentir progressivement la masse. Ceci est utilisé à peu près comme friction ou résistance à l'air. 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, si c'est le cas, nous la mettons à zéro. Cela peut être important pour les performances en raison de la nature des nombres à virgule flottante dénormalisés.

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 la suivante:

 Classe publique Spring private PointMass end1; pointMass privé end2; private float targetLength; rigidité du flotteur privé; amortissement des flotteurs privés; public Spring (PointMass end1, PointMass end2, rigidité de flottement, amortissement de flottement, nœud gridNode, booléen visible, Geometry defaultLine) this.end1 = end1; this.end2 = end2; this.stiffness = raideur; this.damping = amortissement; targetLength = end1.getPosition (). distance (end2.getPosition ()) * 0.95f; if (visible) defaultLine.addControl (new LineControl (end1, end2)); gridNode.attachChild (defaultLine);  mise à jour publique void (float tpf) Vector3f x = end1.getPosition (). subtract (end2.getPosition ()); longueur de float = x.length (); if (length> targetLength) x.normalizeLocal (); x.multLocal (length - targetLength); Vector3f dv = end2.getVelocity (). Subtract (end1.getVelocity ()); Force Vector3f = x.mult (rigidité); force.subtracte (dv.mult (amortissement / 10f)); end1.applyForce (force.negate ()); 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..

Nous devons créer une autre classe pour afficher correctement les lignes. le LineControl s'occupera de déplacer, redimensionner et faire pivoter les lignes:

 Classe publique LineControl étend AbstractControl private PointMass end1, end2; public LineControl (PointMass end1, PointMass end2) this.end1 = end1; this.end2 = end2;  @Override protected void controlUpdate (float tpf) // movement spatial.setLocalTranslation (end1.getPosition ()); // scale Vector3f dif = end2.getPosition (). subtract (end1.getPosition ()); spatial.setLocalScale (dif.length ()); // rotation spatial.lookAt (end2.getPosition (), new Vector3f (1,0,0));  @Override protected void controlRender (RenderManager, vP ViewPort) 

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:

 Classe publique Grid private Node gridNode; ressorts privés []; pointMass privé [] [] points; Géométrie privée defaultLine; Géométrie privée thickLine; grille publique (taille du rectangle, espacement Vector2f, nœud guiNode, AssetManager assetManager) gridNode = new Node (); guiNode.attachChild (gridNode); defaultLine = createLine (1f, assetManager); thickLine = createLine (3f, assetManager); ArrayList springList = new ArrayList (); rigidité du flotteur = 0,28f; amortissement du flotteur = 0,06f; int numColumns = (int) (size.width / spacing.x) + 2; int numRows = (int) (size.height / spacing.y) + 2; points = new PointMass [numColumns] [numRows]; PointMass [] [] fixedPoints = new PointMass [numColumns] [numRows]; // crée les masses de points float xCoord = 0, yCoord = 0; pour (int row = 0; row < numRows; row++)  for (int column = 0; column < numColumns; column++)  points[column][row] = new PointMass(new Vector3f(xCoord,yCoord,0),1); fixedPoints[column][row] = new PointMass(new Vector3f(xCoord,yCoord,0),0); xCoord += spacing.x;  yCoord += spacing.y; xCoord = 0;  // link the point masses with springs Geometry line; for (int y=0; y 0) si (y% 3 == 0) ligne = ligne épaisse;  else line = defaultLine;  springList.add (new Spring (points [x-1] [y], points [x] [y], rigidité, amortissement, gridNode, true, line.clone ()));  if (y> 0) if (x% 3 == 0) ligne = épaisLigne;  else line = defaultLine;  springList.add (new Spring (points [x] [y-1], points [x] [y], rigidité, amortissement, gridNode, true, line.clone ())); 

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 collectées à un moment donné 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..

Il y a une dernière chose à mentionner: le createLine () méthode.

 géométrie privée createLine (épaisseur de flottement, AssetManager assetManager) Vector3f [] vertices = new Vector3f (0,0,0), new Vector3f (0,0,1); int [] indices = 0,1; Mesh lineMesh = new Mesh (); lineMesh.setMode (Mesh.Mode.Lines); lineMesh.setLineWidth (épaisseur); lineMesh.setBuffer (VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer (sommets)); lineMesh.setBuffer (VertexBuffer.Type.Index, 1, BufferUtils.createIntBuffer (indices)); lineMesh.updateBound (); Geometry lineGeom = new Geometry ("lineMesh", lineMesh); Material matWireframe = new Material (assetManager, "Common / MatDefs / Misc / Unshaded.j3md"); matWireframe.getAdditionalRenderState (). setFaceCullMode (RenderState.FaceCullMode.Off); matWireframe.setColor ("Color", nouveau ColorRGBA (0,118f, 0,118f, 0,545f, 0,25f)); matWireframe.getAdditionalRenderState (). setBlendMode (BlendMode.AlphaAdditive); lineGeom.setMaterial (matWireframe); retourne lineGeom; 

Ici, nous créons essentiellement une ligne en spécifiant les sommets de la ligne et leur ordre, en créant un maillage, en ajoutant un matériau bleu, etc. Si vous voulez comprendre exactement le processus de création de la ligne, vous pouvez toujours consulter les tutoriels jME..

Pourquoi la création de ligne doit-elle être si compliquée? N’est-ce pas une simple ligne? Oui, c’est vrai, mais vous devez regarder ce que jME a l’intention de devenir. Habituellement, dans les jeux 3D, le jeu ne comporte pas de lignes simples ni de triangles, mais plutôt des modèles avec des textures et des animations. Ainsi, s’il est possible de générer une seule ligne dans jME, l’accent est mis principalement sur l’importation de modèles générés avec d’autres logiciels, tels que Blender..

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.

 mise à jour publique vide (float tpf) pour (int i = 0; i 

Nous allons maintenant ajouter quelques méthodes pour manipuler 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.

Voici quelques images de ces manipulations en action:


Vague créée en poussant la grille le long de l'axe z.
Balles repoussant la grille vers l'extérieur.
Sucer la grille vers l'intérieur.

Et voici les méthodes pour les effets:

 applyDirectedForce (force Vector3f, position Vector3f, rayon de flottant) pour (int x = 0; x 

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 la grille variable dans MonkeyBlasterMain et l'initialiser dans simpleInitApp ():

 Taille du rectangle = new Rectangle (0, 0, settings.getWidth (), settings.getHeight ()); Espacement Vector2f = nouveau Vector2f (25,25); grid = new Grid (taille, espacement, guiNode, assetManager);

Ensuite, nous devons appeler grid.update (float tpf) du SimpleUpdate méthode:

 @Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("alive")) spawnEnemies (); spawnBlackHoles (); handleCollisions (); handleGravity (tpf);  else if (System.currentTimeMillis () - (Long) player.getUserData ("dieTime")> 4000f &&! gameOver) // spawn player player.setLocalTranslation (500 500,0); guiNode.attachChild (lecteur); player.setUserData ("alive", true); sound.spawn ();  grid.update (tpf); hud.update (); 

Ensuite, nous devons appeler les méthodes d’effet aux bons endroits de notre jeu..

Le premier, créer une vague lorsque le joueur apparaît, est assez facile. Nous étendons simplement l’endroit où le joueur apparaît. simpleUpdate (float tpf) avec la ligne suivante:

 grid.applyDirectedForce (nouveau Vector3f (0,05000), player.getLocalTranslation (), 100);

Notez que nous appliquons une force dans le direction z. Nous avons peut-être un jeu en 2D mais, jME étant un moteur 3D, nous pouvons également utiliser facilement des effets 3D. Si nous tournions la caméra, nous verrions la grille rebondir vers l'intérieur et l'extérieur.

Les deuxième et troisième effets doivent être gérés dans les contrôles. Quand les balles volent dans le jeu, ils appellent cette méthode controlUpdate (float tpf):

 grid.applyExplosiveForce (direction.length () * (18f), spatial.getLocalTranslation (), 80);

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

C'est pareil avec les trous noirs:

 grid.applyImplosiveForce (FastMath.sin (sprayAngle / 2) * 10 +20, spatial.getLocalTranslation (), 250);

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 dix et 30.

Pour que cela fonctionne, vous ne devez pas oublier de passer la grille à BulletControl et BlackHoleControl.


Interpolation

Nous pouvons optimiser le réseau en améliorant la qualité visuelle pour un nombre donné de ressorts sans augmenter de manière significative le coût de performance..

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:

Nous allons créer ces lignes supplémentaires dans le constructeur de notre la grille classe. Si vous jetez un coup d'oeil, vous verrez deux pour boucles où nous relions les masses ponctuelles avec les ressorts. Insérez simplement ce bloc de code ici:

 if (x> 0 && y> 0) Geometry additionalLine = defaultLine.clone (); additionalLine.addControl (nouveau AdditionalLineControl (points [x-1] [y], points [x] [y], points [x-1] [y-1], points [x] [y-1]));; gridNode.attachChild (additionalLine); Géométrie additionalLine2 = defaultLine.clone (); additionalLine2.addControl [new AdditionalLineControl (points [x] [y-1], points [x] [y], points [x-1] [y-1], points [x-1] [y])); gridNode.attachChild (additionalLine2); 

Mais, comme vous le savez, la création d’objets n’est pas la seule chose à faire; nous devons également leur ajouter un contrôle afin de les amener à se comporter correctement. Comme vous pouvez le voir ci-dessus, le AdditionalLineControl obtient quatre masses ponctuelles passées afin qu’il puisse calculer sa position, sa rotation et son échelle:

 Classe publique AdditionalLineControl étend AbstractControl private PointMass end11, end12, end21, end22; public AdditionalLineControl (PointMass end11, PointMass end12, PointMass end21, PointMass end22) this.end11 = end11; this.end12 = end12; this.end21 = end21; this.end22 = end22;  @Override protected void controlUpdate (float tpf) // mouvement spatial.setLocalTranslation (position1 ()); // échelle Vector3f dif = position2 (). soustract (position1 ()); spatial.setLocalScale (dif.length ()); // rotation spatial.lookAt (position2 (), nouveau vecteur3f (1,0,0));  private Vector3f position1 () retourne un nouveau Vector3f (). interpolate (end11.getPosition (), end12.getPosition (), 0.5f);  private Vector3f position2 () retourne un nouveau Vector3f (). interpolate (end21.getPosition (), end22.getPosition (), 0.5f);  @Override protected void controlRender (RenderManager, vP ViewPort) 

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 un tableau des meilleurs scores.
  • Ajoutez des bonus, comme un bouclier ou des bombes. Pour des points bonus, faites preuve de créativité 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!