Erstellen Sie in jMonkeyEngine einen Neon-Vektor-Shooter Gegner und Sounds

Im ersten Teil dieser Serie zum Erstellen eines von Geometry Wars inspirierten Spiels in jMonkeyEngine haben wir das Schiff des Spielers implementiert und lassen es sich bewegen und schießen. Diesmal fügen wir die Feinde und die Soundeffekte hinzu.


Überblick

Wir arbeiten in der gesamten Serie daran:


… Und das haben wir am Ende dieses Teils:


Wir benötigen einige neue Klassen, um die neuen Funktionen zu implementieren:

  • SeekerControl: Dies ist eine Verhaltensklasse für den suchenden Feind.
  • WandererControl: Dies ist auch eine Verhaltensklasse, diesmal für den Wandererfeind.
  • Klingen: Wir erledigen das Laden und Abspielen von Soundeffekten und Musik damit.

Wie Sie vielleicht schon erraten haben, fügen wir zwei Arten von Feinden hinzu. Der erste heißt a Sucher; Der Spieler wird aktiv verfolgt, bis er stirbt. Der andere, der Wanderer, streift einfach in einem zufälligen Muster um den Bildschirm.


Feinde hinzufügen

Wir werden die Feinde an zufälligen Positionen auf dem Bildschirm erscheinen lassen. Um dem Spieler etwas Zeit zum Reagieren zu geben, ist der Feind nicht sofort aktiv, sondern wird langsam eingeblendet. Nachdem es vollständig eingeblendet ist, wird es sich durch die Welt bewegen. Wenn es mit dem Spieler kollidiert, stirbt der Spieler; Wenn es mit einer Kugel kollidiert, stirbt es selbst.

Feinde laichen

Zunächst müssen wir einige neue Variablen in der erstellen MonkeyBlasterMain Klasse:

 private long enemySpawnCooldown; privater Float enemySpawnChance = 80; privater Knoten FeindKnoten;

Wir werden die ersten beiden bald verwenden. Vorher müssen wir das initialisieren FeindNode im simpleInitApp ():

 // Einrichten des FeindNode FeindNode = Neuer Knoten ("Feinde"); guiNode.attachChild (enemyNode);

Okay, jetzt zum eigentlichen Laichcode: Wir überschreiben simpleUpdate (Float-Tpf). Diese Methode wird immer wieder von der Engine aufgerufen und ruft die Spawnfunktion des Feindes einfach so lange auf, wie der Spieler lebt. (Wir haben bereits die Userdaten gesetzt am Leben zu wahr im letzten Tutorial.)

 @Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("alive")) spawnEnemies (); 

Und so spawnen wir tatsächlich die Feinde:

 private void spawnEnemies () if (System.currentTimeMillis () - enemySpawnCooldown> = 17) enemySpawnCooldown = System.currentTimeMillis (); if (enemyNode.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) enemySpawnChance - = 0.005f; 

Lass dich nicht von der verwirren enemySpawnCooldown Variable. Es ist nicht dazu da, Gegner mit einer anständigen Frequenz zum Laichen zu bringen - 17 ms wären viel zu kurz für ein Intervall.

enemySpawnCooldown ist tatsächlich da, um sicherzustellen, dass die Anzahl neuer Gegner auf jeder Maschine gleich ist. Auf schnelleren Computern, simpleUpdate (Float-Tpf) wird viel öfter aufgerufen als bei langsameren. Mit dieser Variable prüfen wir etwa alle 17ms, ob wir neue Feinde erzeugen sollen.
Aber wollen wir sie alle 17ms laichen? Wir möchten, dass sie in zufälligen Abständen laichen ob Aussage:

 if (new Random (). nextInt ((int) enemySpawnChance) == 0) 

Je kleiner der Wert von enemySpawnChance, Je wahrscheinlicher es ist, dass in diesem Intervall von 17 ms ein neuer Feind erscheint, desto mehr Feinde muss der Spieler behandeln. Deshalb ziehen wir ein wenig davon ab enemySpawnChance Bei jedem Tick: Das Spiel wird mit der Zeit schwieriger.

Das Erstellen von Suchern und Wanderern ähnelt dem Erstellen anderer Objekte:

 private void createSeeker () Spatial seeker = getSpatial ("Seeker"); seeker.setLocalTranslation (getSpawnPosition ()); seeker.addControl (neues SeekerControl (Spieler)); seeker.setUserData ("aktiv", false); enemyNode.attachChild (Sucher);  private void createWanderer () Spatial Wanderer = getSpatial ("Wanderer"); wanderer.setLocalTranslation (getSpawnPosition ()); wanderer.addControl (new WandererControl ()); wanderer.setUserData ("active", false); enemyNode.attachChild (Wanderer); 

