Faire un vecteur Neon Shooter dans jMonkeyEngine Notions de base

Dans cette série de didacticiels, nous expliquerons comment créer un jeu inspiré de Geometry Wars à l'aide de jMonkeyEngine. JMonkeyEngine ("jME" en abrégé) est un moteur de jeu Java 3D open source. Pour en savoir plus, visitez le site Web correspondant ou lisez notre guide Comment apprendre jMonkeyEngine.

Bien que jMonkeyEngine soit intrinsèquement un moteur de jeu en 3D, il est également possible de créer des jeux en 2D avec celui-ci..

Articles Similaires
Cette série de tutoriels est basée sur la série de Michael Hoffman expliquant comment créer le même jeu sous XNA:
  • Faire un vecteur de tir néon dans XNA

Les cinq chapitres du didacticiel seront consacrés à certains composants du jeu:

  1. Initialiser la scène 2D, charger et afficher des graphiques, gérer les entrées.
  2. Ajoutez des ennemis, des collisions et des effets sonores.
  3. Ajouter l'interface graphique et les trous noirs.
  4. Ajoutez des effets de particules spectaculaires.
  5. Ajouter la grille de fond de déformation.

Petit avant-goût visuel, voici le résultat final de nos efforts:


… Et voici nos résultats après ce premier chapitre:


La musique et les effets sonores que vous pouvez entendre dans ces vidéos ont été créés par RetroModular, et vous pouvez lire comment il l'a fait ici..

Les sprites sont de Jacob Zinman-Jeanes, notre concepteur résident Tuts +. Toutes les illustrations se trouvent dans le fichier source zip de téléchargement.


La police est Nova Square, de Wojciech Kalinowski.

Le tutoriel est conçu pour vous aider à apprendre les bases du moteur jMonkeyEngine et à créer votre premier jeu avec celui-ci. Nous allons tirer parti des fonctionnalités du moteur, mais nous n’utiliserons pas d’outils complexes pour améliorer les performances. Chaque fois qu'il existe un outil plus avancé pour implémenter une fonctionnalité, je vais créer un lien vers les tutoriels jME appropriés, mais je vais m'en tenir à la manière simple du tutoriel lui-même. Si vous étudiez davantage jME, vous pourrez plus tard développer et améliorer votre version de MonkeyBlaster..

Et c'est parti!


Vue d'ensemble

Le premier chapitre comprendra le chargement des images nécessaires, le traitement des entrées et le déplacement et la prise de vue du navire du joueur..

Pour y parvenir, nous aurons besoin de trois classes:

  • MonkeyBlasterMain: Notre classe principale contenant la boucle de jeu et le gameplay de base.
  • PlayerControl: Cette classe déterminera le comportement du joueur.
  • BulletControl: Similaire à ce qui précède, cela définit le comportement de nos puces.

Au cours du tutoriel, nous allons lancer le code de jeu général dans MonkeyBlasterMain et gérer les objets à l'écran principalement par le biais de contrôles et d'autres classes. Les fonctions spéciales, comme le son, auront également leurs propres classes.


Chargement du vaisseau du joueur

Si vous n'avez pas encore téléchargé le SDK jME, il est grand temps! Vous pouvez le trouver sur la page d'accueil de jMonkeyEngine.

Créez un nouveau projet dans le SDK jME. Il générera automatiquement la classe principale, qui ressemblera à celle-ci:

paquet monkeyblaster; import com.jme3.app.SimpleApplication; import com.jme3.renderer.RenderManager; Classe publique MonkeyBlasterMain étend SimpleApplication public static void main (String [] args) Main app = new Main (); app.start ();  @Override public void simpleInitApp ()  @Override public vid simpleUpdate (float tpf)  @Override public vid simpleRender (RenderManager rm) 

Nous allons commencer par annuler simpleInitApp (). Cette méthode est appelée au démarrage de l'application. C'est l'endroit idéal pour configurer tous les composants:

 @Override public void simpleInitApp () // configuration de la caméra pour les jeux 2D cam.setParallelProjection (true); cam.setLocation (nouveau Vector3f (0,0,0.5f)); getFlyByCamera (). setEnabled (false); // désactive l'affichage des statistiques (vous pouvez le laisser si vous le souhaitez) setDisplayStatView (false); setDisplayFps (false); 

Nous devons d’abord ajuster un peu la caméra, car jME est essentiellement un moteur de jeu en 3D. La vue statistiques du deuxième paragraphe peut être très intéressante, mais c’est ainsi que vous la désactivez..

Quand vous démarrez le jeu maintenant, vous pouvez voir… rien.

