Faire un vecteur de tir néon avec jME HUD et trous noirs

Jusqu'à présent, dans cette série sur la construction d'un jeu inspiré de Geometry Wars dans jMonkeyEngine, nous avons implémenté la plupart du jeu et de l'audio. Dans cette partie, nous terminerons le jeu en ajoutant des trous noirs et une interface utilisateur pour afficher le score des joueurs..


Vue d'ensemble

Voici ce à quoi nous travaillons dans toute la série:


… Et voici ce que nous aurons à la fin de cette partie:


En plus de modifier les classes existantes, nous en ajouterons deux nouvelles:

  • BlackHoleControl: Inutile de dire que cela résoudra le comportement de nos trous noirs.
  • Hud: Ici, nous allons stocker et afficher le score des joueurs, des vies et d'autres éléments de l'interface utilisateur.

Commençons par les trous noirs.


Trous noirs

Le trou noir est l'un des ennemis les plus intéressants de Geometry Wars. Dans MonkeyBlaster, notre clone, c'est particulièrement cool lorsque nous ajoutons des effets de particules et la grille de déformation dans les deux prochains chapitres..

Fonctionnalité de base

Les trous noirs attireront le vaisseau du joueur, les ennemis proches et (après le prochain tutoriel) des particules, mais repousseront les balles..

Il existe de nombreuses fonctions que nous pouvons utiliser pour attirer ou repousser. Le plus simple consiste à utiliser une force constante, de sorte que le trou noir tire avec la même force, quelle que soit la distance de l'objet. Une autre option consiste à augmenter la force linéairement de zéro, à une certaine distance maximale, à la force maximale, pour les objets directement au-dessus du trou noir. Et si nous souhaitons modéliser la gravité de manière plus réaliste, nous pouvons utiliser le carré inverse de la distance, ce qui signifie que la force de gravité est proportionnelle à 1 / (distance * distance).

Nous utiliserons chacune de ces trois fonctions pour gérer différents objets. Les balles seront repoussées avec une force constante, les ennemis et le vaisseau du joueur seront attirés par une force linéaire, et les particules utiliseront une fonction carrée inverse.

la mise en oeuvre

Nous allons commencer par frapper nos trous noirs. Pour cela, nous avons besoin d’une autre varibale MonkeyBlasterMain:

 spawn long privéCooldownBlackHole;

Ensuite, nous devons déclarer un nœud pour les trous noirs; appelons ça blackHoleNode. Vous pouvez le déclarer et l’initialiser comme nous l’avons fait le ennemiNode dans le tutoriel précédent.

Nous allons aussi créer une nouvelle méthode, spawnBlackHoles, que nous appelons juste après spawnEnemies dans simpleUpdate (float tpf). Le frai actuel est assez similaire à celui des ennemis frai:

 void privé spawnBlackHoles () if (blackHoleNode.getQuantity () < 2)  if (System.currentTimeMillis() - spawnCooldownBlackHole > 10f) spawnCooldownBlackHole = System.currentTimeMillis (); if (new Random (). nextInt (1000) == 0) createBlackHole (); 

La création du trou noir suit également notre procédure standard:

 private void createBlackHole () Spatial blackHole = getSpatial ("Trou noir"); blackHole.setLocalTranslation (getSpawnPosition ()); blackHole.addControl (new BlackHoleControl ()); blackHole.setUserData ("active", false); blackHoleNode.attachChild (blackHole); 

Une fois encore, nous chargeons l’espace, définissons sa position, ajoutons un contrôle, le définissons sur non actif et l’attachons enfin au nœud approprié. Quand vous regardez BlackHoleControl, vous remarquerez que ce n'est pas très différent non plus.