Wir erstellen das Räumliche, verschieben es, fügen ein benutzerdefiniertes Steuerelement hinzu, setzen es als nicht aktiv und fügen es unserem hinzu FeindNode. Was? Warum nicht aktiv? Das liegt daran, dass wir nicht wollen, dass der Feind den Spieler jagt, sobald er erscheint. Wir möchten dem Spieler etwas Zeit geben, um zu reagieren.

Bevor wir in die Kontrollen einsteigen, müssen wir die Methode implementieren getSpawnPosition (). Der Feind sollte zufällig spawnen, aber nicht direkt neben dem Spieler:

 private 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; 

Wir berechnen eine neue zufällige Position pos. Wenn es zu nahe am Spieler ist, berechnen wir eine neue Position und wiederholen es, bis es eine anständige Entfernung ist.

Jetzt müssen wir nur noch die Feinde aktivieren und losfahren. Das machen wir in ihren Kontrollen.

Verhalten des Feindes kontrollieren

Wir werden uns mit dem beschäftigen SeekerControl zuerst:

 öffentliche Klasse SeekerControl erweitert AbstractControl private Spatial Player; private Vector3f-Geschwindigkeit; private long spawnTime; public SeekerControl (Spatial Player) this.player = Spieler; Geschwindigkeit = neuer Vector3f (0,0,0); spawnTime = System.currentTimeMillis ();  @Override protected void controlUpdate (float tpf) if ((Boolean) spac.getUserData ("aktiv")) // übersetzt den Sucher Vector3f playerDirection = player.getLocalTranslation (). playerDirection.normalizeLocal (); playerDirection.multLocal (1000f); Velocity.addLocal (Spielerrichtung); Velocity.MultLocal (0,8f); räumliche Bewegung (Geschwindigkeit.mult (tpf * 0,1f)); // Den Sucher drehen, wenn (Geschwindigkeit! = Vector3f.ZERO) räumlich.rotateUpTo (Geschwindigkeit.normalize ()); räumlich.rotieren (0,0, FastMath.PI / 2f);  else // behandelt den "aktiven" -status long dif = System.currentTimeMillis () - spawnTime; if (dif> = 1000f) spaces.setUserData ("active", true);  ColorRGBA color = new ColorRGBA (1,1,1, dif / 1000f); Node räumlichNode = (Node) räumlich; Bild pic = (Picture) spatelNode.getChild ("Seeker"); pic.getMaterial (). setColor ("Farbe", Farbe);  @Override protected void controlRender (RenderManager RM, ViewPort vp) 

Konzentrieren wir uns auf controlUpdate (float tpf):

Zuerst müssen wir überprüfen, ob der Feind aktiv ist. Wenn nicht, müssen wir es langsam einblenden.
Wir prüfen dann die Zeit, die seit dem Erscheinen des Feindes vergangen ist, und wenn es lang genug ist, setzen wir es aktiv.

Unabhängig davon, ob wir es gerade aktiviert haben, müssen wir seine Farbe anpassen. Die lokale Variable räumlich enthält den Raum, an den das Steuerelement angehängt wurde, aber Sie erinnern sich vielleicht, dass wir das Steuerelement nicht an das eigentliche Bild angehängt haben. Das Bild ist ein untergeordnetes Element des Knotens, an den das Steuerelement angehängt ist. (Wenn Sie nicht wissen, wovon ich rede, werfen Sie einen Blick auf die Methode getSpatial (Stringname) wir haben letztes Tutorial implementiert.)

So; wir bekommen das bild als kind von räumlich, Holen Sie sich das Material und setzen Sie die Farbe auf den entsprechenden Wert. Nichts Besonderes, wenn Sie sich an die Flächen, Materialien und Knoten gewöhnt haben.

Info: Sie fragen sich vielleicht, warum wir die Materialfarbe auf Weiß setzen. (Die RGB-Werte sind alle 1 in unserem Code). Wollen wir nicht einen gelben und einen roten Feind??
Dies liegt daran, dass das Material die Materialfarbe mit den Texturfarben mischt. Wenn wir also die Textur des Gegners so anzeigen möchten, wie sie ist, müssen wir sie mit Weiß mischen.

Nun müssen wir uns ansehen, was wir tun, wenn der Feind aktiv ist. Dieses Steuerelement wird benannt SeekerControl Aus einem bestimmten Grund: Wir möchten, dass Feinde mit dieser Kontrolle dem Spieler folgen.