Eh bien, nous devons charger le joueur dans le jeu! Nous allons créer une petite méthode pour gérer le chargement de nos entités:

 private Spatial getSpatial (Nom de chaîne) Nœud = nouveau nœud (nom); // charge l'image Picture pic = new Picture (name); Texture2D tex = (Texture2D) assetManager.loadTexture ("Textures /" + name + ". Png"); pic.setTexture (assetManager, tex, true); // ajuste l'image float width = tex.getImage (). getWidth (); float height = tex.getImage (). getHeight (); pic.setWidth (width); pic.setHeight (hauteur); pic.move (-width / 2f, -height / 2f, 0); // ajouter un matériau à l'image Material picMat = new Material (assetManager, "Common / MatDefs / Gui / Gui.j3md"); picMat.getAdditionalRenderState (). setBlendMode (BlendMode.AlphaAdditive); node.setMaterial (picMat); // définit le rayon de l'espace // (en utilisant seulement la largeur comme approximation simple) node.setUserData ("radius", width / 2); // attache l'image au nœud et le retourne node.attachChild (pic); noeud de retour; 

Au début, nous créons un noeud qui contiendra notre image.

Pointe: Le graphe de scène jME se compose de spatiales (noeuds, images, géométries, etc.). Chaque fois que vous ajoutez un élément spatial à la guiNode, cela devient visible dans la scène. Nous allons utiliser le guiNode parce que nous créons un jeu en 2D. Vous pouvez attacher des spatiales à d’autres et ainsi organiser votre scène. Pour devenir un véritable maître du graphe de la scène, je vous recommande ce tutoriel sur le graphe de la scène jME.

Après avoir créé le nœud, nous chargeons l'image et appliquons la texture appropriée. Appliquer la bonne taille à la photo est jolie est facile à comprendre, mais pourquoi avons-nous besoin de la déplacer?

Lorsque vous chargez une image dans jME, le centre de rotation n'est pas au centre, mais dans un coin de l'image. Mais nous pouvons déplacer l'image de la moitié de sa largeur vers la gauche et de la moitié de sa hauteur vers le haut et l'ajouter à un autre nœud. Ensuite, lorsque nous faisons pivoter le noeud parent, l'image elle-même pivote autour de son centre.

L'étape suivante consiste à ajouter un matériau à l'image. Un matériau détermine le mode d'affichage de l'image. Dans cet exemple, nous utilisons le matériel d'interface graphique par défaut et définissons le Mode de fusion à AlphaAdditive. Cela signifie que les parties transparentes superposées de plusieurs images seront plus claires. Cela sera utile plus tard pour rendre les explosions plus brillantes.

Enfin, nous ajoutons notre image au noeud et la renvoyons.

Maintenant, nous devons ajouter le joueur à la guiNode. Nous étendrons simpleInitApp un peu plus:

// configure le joueur player = getSpatial ("Player"); player.setUserData ("alive", true); player.move (settings.getWidth () / 2, settings.getHeight () / 2, 0); guiNode.attachChild (lecteur);

En bref: nous chargeons le lecteur, configurons des données, les déplaçons au milieu de l’écran et les attachons au guiNode pour le faire être affiché.

Données d'utilisateur est simplement quelques données que vous pouvez attacher à n'importe quel espace. Dans ce cas, nous ajoutons un booléen et l'appelons vivant, afin que nous puissions regarder si le joueur est en vie. Nous l'utiliserons plus tard.

Maintenant, lancez le programme! Vous devriez pouvoir voir le joueur au milieu. Pour le moment, c'est plutôt ennuyeux, je l'avoue. Alors ajoutons de l'action!


Gestion des entrées et déplacement du lecteur

L'entrée jMonkeyEngine est assez simple une fois que vous l'avez faite une fois. Nous commençons par mettre en place un Action Listener:

Classe publique MonkeyBlasterMain étend SimpleApplication implémente ActionListener 

Maintenant, pour chaque clé, nous allons ajouter le mappage d’entrée et l’auditeur dans simpleInitApp ():

 inputManager.addMapping ("left", nouveau KeyTrigger (KeyInput.KEY_LEFT)); inputManager.addMapping ("right", nouveau KeyTrigger (KeyInput.KEY_RIGHT)); inputManager.addMapping ("up", nouveau KeyTrigger (KeyInput.KEY_UP)); inputManager.addMapping ("down", nouveau KeyTrigger (KeyInput.KEY_DOWN)); inputManager.addMapping ("return", nouveau KeyTrigger (KeyInput.KEY_RETURN)); inputManager.addListener (this, "left"); inputManager.addListener (this, "right"); inputManager.addListener (this, "up"); inputManager.addListener (this, "down"); inputManager.addListener (this, "return");