Nous mettrons en œuvre l'attraction et la répulsion plus tard, dans MonkeyBlasterMain, mais il y a une chose à laquelle nous devons nous attaquer maintenant. Puisque le trou noir est un puissant ennemi, nous ne voulons pas qu’il s’effondre facilement. Par conséquent, nous ajoutons une variable, points de dommage, au BlackHoleControl, et définir sa valeur initiale à dix de sorte qu'il mourra après dix coups.

 Classe publique BlackHoleControl étend AbstractControl private long spawnTime; points de repère int privés; public BlackHoleControl () spawnTime = System.currentTimeMillis (); points de repère = 10;  @Override protected void controlUpdate (float tpf) if ((Boolean) spatial.getUserData ("active")) // nous utiliserons cet emplacement plus tard… else // gérons l'état "actif". Long dif = System.currentTimeMillis () - spawnTime; if (dif> = 1000f) spatial.setUserData ("actif", vrai);  ColorRGBA color = new ColorRGBA (1,1,1, dif / 1000f); Node spatialNode = (Node) spatial; Picture pic = (Picture) spatialNode.getChild ("Trou noir"); pic.getMaterial (). setColor ("Couleur", couleur);  @Override protected void controlRender (gestionnaire de fichiers RenderManager, ViewPort vp)  void public wasShot () hitpoints--;  public boolean isDead () return hitpoints <= 0;  

Nous avons presque fini avec le code de base pour les trous noirs. Avant de mettre en œuvre la gravité, nous devons nous occuper des collisions.

Lorsque le joueur ou un ennemi s'approche trop près du trou noir, il mourra. Mais quand une balle parvient à la toucher, le trou noir perd un point de vie.

Regardez le code suivant. Il appartient à handleCollisions (). C'est fondamentalement la même chose que pour toutes les autres collisions:

 // quelque chose entre-t-il en collision avec un trou noir? pour (i = 0; i 

Eh bien, vous pouvez tuer le trou noir maintenant, mais ce n’est pas le seul moment où il devrait disparaître. Chaque fois que le joueur meurt, tous les ennemis disparaissent, de même que le trou noir. Pour gérer cela, ajoutez simplement la ligne suivante à notre killPlayer () méthode:

 blackHoleNode.detachAllChildren ();

Maintenant son heure d'implémenter les trucs cool. Nous allons créer une autre méthode, handleGravity (float tpf). Appelez-le simplement avec les autres méthodes de simplueUpdate (float tpf).

Dans cette méthode, nous vérifions toutes les entités (joueurs, balles et ennemis) pour voir si elles sont proches d'un trou noir - disons dans les 250 pixels - et si elles le sont, nous appliquons l'effet approprié:

 handle de vide privéGravity (float tpf) pour (int i = 0; i 

Pour vérifier si deux entités sont à une certaine distance l'une de l'autre, nous créons une méthode appelée isNearby () qui compare les emplacements des deux spatiales:

 booléen privé isNearby (Spatial a, Spatial b, distance de flottement) Vector3f pos1 = a.getLocalTranslation (); Vector3f pos2 = b.getLocalTranslation (); retour pos1.distanceSquared (pos2) <= distance * distance; 

Maintenant que nous avons vérifié chaque entité, si elle est active et à la distance spécifiée d’un trou noir, nous pouvons enfin appliquer l’effet de la gravité. Pour ce faire, nous allons utiliser les contrôles: nous créons une méthode dans chaque contrôle, appelée applyGravity (Gravité Vector3f).

Jetons un coup d'oeil à chacun d'eux:

PlayerControl:

 vide public applyGravity (gravité Vector3f) spatial.move (gravité); 

BulletControl:

 vide public applyGravity (gravité Vector3f) direction.addLocal (gravité); 

SeekerControl et WandererControl:

 vide public applyGravity (gravité Vector3f) velocity.addLocal (gravité); 

Et maintenant retour à la classe principale, MonkeyBlasterMain. Je vais d'abord vous expliquer la méthode et expliquer les étapes à suivre:

 private void applyGravity (espace noir spatial, cible spatiale, float tpf) différence Vector3f = blackHole.getLocalTranslation (). soustract (target.getLocalTranslation ()); Gravité de Vector3f = difference.normalize (). MultLocal (tpf); distance de flottement = différence.longueur (); if (target.getName (). equals ("Player"))) gravity.multLocal (250f / distance); target.getControl (PlayerControl.class) .applyGravity (gravity.mult (80f));  else if (target.getName (). equals ("Bullet")) gravity.multLocal (250f / distance); target.getControl (BulletControl.class) .applyGravity (gravity.mult (-0.8f));  else if (target.getName (). equals ("Seeker")) target.getControl (SeekerControl.class) .applyGravity (gravity.mult (150000));  else if (target.getName (). equals ("Wanderer")) target.getControl (WandererControl.class) .applyGravity (gravity.mult (150000)); 