Um dies zu erreichen, berechnen wir die Richtung vom Sucher zum Spieler und addieren diesen Wert zur Geschwindigkeit. Danach verringern wir die Geschwindigkeit um 80%, damit sie nicht unendlich wachsen kann, und bewegen den Sucher entsprechend.

Die Rotation ist nichts Besonderes: Wenn der Sucher nicht stillsteht, drehen wir ihn in Richtung Spieler. Wir drehen es dann ein wenig mehr, weil der Sucher in Sucher.png zeigt nicht nach oben, sondern nach rechts.

Info: Das rotateUpTo (Vector3f-Richtung) Methode von Räumlich Dreht ein räumliches Objekt so, dass seine y-Achse in die angegebene Richtung zeigt.

Das war also der erste Feind. Der Code des zweiten Feindes, des Wanderers, ist nicht viel anders:

 public class WandererControl erweitert AbstractControl private int screenWidth, screenHeight; private Vector3f-Geschwindigkeit; private float directionAngle; private long spawnTime; public WandererControl (int screenWidth, int screenHeight) this.screenWidth = screenWidth; this.screenHeight = screenHeight; Geschwindigkeit = new Vector3f (); directionAngle = new Random (). nextFloat () * FastMath.PI * 2f; spawnTime = System.currentTimeMillis ();  @Override protected void controlUpdate (float tpf) if ((Boolean) spac.getUserData ("active")) // den Wanderer übersetzen // die Richtung ändernAngle ein bisschen directionAngle + = (new Zufall () * 20f - 10f) * tpf; System.out.println (directionAngle); Vector3f directionVector = MonkeyBlasterMain.getVectorFromAngle (directionAngle); directionVector.multLocal (1000f); Velocity.addLocal (directionVector); // Verringere die Geschwindigkeit etwas und bewege den Wanderer Velocity.multLocal (0.8f); räumliche Bewegung (Geschwindigkeit.mult (tpf * 0,1f)); // den Wanderer von den Bildschirmrändern abprallen lassen Vector3f loc = spac.getLocalTranslation (); if (loc.x screenWidth || loc.y> screenHeight) Vector3f newDirectionVector = new Vector3f (screenWidth / 2, screenHeight / 2,0) .subtract (loc); directionAngle = MonkeyBlasterMain.getAngleFromVector (newDirectionVector);  // den Wanderer räumlich drehen (0,0, tpf * 2);  else // behandelt den "aktiven" -status long dif = System.currentTimeMillis () - spawnTime; if (dif> = 1000f) spaces.setUserData ("active", true);  ColorRGBA color = new ColorRGBA (1,1,1, dif / 1000f); Node räumlichNode = (Node) räumlich; Bild pic = (Picture) spatelNode.getChild ("Wanderer"); pic.getMaterial (). setColor ("Farbe", Farbe);  @Override protected void controlRender (RenderManager RM, ViewPort vp) 

Das einfache Zeug zuerst: Das Einblenden des Feindes ist das gleiche wie bei der Suchersteuerung. Im Konstruktor wählen wir eine zufällige Richtung für den Wanderer, in die er fliegt, sobald er aktiviert ist.

Spitze: Wenn Sie mehr als zwei Gegner haben oder das Spiel einfach sauberer strukturieren möchten, können Sie ein drittes Steuerelement hinzufügen: EnemyControl Es würde alles bewältigen, was alle Feinde gemeinsam hatten: den Feind bewegen, ihn einblenden, aktiv machen ...

Nun zu den wichtigsten Unterschieden:

Wenn der Feind aktiv ist, ändern wir zuerst die Richtung ein wenig, so dass der Wanderer sich nicht immer in einer geraden Linie bewegt. Wir tun dies, indem wir unsere ändern directionAngle ein bisschen und das hinzufügen directionVector zum Geschwindigkeit. Wir wenden dann die Geschwindigkeit genau so an wie im SeekerControl.

Wir müssen prüfen, ob sich der Wanderer außerhalb der Bildschirmgrenzen befindet, und wenn ja, ändern wir die directionAngle in eine passendere Richtung, damit es im nächsten Update angewendet wird.

Schließlich drehen wir den Wanderer ein bisschen. Dies ist nur, weil ein sich drehender Feind cooler aussieht.

Nachdem wir nun beide Feinde implementiert haben, können Sie das Spiel starten und ein bisschen spielen. Sie erhalten einen kleinen Einblick in die Spielweise, auch wenn Sie die Feinde nicht töten können und auch nicht. Lass uns das als nächstes hinzufügen.

Kollisionserkennung

