Dans la première partie de cette série sur la création d'un jeu inspiré de Geometry Wars dans jMonkeyEngine, nous avons implémenté le vaisseau du joueur et l'avons laissé bouger et tirer. Cette fois, nous allons ajouter les ennemis et les effets sonores.
Voici ce à quoi nous travaillons dans toute la série:
… Et voici ce que nous aurons à la fin de cette partie:
Nous aurons besoin de nouvelles classes pour implémenter les nouvelles fonctionnalités:
SeekerControl
: Ceci est une classe de comportement pour l'ennemi chercheur.WandererControl
: Ceci est également une classe de comportement, cette fois pour l'ennemi errant.Du son
: Nous allons gérer le chargement et la lecture des effets sonores et de la musique avec cette.Comme vous l'aurez deviné, nous ajouterons deux types d'ennemis. Le premier s'appelle un chercheur; il poursuivra activement le joueur jusqu'à sa mort. L'autre, le vagabond, se promène juste autour de l'écran dans un motif aléatoire.
Nous allons engendrer les ennemis à des positions aléatoires sur l'écran. Afin de donner au joueur le temps de réagir, l'ennemi ne sera pas actif immédiatement, mais se fondra plutôt lentement. Une fois complètement effacé, il commencera à se déplacer à travers le monde. Quand il entre en collision avec le joueur, le joueur meurt; quand il entre en collision avec une balle, il meurt.
Tout d’abord, nous devons créer de nouvelles variables dans le MonkeyBlasterMain
classe:
privé long ennemiSpawnCooldown; flotteur privé ennemiSpawnChance = 80; nœud privé ennemi;
Nous allons utiliser les deux premiers assez tôt. Avant cela, nous devons initialiser le ennemiNode
dans simpleInitApp ()
:
// configuration du noeud ennemi ennemiNode = new Node ("ennemis"); guiNode.attachChild (ennemiNode);
Bon, passons maintenant au vrai code de frai: nous remplacerons simpleUpdate (float tpf)
. Cette méthode est appelée par le moteur encore et encore, et continue simplement à appeler la fonction de génération d'ennemis tant que le joueur est en vie. (Nous avons déjà défini les données utilisateur vivant
à vrai
dans le dernier tutoriel.)
@Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("alive")) spawnEnemies ();
Et voici comment nous engendrons les ennemis:
void privé spawnEnemies () if (System.currentTimeMillis () - ennemisSpawnCooldown> = 17) ennemiSpawnCooldown = System.currentTimeMillis (); if (ennemiNode.getQuantity () < 50) if (new Random().nextInt((int) enemySpawnChance) == 0) createSeeker(); if (new Random().nextInt((int) enemySpawnChance) == 0) createWanderer(); //increase Spawn Time if (enemySpawnChance >= 1.1f) ennemiSpawnChance - = 0,005f;
Ne soyez pas confus par le ennemiSpawnCooldown
variable. Ce n'est pas là pour faire frayer l'ennemi à une fréquence décente - 17 ms serait beaucoup trop court.
ennemiSpawnCooldown
est réellement là pour assurer que la quantité de nouveaux ennemis est la même sur chaque machine. Sur des ordinateurs plus rapides, simpleUpdate (float tpf)
se fait appeler beaucoup plus souvent que sur les plus lents. Avec cette variable, nous vérifions environ toutes les 17 ms si nous devons générer de nouveaux ennemis..
Mais voulons-nous les engendrer toutes les 17ms? Nous voulons en fait qu’ils se reproduisent à intervalles aléatoires, nous introduisons donc un si
déclaration:
if (new Random (). nextInt ((int) ennemisSpawnChance) == 0)
Plus la valeur de ennemiSpawnChance
, plus il est probable qu'un nouvel ennemi apparaisse dans cet intervalle de 17 ms, et plus le joueur devra affronter d'ennemis. C'est pourquoi nous soustrayons un peu de ennemiSpawnChance
chaque tick: cela signifie que le jeu deviendra plus difficile avec le temps.
Créer des chercheurs et des vagabonds est similaire à créer un autre objet:
void privé createSeeker () chercheur spatial = getSpatial ("chercheur"); seeker.setLocalTranslation (getSpawnPosition ()); seeker.addControl (nouveau SeekerControl (lecteur)); seeker.setUserData ("active", false); ennemiNode.attachChild (chercheur); void privé createWanderer () Spatial wanderer = getSpatial ("Wanderer"); wanderer.setLocalTranslation (getSpawnPosition ()); wanderer.addControl (new WandererControl ()); wanderer.setUserData ("active", false); ennemiNode.attachChild (vagabond);
Nous créons l’espace, nous le déplaçons, nous ajoutons un contrôle personnalisé, nous le désactivons et nous l’attachons à notre ennemiNode
. Quoi? Pourquoi non-actif? C'est parce que nous ne voulons pas que l'ennemi commence à pourchasser le joueur dès qu'il apparaît; nous voulons donner au joueur un peu de temps pour réagir.
Avant d'entrer dans les contrôles, nous devons implémenter la méthode getSpawnPosition ()
. L'ennemi devrait apparaître de manière aléatoire, mais pas juste à côté du joueur:
privé Vector3f getSpawnPosition () Vector3f pos; do pos = new Vector3f (new Random (). nextInt (settings.getWidth ()), new Random (). nextInt (settings.getHeight ()), 0); while (pos.distanceSquared (player.getLocalTranslation ()) < 8000); return pos;
Nous calculons une nouvelle position aléatoire pos
. Si c'est trop près du joueur, nous calculons une nouvelle position et répétons jusqu'à ce que la distance soit correcte.
Il ne reste plus qu’à rendre les ennemis actifs et à commencer à bouger. Nous ferons cela dans leurs contrôles.
Nous nous occuperons du SeekerControl
premier:
Classe publique SeekerControl étend AbstractControl lecteur Spatial privé; vitesse privée de Vector3f; privé long spawnTime; public SeekerControl (lecteur spatial) this.player = player; vélocité = nouveau Vector3f (0,0,0); spawnTime = System.currentTimeMillis (); @Override protected void controlUpdate (float tpf) if ((Booléen) spatial.getUserData ("active")) // traduit le chercheur Vector3f playerDirection = player.getLocalTranslation (). Subtract (spatial.getLocalTranslation ()); playerDirection.normalizeLocal (); playerDirection.multLocal (1000f); velocity.addLocal (playerDirection); vélocité.multLocal (0.8f); spatial.move (velocity.mult (tpf * 0.1f)); // fait pivoter le chercheur si (vélocité! = vecteur3f.ZERO) spatial.rotateUpTo (vélocité.normalise ()); spatial.rotate (0,0, FastMath.PI / 2f); else // gérer 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 ("Seeker"); pic.getMaterial (). setColor ("Couleur", couleur); @Override protected controlRender void (RenderManager, vP ViewPort)
Concentrons-nous sur controlUpdate (float tpf)
:
Premièrement, nous devons vérifier si l'ennemi est actif. Si ce n'est pas le cas, nous devons le faire disparaître lentement.
Nous vérifions ensuite le temps écoulé depuis que nous avons engendré l'ennemi et, s'il est suffisamment long, nous le mettons en service..
Que nous l'ayions activée ou non, nous devons en ajuster la couleur. La variable locale spatial
contient l'espace auquel le contrôle a été attaché, mais vous vous souviendrez peut-être que nous n'avons pas attaché le contrôle à l'image réelle. L'image est un enfant du nœud auquel nous avons attaché le contrôle. (Si vous ne savez pas de quoi je parle, jetez un coup d'œil à la méthode getSpatial (nom de chaîne)
nous avons implémenté le dernier tutoriel.)
Alors; nous obtenons l'image d'un enfant de spatial
, obtenir son matériau et définir sa couleur à la valeur appropriée. Rien de spécial une fois que vous êtes habitué aux spatiales, matériaux et nœuds.
1
dans notre code). Ne voulons-nous pas un ennemi jaune et rouge?Nous devons maintenant examiner ce que nous faisons lorsque l’ennemi est actif. Ce contrôle est nommé SeekerControl
pour une raison: nous voulons que les ennemis avec ce contrôle attaché suivent le joueur.
Pour ce faire, nous calculons la direction du chercheur au joueur et ajoutons cette valeur à la vélocité. Après cela, nous diminuons la vitesse de 80% pour qu’elle ne puisse pas croître à l’infini et nous déplaçons le chercheur.
La rotation n'a rien de spécial: si le chercheur n'est pas immobile, nous le faisons pivoter dans la direction du joueur. Nous faisons ensuite la rotation un peu plus car le chercheur Seeker.png
ne pointe pas vers le haut, mais vers la droite.
rotateUpTo (direction de Vector3f)
méthode de Spatial
fait une rotation spatiale de sorte que son axe des y pointe dans la direction indiquée. C'était donc le premier ennemi. Le code du deuxième ennemi, le vagabond, n’est pas très différent:
Classe publique WandererControl étend AbstractControl private int screenWidth, screenHeight; vitesse privée de Vector3f; float privé directionAngle; privé long spawnTime; public WandererControl (int screenWidth, int screenHeight) this.screenWidth = screenWidth; this.screenHeight = screenHeight; vélocité = nouveau Vector3f (); directionAngle = new Random (). nextFloat () * FastMath.PI * 2f; spawnTime = System.currentTimeMillis (); @Override protected void controlUpdate (float tpf) if ((Boolean) spatial.getUserData ("active")) // traduit le vagabond // change la directionAngle du bit directionAngle + = (nouveau Random (). NextFloat () * 20f - 10f) * tpf; System.out.println (directionAngle); Vector3f directionVector = MonkeyBlasterMain.getVectorFromAngle (directionAngle); directionVector.multLocal (1000f); velocity.addLocal (directionVector); // diminue un peu la vitesse et déplace le vagabond velocity.multLocal (0.8f); spatial.move (velocity.mult (tpf * 0.1f)); // faire rebondir le voyageur sur les bords de l'écran Vector3f loc = spatial.getLocalTranslation (); if (loc.x screenWidth || loc.y> screenHeight) Vector3f newDirectionVector = new Vector3f (screenWidth / 2, screenHeight / 2,0) .subtract (loc); directionAngle = MonkeyBlasterMain.getAngleFromVector (newDirectionVector); // fait pivoter le voyageur spatial.rotate (0,0, tpf * 2); else // gère 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 ("Wanderer"); pic.getMaterial (). setColor ("Couleur", couleur); @Override protected controlRender void (RenderManager, vP ViewPort)
La chose la plus facile en premier: atténuer l’ennemi dans est identique à celle du contrôle du chercheur. Dans le constructeur, on choisit une direction aléatoire pour le vagabond, dans laquelle il volera une fois activé.
Pointe: Si vous avez plus de deux ennemis, ou si vous voulez simplement structurer le jeu plus proprement, vous pouvez ajouter un troisième contrôle:EnemyControl
Il gérerait tout ce que tous les ennemis avaient en commun: déplacer l'ennemi, le faire disparaître, l'activer… Passons maintenant aux différences majeures:
Lorsque l'ennemi est actif, nous modifions d'abord légèrement sa direction afin que le vagabond ne se déplace pas en ligne droite tout le temps. Nous faisons cela en changeant notre directionAngle
un peu et en ajoutant le directionVector
au rapidité
. Nous appliquons ensuite la vitesse juste comme nous le faisons dans le SeekerControl
.
Nous devons vérifier si le vagabond se trouve en dehors des limites de l’écran et, le cas échéant, modifier le directionAngle
à une direction plus appropriée pour qu'il soit appliqué dans la prochaine mise à jour.
Enfin, nous tournons un peu le vagabond. C'est juste parce qu'un ennemi en rotation semble plus froid.
Maintenant que nous avons fini d'implémenter les deux ennemis, vous pouvez commencer le jeu et jouer un peu. Cela vous donne un petit aperçu de la façon dont le jeu va se dérouler, même si vous ne pouvez pas tuer les ennemis et qu'ils ne peuvent pas vous tuer non plus. Ajoutons que la prochaine.
Afin de faire tuer le joueur par des ennemis, nous devons savoir s’ils entrent en collision. Pour cela, nous allons ajouter une nouvelle méthode, handleCollisions
, appelé dans simpleUpdate (float tpf)
:
@Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("alive")) spawnEnemies (); handleCollisions ();
Et maintenant la méthode actuelle:
private void handleCollisions () // le joueur doit-il mourir? pour (int i = 0; iNous parcourons tous les ennemis en obtenant la quantité des enfants du nœud et en obtenant chacun d'eux. De plus, il suffit de vérifier si l'ennemi tue le joueur quand l'ennemi est réellement actif. Si ce n'est pas le cas, nous n'avons pas besoin de nous en soucier. Donc, s'il est actif, nous vérifions si le joueur et l'ennemi entrent en collision. Nous faisons cela d'une autre méthode,
checkCollisoin (Spatial a, Spatial b)
:contrôle booléen privéCollision (Spatial a, Spatial b) distance flottante = a.getLocalTranslation (). distance (b.getLocalTranslation ()); float maxDistance = (Float) a.getUserData ("radius") + (Float) b.getUserData ("radius"); distance de retour <= maxDistance;Le concept est assez simple: tout d'abord, nous calculons la distance entre les deux spatiales. Ensuite, nous devons savoir à quel point les deux spatiales doivent être proches les unes des autres pour pouvoir être considérées comme étant entrées en collision. Nous obtenons donc le rayon de chaque spatial et nous les ajoutons. (Nous définissons les données utilisateur "rayon" dans
getSpatial (nom de chaîne)
dans le didacticiel précédent.) Ainsi, si la distance réelle est inférieure ou égale à cette distance maximale, la méthode retournevrai
, ce qui signifie qu'ils sont entrés en collision.Et maintenant? Nous devons tuer le joueur. Créons une autre méthode:
kill voilier privé () player.removeFromParent (); player.getControl (PlayerControl.class) .reset (); player.setUserData ("alive", false); player.setUserData ("dieTime", System.currentTimeMillis ()); ennemiNode.detachAllChildren ();Premièrement, nous séparons le lecteur de son nœud parent, ce qui le supprime automatiquement de la scène. Ensuite, nous devons réinitialiser le mouvement dans
PlayerControl
-sinon, le joueur pourrait toujours bouger quand il se reproduirait.Nous définissons ensuite les données utilisateur
vivant
àfaux
et créer une nouvelle userdataheure de mort
. (Nous en aurons besoin pour réapparaître le joueur lorsqu'il sera mort.)Enfin, nous détachons tous les ennemis, car le joueur aurait du mal à combattre les ennemis déjà existants au moment où il apparait..
Nous avons déjà parlé de la réapparition, alors prenons cela en charge. Nous allons, encore une fois, modifier le
simpleUpdate (float tpf)
méthode:@Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("alive")) spawnEnemies (); handleCollisions (); else if (System.currentTimeMillis () - (Long) player.getUserData ("dieTime")> 4000f &&! gameOver) // spawn player player.setLocalTranslation (500 500,0); guiNode.attachChild (lecteur); player.setUserData ("alive", true);Donc, si le joueur n'est pas en vie et qu'il est mort suffisamment longtemps, nous plaçons sa position au milieu de l'écran, nous l'ajoutons à la scène et nous définissons enfin ses données utilisateur.
vivant
àvrai
encore!Le moment est peut-être venu de commencer le jeu et de tester nos nouvelles fonctionnalités. Vous aurez du mal à durer plus de vingt secondes car votre arme ne vaut rien, alors faisons quelque chose à ce sujet..
Pour que les balles tuent les ennemis, nous allons ajouter du code à la
handleCollisions ()
méthode:// un ennemi devrait-il mourir? int i = 0; alors que je < enemyNode.getQuantity()) int j=0; while (j < bulletNode.getQuantity()) if (checkCollision(enemyNode.getChild(i),bulletNode.getChild(j))) enemyNode.detachChildAt(i); bulletNode.detachChildAt(j); break; j++; i++;La procédure pour tuer des ennemis est à peu près la même que pour tuer le joueur; nous parcourons tous les ennemis et toutes les balles, vérifions si elles entrent en collision et, si tel est le cas, nous les détachons.
À présent lancer le jeu et voir jusqu'où vous obtenez!
Info: Itérer à travers chaque ennemi et comparer sa position à celle de chaque balle est un très mauvais moyen de vérifier les collisions. C'est bien dans cet exemple, par souci de simplicité, mais dans un réal Pour ce faire, vous devrez implémenter de meilleurs algorithmes, comme la détection des collisions sur quatre arbres. Heureusement, jMonkeyEngine utilise le moteur physique Bullet. Ainsi, chaque fois que vous avez de la physique 3D complexe, vous n'avez pas à vous soucier de cela..Nous en avons terminé avec le gameplay principal. Nous allons toujours implémenter des trous noirs et afficher le score et la vie du joueur. Pour rendre le jeu plus amusant et plus excitant, nous ajouterons des effets sonores et de meilleurs graphismes. Ce dernier sera obtenu grâce au filtre de post-traitement de la floraison, à des effets de particules et à un effet de fond froid..
Avant de considérer cette partie de la série comme terminée, nous allons ajouter de l'audio et l'effet Bloom.
Jouer des sons et de la musique
Pour créer un son pour notre jeu, nous allons créer une nouvelle classe, simplement appelée
Du son
:public class Sound musique privée AudioNode; plans AudioNode [] privés; explosions AudioNode [] privées; private AudioNode [] apparaît; AssetManager AssetManager privé; son public (AssetManager assetManager) this.assetManager = assetManager; shots = new AudioNode [4]; explosions = nouvel AudioNode [8]; spawns = new AudioNode [8]; loadSounds (); private void loadSounds () music = new AudioNode (assetManager, "Sounds / Music.ogg"); music.setPositional (false); music.setReverbEnabled (false); music.setLooping (true); pour (int i = 0; iIci, on commence par mettre en place le nécessaire
AudioNode
variables et initialiser les tableaux.Ensuite, nous chargeons les sons et, pour chaque son, nous faisons à peu près la même chose. Nous créons un nouveau
AudioNode
, avec l'aide dugestionnaire d'actifs
. Ensuite, nous réglons la position et désactivons la réverbération. (Nous n'avons pas besoin que le son soit positionnel car nous n'avons pas de sortie stéréo dans notre jeu en 2D, mais vous pouvez le mettre en œuvre si vous le souhaitez.) La désactivation de la réverbération permet de reproduire le son comme dans l'audio réel. fichier; si nous l'activions, nous pourrions faire en sorte que jME laisse l'audio sonner comme si nous étions dans une grotte ou un donjon, par exemple. Après cela, nous mettons la boucle àvrai
pour la musique et àfaux
pour tout autre son.Jouer les sons est assez simple: il suffit d'appeler
Info: Lorsque vous appelez simplementsoundX.play ()
.jouer()
sur certains sons, il ne fait que lire le son. Mais parfois, nous voulons jouer le même son deux fois, voire plus, simultanément. C'est ce queplayInstance ()
existe pour: il crée une nouvelle instance pour chaque son afin que nous puissions jouer le même son plusieurs fois en même temps.Je vous laisse le reste du travail: vous devez appeler
startMusic
,tirer()
,explosion()
(pour les ennemis mourants), etfrayer()
aux endroits appropriés dans notre classe principaleMonkeyBlasterMain ()
.Lorsque vous aurez terminé, vous constaterez que le jeu est maintenant beaucoup plus amusant. ces quelques effets sonores ajoutent vraiment à l'atmosphère. Mais finissons un peu le graphisme.
Ajout du filtre de post-traitement de Bloom
L'activation de bloom est très simple dans jMonkeyEngine, car tout le code et les shaders nécessaires sont déjà implémentés pour vous. Allez-y et collez ces lignes dans
simpleInitApp ()
:FilterPostProcessor fpp = new FilterPostProcessor (assetManager); BloomFilter bloom = new BloomFilter (); bloom.setBloomIntensity (2f); bloom.setExposurePower (2); bloom.setExposureCutOff (0f); bloom.setBlurScale (1.5f); fpp.addFilter (floraison); guiViewPort.addProcessor (fpp); guiViewPort.setClearColor (true);J'ai configuré le
BloomFilter
un peu; si vous voulez savoir à quoi servent tous ces paramètres, consultez le tutoriel jME sur bloom..
Conclusion
Félicitations pour avoir terminé la deuxième partie. Il reste trois parties à jouer, alors ne vous laissez pas distraire en jouant trop longtemps! La prochaine fois, nous allons ajouter l'interface graphique et les trous noirs.