Chaque fois que l’une ou l’autre de ces touches est pressée ou relâchée, la méthode onAction est appelé. Avant d'entrer dans quoi réellement faire lorsque vous appuyez sur une touche, nous devons ajouter un contrôle à notre lecteur.

Info: Les contrôles représentent certains comportements des objets dans la scène. Par exemple, vous pouvez ajouter un FightControl Et un IdleControl à une IA ennemie. Selon la situation, vous pouvez activer et désactiver ou associer et détacher des contrôles.

Notre PlayerControl se chargera simplement de déplacer le lecteur chaque fois qu'une touche est enfoncée, de la tourner dans la bonne direction et de s'assurer que le lecteur ne quitte pas l'écran.

Voici:

Classe publique PlayerControl étend AbstractControl private int screenWidth, screenHeight; // le joueur est-il en train de bouger? public booléen haut, bas, gauche, droite; // vitesse du joueur private float speed = 800f; // lastRotation du joueur private float lastRotation; public PlayerControl (int width, int height) this.screenWidth = width; this.screenHeight = height;  @Override protected void controlUpdate (float tpf) // déplace le joueur dans une certaine direction // s'il n'est pas hors de l'écran if (up) if (spatial.getLocalTranslation (). Y < screenHeight - (Float)spatial.getUserData("radius"))  spatial.move(0,tpf*speed,0);  spatial.rotate(0,0,-lastRotation + FastMath.PI/2); lastRotation=FastMath.PI/2;  else if (down)  if (spatial.getLocalTranslation().y > (Float) spatial.getUserData ("radius")) spatial.move (0, tpf * -speed, 0);  spatial.rotate (0,0, -lastRotation + FastMath.PI * 1.5f); lastRotation = FastMath.PI * 1.5f;  else if (left) if (spatial.getLocalTranslation (). x> (Float) spatial.getUserData ("radius")) spatial.move (tpf * -speed, 0,0);  spatial.rotate (0,0, -lastRotation + FastMath.PI); lastRotation = FastMath.PI;  else if (right) if (spatial.getLocalTranslation (). x < screenWidth - (Float)spatial.getUserData("radius"))  spatial.move(tpf*speed,0,0);  spatial.rotate(0,0,-lastRotation + 0); lastRotation=0;   @Override protected void controlRender(RenderManager rm, ViewPort vp)  // reset the moving values (i.e. for spawning) public void reset()  up = false; down = false; left = false; right = false;  

D'accord; maintenant, jetons un coup d'oeil au code pièce par pièce.

 private int screenWidth, screenHeight; // le joueur est-il en train de bouger? public booléen haut, bas, gauche, droite; // vitesse du joueur private float speed = 800f; // lastRotation du joueur private float lastRotation; public PlayerControl (int width, int height) this.screenWidth = width; this.screenHeight = height; 

Tout d'abord, nous initialisons certaines variables, en définissant dans quelle direction et à quelle vitesse le joueur se déplace, et dans quelle mesure il est tourné. Ensuite, nous définissons le largeur d'écran et screenHeight, dont nous aurons besoin dans la prochaine grande méthode.

controlUpdate (float tpf) est automatiquement appelé par jME à chaque cycle de mise à jour. La variable tpf indique l'heure depuis la dernière mise à jour. Cela est nécessaire pour contrôler la vitesse: si certains ordinateurs mettent deux fois plus de temps à calculer une mise à jour que d'autres, le lecteur doit alors se déplacer deux fois plus loin en une seule mise à jour sur ces ordinateurs..

Maintenant au premier si déclaration:

 if (up) if (spatial.getLocalTranslation (). y < screenHeight - (Float)spatial.getUserData("radius"))  spatial.move(0,tpf*speed,0); 

Nous vérifions si le joueur est en train de monter et, si c'est le cas, nous vérifions s'il peut continuer à monter. S'il est assez éloigné de la frontière, nous le déplaçons un peu plus haut..

Passons maintenant à la rotation:

 spatial.rotate (0,0, -lastRotation + FastMath.PI / 2); lastRotation = FastMath.PI / 2;

Nous faisons tourner le joueur par lastRotation faire face à sa direction d'origine. Dans cette direction, nous pouvons faire pivoter le joueur dans la direction souhaitée. Enfin, nous enregistrons la rotation réelle.

Nous utilisons le même type de logique pour les quatre directions. le réinitialiser() La méthode est juste ici pour remettre toutes les valeurs à zéro, pour les utiliser lorsque le joueur est réapparu..