La première chose à faire est de calculer le Vecteur entre le trou noir et la cible. Ensuite, nous calculons la force de gravitation. La chose importante à noter est que nous multiplions encore la force par le temps écoulé depuis la dernière mise à jour., tpf, afin d'obtenir le même effet à chaque cadence. Enfin, nous calculons la distance entre la cible et le trou noir.

Pour chaque type de cible, nous devons appliquer la force de manière légèrement différente. Pour le joueur et pour les balles, la force augmente à mesure que l'on se rapproche du trou noir:

 gravité.multLocal (250f / distance);

Les balles doivent être repoussées; c'est pourquoi nous multiplions leur force de gravitation par un nombre négatif.

Les chercheurs et les vagabonds reçoivent simplement une force qui est toujours la même, quelle que soit leur distance du trou noir.

Nous avons maintenant terminé avec la mise en œuvre des trous noirs. Nous ajouterons des effets intéressants dans les prochains chapitres, mais pour le moment, vous pouvez le tester.!

Pointe: Notez que c'est votre Jeu; n'hésitez pas à modifier les paramètres de votre choix! Vous pouvez changer la zone d’effet pour le trou noir, la vitesse des ennemis ou du joueur… Ces choses ont un effet considérable sur le gameplay. Parfois, ça vaut la peine de jouer un peu avec les valeurs.

L'affichage tête haute

Certaines informations doivent être suivies et affichées sur le lecteur. C’est ce à quoi le HUD (Head-Up Display) est destiné. Nous voulons suivre les vies des joueurs, le multiplicateur de score actuel, et bien sûr le score lui-même, et montrer tout cela au joueur..

Lorsque le joueur marque 2 000 points (ou 4 000, ou 6 000, ou…), le joueur aura une autre vie. De plus, nous voulons sauvegarder le score après chaque match et le comparer au meilleur score actuel. Le multiplicateur augmente chaque fois que le joueur tue un ennemi et revient à un lorsque le joueur ne tue rien après un certain temps..

Nous allons créer une nouvelle classe pour tout cela, appelée Hud. Dans Hud nous avons pas mal de choses à initialiser dès le début:

 Classe publique Hud private AssetManager assetManager; nœud privé guiNode; private int screenWidth, screenHeight; int finale finale fontSize = 30; privé final int multiplierExpiryTime = 2000; final privé int maxMultiplier = 25; public int vies; public score int; public int multiplier; private long multiplierActivationTime; privé int scoreForExtraLife; private BitmapFont guiFont; BitmapText privé livesText; bitmapText privé scoreText; bitmapText privé multiplierText; nœud privé gameOverNode; public Hud (AssetManager assetManager, Node guiNode, int screenWidth, int screenHeight) this.assetManager = assetManager; this.guiNode = guiNode; this.screenWidth = screenWidth; this.screenHeight = screenHeight; setupText (); 

C'est beaucoup de variables, mais la plupart d'entre elles sont assez explicites. Nous devons avoir une référence à la Gestionnaire d'actifs charger du texte, au guiNode pour l'ajouter à la scène, etc..

Ensuite, il y a quelques variables que nous devons suivre en permanence, comme le multiplicateur, son heure d'expiration, le multiplicateur maximum possible et la vie du joueur.

Et enfin nous en avons BitmapText objets, qui stockent le texte actuel et l’affichent à l’écran. Ce texte est mis en place dans la méthode setupText (), qui s'appelle à la fin du constructeur.

 private void setupText () guiFont = assetManager.loadFont ("Interface / Fonts / Default.fnt"); livesText = new BitmapText (guiFont, false); livesText.setLocalTranslation (30, screenHeight-30,0); livesText.setSize (fontSize); livesText.setText ("Vies:" + vies); guiNode.attachChild (livesText); scoreText = new BitmapText (guiFont, true); scoreText.setLocalTranslation (screenWidth - 200, screenHeight-30,0); scoreText.setSize (fontSize); scoreText.setText ("Score:" + score); guiNode.attachChild (scoreText); multiplierText = new BitmapText (guiFont, true); multiplierText.setLocalTranslation (screenWidth-200, screenHeight-100,0); multiplierText.setSize (fontSize); multiplierText.setText ("Multiplier:" + lives); guiNode.attachChild (multiplierText); 