Damit der Gegner von Feinden getötet werden kann, müssen wir wissen, ob er kollidiert. Dazu fügen wir eine neue Methode hinzu, handleCollisions, hereingerufen simpleUpdate (Float-Tpf):

 @Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("alive")) spawnEnemies (); handleCollisions (); 

Und jetzt die eigentliche Methode:

 private void handleCollisions () // sollte der Spieler sterben? für (int i = 0; i 

Wir durchlaufen alle Feinde, indem wir die Anzahl der Kinder des Knotens festlegen und dann jedes einzelne davon erhalten. Außerdem müssen wir nur prüfen, ob der Gegner den Spieler tötet, wenn der Gegner tatsächlich aktiv ist. Wenn nicht, brauchen wir uns nicht darum zu kümmern. Wenn er aktiv ist, prüfen wir, ob der Spieler und der Feind zusammenstoßen. Wir machen das auf andere Weise, checkCollisoin (Spatial a, Spatial b):

 private boolean checkCollision (Spatial a, Spatial b) float distance = a.getLocalTranslation (). distance (b.getLocalTranslation ()); float maxDistance = (Float) a.getUserData ("radius") + (Float) b.getUserData ("radius"); Rückweg <= maxDistance; 

Das Konzept ist ziemlich einfach: Zuerst berechnen wir den Abstand zwischen den beiden Flächen. Als Nächstes müssen wir wissen, wie nahe die beiden Spatials sein müssen, um als kollidiert betrachtet zu werden. Daher erhalten wir den Radius jedes räumlichen Bereichs und fügen diesen hinzu. (Wir setzen die Benutzerdaten in "Radius") getSpatial (Stringname) im vorherigen Tutorial.) Wenn also die tatsächliche Entfernung kleiner oder gleich dieser maximalen Entfernung ist, kehrt die Methode zurück wahr, was bedeutet, dass sie kollidierten.

Was jetzt? Wir müssen den Spieler töten. Erstellen wir eine andere Methode:

 private void killPlayer () player.removeFromParent (); player.getControl (PlayerControl.class) .reset (); player.setUserData ("lebendig", false); player.setUserData ("dieTime", System.currentTimeMillis ()); enemyNode.detachAllChildren (); 

Zuerst trennen wir den Player vom übergeordneten Knoten, der ihn automatisch aus der Szene entfernt. Als nächstes müssen wir die Bewegung in zurücksetzen PlayerControl-Andernfalls könnte sich der Spieler noch bewegen, wenn er erneut erscheint.

Dann setzen wir die Userdaten am Leben zu falsch und legen Sie eine neue Benutzerdaten an dieTime. (Wir werden das brauchen, um den Spieler neu zu sehen, wenn er tot ist.)

Schließlich lösen wir alle Feinde ab, da es dem Spieler schwer fällt, die bereits vorhandenen Feinde zu bekämpfen, wenn er erscheint.

Wir haben bereits das Respawning erwähnt, also machen wir das nächste. Wir werden das noch einmal ändern simpleUpdate (Float-Tpf) Methode:

 @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 (Spieler); player.setUserData ("lebendig", wahr); 

Wenn der Spieler also nicht lebt und lange genug tot war, setzen wir seine Position in die Mitte des Bildschirms, fügen ihn der Szene hinzu und setzen schließlich seine Benutzerdaten am Leben zu wahr nochmal!

Jetzt ist es an der Zeit, das Spiel zu starten und unsere neuen Funktionen zu testen. Sie haben jedoch eine harte Zeit, die länger als zwanzig Sekunden dauert, weil Ihre Waffe wertlos ist. Lassen Sie uns etwas dagegen unternehmen.

Damit Kugeln Kugeln töten können, fügen wir dem Code einen Code hinzu handleCollisions () Methode:

 // soll ein Feind sterben? int i = 0; während ich < 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++; 

Das Verfahren zum Töten von Feinden ist ziemlich dasselbe wie beim Töten des Spielers; Wir durchlaufen alle Feinde und alle Kugeln, prüfen, ob sie kollidieren, und wenn ja, trennen wir beide.

Jetzt Führe das Spiel aus und schau, wie weit du kommst!

Info: Durch jeden Feind zu iterieren und seine Position mit der Position jeder Kugel zu vergleichen, ist eine sehr schlechte Möglichkeit, auf Kollisionen zu prüfen. Es ist in diesem Beispiel der Einfachheit halber okay, aber in einem echt Um das zu erreichen, müssten Sie bessere Algorithmen implementieren, wie die Quadtree-Kollisionserkennung. Glücklicherweise verwendet die jMonkeyEngine die Bullet-Physik-Engine. Wenn Sie also komplizierte 3D-Physik haben, müssen Sie sich keine Gedanken darüber machen.