Nous avons donc enfin le contrôle de notre joueur. Il est temps de l'ajouter à l'espace réel. Ajoutez simplement la ligne suivante au simpleInitApp () méthode:

player.addControl (nouveau PlayerControl (settings.getWidth (), settings.getHeight ()));

L'object réglages est inclus dans la classe SimpleApplication. Il contient des données sur les paramètres d'affichage du jeu..

Si nous commençons le jeu maintenant, rien ne se passe encore. Nous devons indiquer au programme quoi faire lorsque l’une des touches mappées est enfoncée. Pour ce faire, nous allons remplacer le onAction méthode:

 onAction (nom de chaîne, booléen isPressed, float tpf) if ((booléen) player.getUserData ("alive")) if (name.equals ("up")) player.getControl (PlayerControl.class). up = isPressed;  else if (name.equals ("down")) player.getControl (PlayerControl.class) .down = isPressed;  else if (name.equals ("left")) player.getControl (PlayerControl.class) .left = isPressed;  else if (name.equals ("right")) player.getControl (PlayerControl.class) .right = isPressed; 

Pour chaque touche enfoncée, nous disons au PlayerControl le nouveau statut de la clé. Maintenant, il est enfin temps de commencer notre jeu et de voir quelque chose bouger à l'écran!

Lorsque vous êtes satisfait de comprendre les bases de la gestion des entrées et du comportement, il est temps de refaire la même chose, cette fois pour les puces..


Ajout de l'action Bullet

Si nous voulons avoir des réal l'action en cours, nous devons être en mesure de tirer sur certains ennemis. Nous allons suivre la même procédure de base qu'à l'étape précédente: gérer les entrées, créer des puces et leur ajouter un comportement..

Pour gérer les entrées de la souris, nous allons implémenter un autre écouteur:

Classe publique MonkeyBlasterMain étend SimpleApplication implémente ActionListener, AnalogListener 

Avant que quoi que ce soit ne se produise, nous devons ajouter le mappage et l'auditeur comme nous l'avions fait la dernière fois. Nous ferons cela dans le simpleInitApp () méthode, à côté de l'autre initialisation d'entrée:

 inputManager.addMapping ("mousePick", nouveau MouseButtonTrigger (MouseInput.BUTTON_LEFT)); inputManager.addListener (this, "mousePick");

Chaque fois que nous cliquons avec la souris, la méthode onAnalog se fait appeler. Avant de nous lancer dans le tournage, nous devons implémenter une méthode d'assistance, Vector3f getAimDirection (), ce qui nous donnera la direction dans laquelle tirer, en soustrayant la position du joueur de celle de la souris:

 private Vector3f getAimDirection () Vector2f mouse = inputManager.getCursorPosition (); Vector3f playerPos = player.getLocalTranslation (); Vector3f dif = new Vector3f (mouse.x-playerPos.x, mouse.y-playerPos.y, 0); retourne dif.normalizeLocal (); 
Pointe: Lorsque vous attachez des objets à la guiNode, leurs unités de traduction locales sont égales à un pixel. Cela nous permet de calculer facilement la direction, car la position du curseur est également spécifiée en unités de pixels..

Maintenant que nous avons une direction à tirer, implémentons le tir réel:

 public void onAnalog (nom de chaîne, valeur float, float tpf) if ((booléen) player.getUserData ("alive")) if (name.equals ("mousePick")) // tire Bullet if (System.currentTimeMillis () - bulletCooldown> 83f) bulletCooldown = System.currentTimeMillis (); Vector3f objectif = getAimDirection (); Vector3f offset = nouveau Vector3f (aim.y / 3, -aim.x / 3,0); // init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (nouveau BulletControl (objectif, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (nouveau BulletControl (objectif, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2); 

Bon, passons par ceci:

 if (System.currentTimeMillis () - bulletCooldown> 83f) bulletCooldown = System.currentTimeMillis (); Vector3f objectif = getAimDirection (); Vector3f offset = nouveau Vector3f (aim.y / 3, -aim.x / 3,0);

Si le joueur est en vie et que le bouton de la souris est cliqué, notre code vérifie d’abord si le dernier coup a été tiré il ya au moins 83 ms (bulletColde est une variable longue que nous initialisons au début de la classe). Si c'est le cas, nous sommes autorisés à tirer et nous calculons la bonne direction pour viser et le décalage.

// init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (nouveau BulletControl (objectif, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (nouveau BulletControl (objectif, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);

Nous voulons créer deux balles côte à côte, il faudra donc ajouter un petit décalage à chacune d’elles. Un décalage approprié est orthogonal à la direction visée, ce qui est facilement obtenu en commutant le X et y valeurs et négation de celui-ci. Le second sera simplement une négation du premier.