Pour charger du texte, nous devons d'abord charger la police. Dans notre exemple, nous utilisons une police par défaut fournie avec jMonkeyEngine..

Pointe: Bien sûr, vous pouvez créer vos propres polices, les placer quelque part dans le les atouts répertoire de préférence actifs / interface-et les charger. Si vous voulez en savoir plus, consultez ce tutoriel sur le chargement de polices dans jME..

Ensuite, nous aurons besoin d’une méthode pour réinitialiser toutes les valeurs afin de pouvoir tout recommencer si le joueur meurt trop de fois:

 public void reset () score = 0; multiplicateur = 1; vies = 4; multiplierActivationTime = System.currentTimeMillis (); scoreForExtraLife = 2000; updateHUD (); 

La réinitialisation des valeurs est simple, mais nous devons également appliquer les modifications des variables au HUD. Nous faisons cela dans une méthode séparée:

 updateHUD () privé livesText.setText ("Lives:" + lives); scoreText.setText ("Score:" + score); multiplierText.setText ("Multiplier:" + multiplier); 

Pendant la bataille, le joueur gagne des points et perd des vies. Nous appellerons ces méthodes de MonkeyBlasterMain:

 public vide addPoints (int basePoints) score + = basePoints * multiplicateur; si (score> = scoreForExtraLife) scoreForExtraLife + = 2000; vit ++;  augmentationMultiplier (); updateHUD ();  private void augmentationMultiplier () multiplierActivationTime = System.currentTimeMillis (); si (multiplicateur < maxMultiplier)  multiplier++;   public boolean removeLife()  if (lives == 0) return false; lives--; updateHUD(); return true; 

Les concepts notables dans ces méthodes sont:

  • Chaque fois que nous ajoutons des points, nous vérifions si nous avons déjà atteint le score nécessaire pour obtenir une vie supplémentaire.
  • Chaque fois que nous ajoutons des points, nous devons également augmenter le multiplicateur en appelant une méthode distincte..
  • Chaque fois que nous augmentons le multiplicateur, nous devons être conscients du multiplicateur maximal possible et ne pas aller au-delà..
  • Chaque fois que le joueur frappe un ennemi, nous devons réinitialiser le multiplierActivationTime.
  • Quand le joueur n'a plus aucune vie à enlever, nous revenons faux afin que la classe principale puisse agir en conséquence.

Il nous reste deux choses à gérer.

Premièrement, nous devons réinitialiser le multiplicateur si le joueur ne tue rien pendant un moment. Nous allons mettre en œuvre un mettre à jour() méthode qui vérifie s'il est temps de le faire:

 public void update () if (multiplicateur> 1) if (System.currentTimeMillis () - multiplierActivationTime> multiplierExpiryTime) multiplier = 1; multiplierActivationTime = System.currentTimeMillis (); updateHUD (); 

La dernière chose dont nous devons nous occuper est la fin du match. Lorsque le joueur a épuisé toutes ses vies, la partie est terminée et le score final doit être affiché au centre de l'écran. Nous devons également vérifier si le meilleur score actuel est inférieur au score actuel du joueur et, le cas échéant, enregistrer le score actuel en tant que nouveau score le plus élevé. (Notez que vous devez créer un fichier highscore.txt d’abord, sinon vous ne pourrez pas charger de partition.)

Voici comment nous terminons le jeu dans Hud:

 public void endGame () // init gameOverNode gameOverNode = new Node (); gameOverNode.setLocalTranslation (screenWidth / 2 - 180, screenHeight / 2 + 100,0); guiNode.attachChild (gameOverNode); // check highscore int highscore = loadHighscore (); if (score> highscore) saveHighscore (); // initier et afficher le texte BitmapText gameOverText = new BitmapText (guiFont, false); gameOverText.setLocalTranslation (0,0,0); gameOverText.setSize (fontSize); gameOverText.setText ("Game Over"); gameOverNode.attachChild (gameOverText); BitmapText yourScoreText = new BitmapText (guiFont, false); yourScoreText.setLocalTranslation (0, -50,0); yourScoreText.setSize (fontSize); yourScoreText.setText ("Votre score:" + score); gameOverNode.attachChild (yourScoreText); BitmapText highscoreText = new BitmapText (guiFont, false); highscoreText.setLocalTranslation (0, -100,0); highscoreText.setSize (fontSize); highscoreText.setText ("Highscore:" + highscore); gameOverNode.attachChild (highscoreText); 