Nun sind wir mit dem Hauptspiel fertig. Wir werden immer noch schwarze Löcher implementieren und die Punktzahl und das Leben des Spielers anzeigen. Um das Spiel unterhaltsamer und aufregender zu gestalten, fügen wir Soundeffekte und bessere Grafiken hinzu. Letzteres wird durch den Bloom-Nachverarbeitungsfilter, einige Partikeleffekte und einen kühlen Hintergrundeffekt erreicht.

Bevor wir diesen Teil der Serie als abgeschlossen betrachten, werden wir etwas Audio und den Bloom-Effekt hinzufügen.


Sounds und Musik abspielen

Um ein wenig Audio in unser Spiel aufzunehmen, erstellen wir eine neue Klasse, die einfach aufgerufen wird Klingen:

 öffentliche Klasse Sound private AudioNode-Musik; private AudioNode [] Aufnahmen; private AudioNode [] Explosionen; private AudioNode [] spawns; privater AssetManager AssetManager; Öffentlicher Ton (AssetManager assetManager) this.assetManager = assetManager; Aufnahmen = neuer AudioNode [4]; Explosionen = neuer AudioNode [8]; Spawns = neuer AudioNode [8]; loadSounds ();  private void loadSounds () music = new AudioNode (assetManager, "Sounds / Music.ogg"); music.setPositional (false); music.setReverbEnabled (false); music.setLooping (true); für (int i = 0; i 

Hier beginnen wir mit dem Einrichten des notwendigen AudioNode Variablen und initialisieren Sie die Arrays.

Als Nächstes laden wir die Sounds, und für jeden Sound machen wir so ziemlich dasselbe. Wir schaffen ein neues AudioNode, mit der Hilfe des Vermögensverwalter. Dann setzen wir ihn nicht auf Position und deaktivieren den Nachhall. (Wir müssen den Sound nicht positionieren, da wir in unserem 2D-Spiel keine Stereo-Ausgabe haben, obwohl Sie ihn auch implementieren könnten, wenn Sie möchten.) Durch Deaktivieren des Nachhalls wird der Sound genauso gespielt wie im eigentlichen Audio Datei; Wenn wir es aktivieren, könnten wir jME das Audiomaterial wie in einer Höhle oder einem Dungeon klingen lassen. Danach setzen wir die Schleife auf wahr für die Musik und zu falsch für jeden anderen Ton.

Das Spielen der Sounds ist ziemlich einfach: Wir rufen einfach an soundX.play ().

Info: Wenn du einfach anrufst abspielen() Bei einigen Sounds spielt es nur den Sound. Aber manchmal möchten wir denselben Sound zweimal oder sogar mehrmals gleichzeitig spielen. Das ist, was playInstance () ist da für: es erstellt für jeden Sound eine neue Instanz, sodass wir denselben Sound mehrmals gleichzeitig spielen können.

Ich überlasse Ihnen den Rest der Arbeit: Sie müssen anrufen startMusic, schießen(), Explosion() (für sterbende Feinde) und laichen() an den entsprechenden Stellen in unserer Hauptklasse MonkeyBlasterMain ().

Wenn Sie fertig sind, werden Sie feststellen, dass das Spiel jetzt viel mehr Spaß macht. Diese wenigen Soundeffekte tragen wirklich zur Atmosphäre bei. Aber lassen Sie uns auch die Grafiken ein wenig polieren.


Bloom-Nachbearbeitungsfilter hinzufügen

Die Aktivierung von bloom ist in der jMonkeyEngine sehr einfach, da alle erforderlichen Codes und Shader bereits für Sie implementiert sind. Fahren Sie einfach fort und fügen Sie diese Zeilen ein simpleInitApp ():

 FilterPostProcessor fpp = neuer FilterPostProcessor (assetManager); BloomFilter Bloom = neuer BloomFilter (); bloom.setBloomIntensity (2f); bloom.setExposurePower (2); bloom.setExposureCutOff (0f); bloom.setBlurScale (1.5f); fpp.addFilter (Blüte); guiViewPort.addProcessor (fpp); guiViewPort.setClearColor (true);

Ich habe die konfiguriert BloomFilter ein bisschen; Wenn Sie wissen möchten, wofür all diese Einstellungen gelten, sollten Sie das JME-Tutorial zu Bloom lesen.


Fazit

Herzlichen Glückwunsch zum Abschluss des zweiten Teils. Es gibt noch drei weitere Teile, also lassen Sie sich nicht zu lange ablenken! Nächstes Mal fügen wir die GUI und die schwarzen Löcher hinzu.