// init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (nouveau BulletControl (objectif, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (nouveau BulletControl (objectif, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);

Le reste devrait sembler assez familier: nous initialisons la balle en utilisant notre propre getSpatial méthode depuis le début. Ensuite, nous le traduisons au bon endroit et le fixons au nœud. Mais attendez, quel noeud?

Nous allons organiser nos entités dans des nœuds spécifiques, il est donc logique de créer un nœud auquel nous pourrons attacher toutes nos puces. Pour afficher les enfants de ce noeud, nous devrons l’attacher au guiNode.

L'initialisation en simpleInitApp () est assez simple:

// configuration du bulletNode bulletNode = new Node ("ballets"); guiNode.attachChild (bulletNode);

Si vous commencez le jeu, vous pourrez voir les balles apparaître, mais elles ne bougent pas! Si vous voulez vous tester, arrêtez votre lecture et réfléchissez vous-même à ce que nous devons faire pour les faire bouger..

Avez-vous compris?

Nous devons ajouter un contrôle à chaque balle qui prendra en charge son mouvement. Pour ce faire, nous allons créer une autre classe appelée BulletControl:

Classe publique BulletControl étend AbstractControl private int screenWidth, screenHeight; vitesse de flottement privée = 1100f; direction publique de Vector3f; rotation des flotteurs privés; BulletControl public (direction Vector3f, int screenWidth, int screenHeight) this.direction = direction; this.screenWidth = screenWidth; this.screenHeight = screenHeight;  @Override protected void controlUpdate (float tpf) // mouvement spatial.move (direction.mult (speed * tpf)); // rotation float actualRotation = MonkeyBlasterMain.getAngleFromVector (direction); if (actualRotation! = rotation) spatial.rotate (0,0, actualRotation - rotation); rotation = actualRotation;  // vérifier les limites Vector3f loc = spatial.getLocalTranslation (); if (loc.x> screenWidth || loc.y> screenHeight || loc.x < 0 || loc.y < 0)  spatial.removeFromParent();   @Override protected void controlRender(RenderManager rm, ViewPort vp)  

Un rapide coup d’œil sur la structure de la classe montre qu’elle est assez similaire à la PlayerControl classe. La principale différence est que nous n’avons aucune clé à vérifier, et nous avons un direction variable. Nous déplaçons simplement la balle dans sa direction et la faisons pivoter en conséquence.

 Vector3f loc = spatial.getLocalTranslation (); if (loc.x> screenWidth || loc.y> screenHeight || loc.x < 0 || loc.y < 0)  spatial.removeFromParent(); 

Dans le dernier bloc, nous vérifions si la puce se situe en dehors des limites de l'écran et, si c'est le cas, nous la supprimons de son nœud parent, ce qui supprimera l'objet..

Vous avez peut-être attrapé cet appel de méthode:

MonkeyBlasterMain.getAngleFromVector (direction);

Il fait référence à une méthode d'assistance mathématique statique courte dans la classe principale. J'ai créé deux d'entre eux, l'un convertissant un angle dans un vecteur dans un espace 2D et l'autre convertissant ces vecteurs en une valeur d'angle.

 getAngleFromVector (Vector3f vec) Vector2f vec2 = new Vector2f (vec.x, vec.y); retourne vec2.getAngle ();  public statique Vector3f getVectorFromAngle (angle de flottaison) renvoie le nouveau vecteur Vector3f (FastMath.cos (angle), FastMath.sin (angle), 0); 
Pointe: Si toutes ces opérations vectorielles vous déroutent, alors faites-vous une faveur et explorez quelques tutoriels sur les mathématiques vectorielles. C'est essentiel dans les espaces 2D et 3D. Pendant que vous y êtes, vous devriez également rechercher la différence entre les degrés et les radians. Et si vous souhaitez vous initier davantage à la programmation de jeux 3D, les quaternions sont également géniaux…

Revenons maintenant à l’aperçu principal: nous avons créé un écouteur d’entrée, initialisé deux puces et créé un BulletControl classe. Il ne reste plus qu'à ajouter un BulletControl à chaque balle lors de son initialisation:

bullet.addControl (nouveau BulletControl (objectif, settings.getWidth (), settings.getHeight ()));

Maintenant, le jeu est beaucoup plus amusant!



Conclusion

Même s’il n’est pas vraiment difficile de voler et de tirer des balles, vous pouvez au moins faire quelque chose. Mais ne désespérez pas. Après le prochain tutoriel, vous aurez du mal à vous échapper des hordes d'ennemis grandissants.!