Enfin, nous avons besoin de deux dernières méthodes: loadHighscore () et saveHighscore ():

 private int loadHighscore () try FileReader fileReader = new FileReader (nouveau Fichier ("highscore.txt")); BufferedReader reader = new BufferedReader (fileReader); Chaîne de caractères = reader.readLine (); return Integer.valueOf (line);  catch (FileNotFoundException e) e.printStackTrace ();  catch (IOException e) e.printStackTrace (); retourne 0;  private void saveHighscore () try FileWriter writer = nouveau FileWriter (nouveau Fichier ("highscore.txt"), false); writer.write (score + System.getProperty ("line.separator")); writer.close ();  catch (IOException e) e.printStackTrace ();
Pointe: Comme vous l'avez peut-être remarqué, je n'ai pas utilisé le gestionnaire d'actifs charger et sauvegarder le texte. Nous l'avons utilisé pour charger tous les sons et les graphiques, et le correct jME moyen de charger et de sauvegarder des textes utilise en fait le gestionnaire d'actifs pour cela, mais comme il ne supporte pas le chargement de fichier texte seul, nous aurions besoin d'enregistrer un TextLoader avec le gestionnaire d'actifs. Vous pouvez le faire si vous voulez, mais dans ce tutoriel, je suis resté fidèle à la méthode de chargement et d’enregistrement de texte par défaut de Java, par souci de simplicité.

Nous avons maintenant une grande classe qui traitera tous nos problèmes liés au HUD. La seule chose que nous devons faire maintenant, c'est l'ajouter au jeu..

Nous devons déclarer l'objet au début:

 Hud Hud privé;

… L'initialise dans simpleInitApp ():

 hud = new Hud (assetManager, guiNode, settings.getWidth (), settings.getHeight ()); hud.reset ();

… Mettre à jour le HUD dans simpleUpdate (float tpf) (peu importe si le joueur est en vie):

 hud.update ();

… Ajouter des points lorsque le joueur frappe des ennemis (en checkCollisions ()):

 // ajouter des points en fonction du type d'ennemi if ((ennemiNode.getChild (i). Nommé (). égal ("chercheur")) hud.addPoints (2);  else if (ennemiNode.getChild (i) .getName (). est égal à ("Voyageur")) hud.addPoints (1); 
Fais attention! Vous devez ajouter les points avant vous détachez les ennemis de la scène ou vous rencontrez des problèmes avec ennemiNode.getChild (i).

… Et enlever des vies lorsque le joueur meurt (dans killPlayer ()):

 if (! hud.removeLife ()) hud.endGame (); gameOver = true; 

Vous avez peut-être remarqué que nous avons également introduit une nouvelle variable, jeu terminé. Nous allons le mettre à faux au début:

 gameOver booléen privé = false;

Le joueur ne doit plus apparaître une fois le jeu terminé. Nous ajoutons cette condition à simpleUpdate (float tpf)

  else if (System.currentTimeMillis () - (Long) player.getUserData ("dieTime")> 4000f &&! gameOver) 

Maintenant, vous pouvez lancer le jeu et vérifier si vous avez manqué quelque chose! Et votre jeu a un nouvel objectif: battre les meilleurs scores. Je te souhaite bonne chance!

Curseur personnalisé

Comme nous avons un jeu en 2D, il reste encore quelque chose à ajouter pour perfectionner notre HUD: un curseur de souris personnalisé..
Ce n'est rien de spécial il suffit d'insérer cette ligne dans simpleInitApp ():

 inputManager.setMouseCursor ((JmeCursor) assetManager.loadAsset ("Textures / Pointer.ico"));

Conclusion

Le gameplay est maintenant complètement terminé. Dans les deux parties restantes de cette série, nous ajouterons des effets graphiques intéressants. Cela rendra le jeu un peu plus difficile, car les ennemis risquent de ne plus être aussi faciles à repérer.!