Erstellen Sie einen Neon-Vektor-Shooter mit jME HUD und schwarzen Löchern

Bisher haben wir in dieser Serie zum Erstellen eines von Geometry Wars inspirierten Spiels in jMonkeyEngine den Großteil des Gameplays und des Audiomaterials implementiert. In diesem Teil beenden wir das Gameplay durch Hinzufügen von schwarzen Löchern und fügen eine Benutzeroberfläche hinzu, um die Punktzahl der Spieler anzuzeigen.


Überblick

Wir arbeiten in der gesamten Serie daran:


… Und das haben wir am Ende dieses Teils:


Wir ändern nicht nur vorhandene Klassen, sondern fügen zwei neue hinzu:

  • BlackHoleControl: Es versteht sich von selbst, dass dies das Verhalten unserer Schwarzen Löcher regeln wird.
  • Hud: Hier speichern und zeigen wir die Punkte der Spieler, ihre Leben und andere Elemente der Benutzeroberfläche an.

Beginnen wir mit den schwarzen Löchern.


Schwarze Löcher

Das Schwarze Loch ist einer der interessantesten Gegner in Geometry Wars. In MonkeyBlaster, unserem Klon, ist es besonders cool, wenn wir in den nächsten beiden Kapiteln Partikeleffekte und das Warping-Raster hinzufügen.

Grundlegende Funktionalität

Die schwarzen Löcher ziehen das Schiff des Spielers, Feinde in der Nähe und (nach dem nächsten Lernprogramm) Partikel an, stoßen aber Kugeln ab.

Es gibt viele mögliche Funktionen, die wir zum Anziehen oder Abstoßen verwenden können. Am einfachsten ist es, eine konstante Kraft anzuwenden, so dass das Schwarze Loch unabhängig von der Entfernung des Objekts mit der gleichen Stärke zieht. Eine andere Möglichkeit besteht darin, die Kraft von Objekten in einem maximalen Abstand linear bis zur vollen Stärke für Objekte erhöhen zu lassen, die sich direkt auf dem Schwarzen Loch befinden. Und wenn wir die Schwerkraft realistischer modellieren möchten, können wir das umgekehrte Quadrat der Entfernung verwenden, dh die Schwerkraft ist proportional zu 1 / (Abstand * Abstand).

Wir werden tatsächlich jede dieser drei Funktionen verwenden, um verschiedene Objekte zu behandeln. Die Kugeln werden mit einer konstanten Kraft abgestoßen, die Feinde und das Schiff des Spielers werden mit einer linearen Kraft angezogen und die Partikel verwenden eine umgekehrte quadratische Funktion.

Implementierung

Wir werden damit beginnen, unsere schwarzen Löcher zu spawnen. Um dies zu erreichen, benötigen wir ein anderes Varibale in MonkeyBlasterMain:

 private long spawnCooldownBlackHole;

Als nächstes müssen wir einen Knoten für die schwarzen Löcher deklarieren. Lass es uns nennen blackHoleNode. Sie können es so deklarieren und initialisieren, wie wir es getan haben FeindNode im vorherigen Tutorial.

Wir erstellen auch eine neue Methode, spawnSchwarzlöcher, was wir gleich anrufen spawnEnemies im simpleUpdate (Float-Tpf). Das tatsächliche Laichen ist dem Laichen von Feinden ziemlich ähnlich:

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

Das Erstellen des Schwarzen Lochs folgt ebenfalls unserem Standardverfahren:

 private void createBlackHole () Spatial blackHole = getSpatial ("Schwarzes Loch"); blackHole.setLocalTranslation (getSpawnPosition ()); blackHole.addControl (neue BlackHoleControl ()); blackHole.setUserData ("active", false); blackHoleNode.attachChild (blackHole); 

Wieder laden wir das räumliche Element, setzen seine Position, fügen ein Steuerelement hinzu, setzen es auf "nicht aktiv" und hängen es schließlich an den entsprechenden Knoten an. Wenn Sie einen Blick darauf werfen BlackHoleControl, Sie werden feststellen, dass es auch nicht viel anders ist.

Wir werden die Anziehung und die Abstoßung später umsetzen MonkeyBlasterMain, Aber es gibt eine Sache, die wir jetzt ansprechen müssen. Da das Schwarze Loch ein starker Feind ist, möchten wir nicht, dass es leicht fällt. Deshalb fügen wir eine Variable hinzu, Trefferpunkte, zum BlackHoleControl, und setzen Sie ihren Anfangswert auf 10 so dass es nach zehn Treffern stirbt.

 public class BlackHoleControl erweitert AbstractControl private long spawnTime; private int-Trefferpunkte; public BlackHoleControl () spawnTime = System.currentTimeMillis (); Trefferpunkte = 10;  @Override protected void controlUpdate (float tpf) if ((Boolean) spac.getUserData ("active")) // Wir werden diese Stelle später verwenden ... else // behandelt den "active" -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; Picture pic = (Picture) spatelNode.getChild ("schwarzes Loch"); pic.getMaterial (). setColor ("Farbe", Farbe);  @Override protected void controlRender (RenderManager rm, ViewPort vp)  public void wasShot () hitpoints--;  public boolean isDead () Trefferpunkte zurückgeben <= 0;  

Der grundlegende Code für die schwarzen Löcher ist fast fertig. Bevor wir die Schwerkraft umsetzen können, müssen wir uns um die Kollisionen kümmern.

Wenn der Spieler oder ein Feind dem schwarzen Loch zu nahe kommt, stirbt er. Aber wenn eine Kugel es trifft, verliert das Schwarze Loch einen Trefferpunkt.

Schauen Sie sich den folgenden Code an. Es gehört handleCollisions (). Es ist im Grunde dasselbe wie bei allen anderen Kollisionen:

 // kollidiert etwas mit einem schwarzen Loch? für (i = 0; i 

Nun, Sie können das Schwarze Loch jetzt töten, aber das ist nicht der einzige Zeitpunkt, zu dem es verschwinden sollte. Immer wenn der Spieler stirbt, verschwinden alle Feinde und das Schwarze Loch sollte auch. Fügen Sie dazu einfach die folgende Zeile hinzu killPlayer () Methode:

 blackHoleNode.detachAllChildren ();

Jetzt ist es an der Zeit, das coole Zeug umzusetzen. Wir erstellen eine andere Methode, handleGravity (Float-Tpf). Rufen Sie es einfach mit den anderen Methoden in auf simplueUpdate (Float-Tpf).

Bei dieser Methode überprüfen wir alle Entitäten (Spieler, Kugeln und Feinde), um zu sehen, ob sie sich in der Nähe eines schwarzen Lochs befinden, sagen wir etwa 250 Pixel, und wenn dies der Fall ist, wenden wir den entsprechenden Effekt an:

 private void handleGravity (float tpf) für (int i = 0; i 

Um zu prüfen, ob sich zwei Entitäten in einem bestimmten Abstand voneinander befinden, erstellen wir eine aufgerufene Methode ist in der Nähe() die die Standorte der beiden Spatials vergleicht:

 private boolean isNearby (Spatial a, Spatial b, Floatdistanz) Vector3f pos1 = a.getLocalTranslation (); Vector3f pos2 = b.getLocalTranslation (); return pos1.distanceSquared (pos2) <= distance * distance; 

Nachdem wir nun jede Entität überprüft haben, können wir, wenn sie aktiv ist und sich innerhalb der angegebenen Entfernung eines Schwarzen Lochs befindet, endlich die Wirkung der Schwerkraft anwenden. Dazu verwenden wir die Steuerelemente: Wir erstellen in jedem Steuerelement eine Methode, die aufgerufen wird applyGravity (Vector3f Schwerkraft).

Werfen wir einen Blick auf jeden von ihnen:

PlayerControl:

 public void applyGravitation (Vector3f Schwerkraft) räumlich.bewegen (Schwerkraft); 

BulletControl:

 public void applyGravitation (Vector3f Schwerkraft) direction.addLocal (Schwerkraft); 

SeekerControl und WandererControl:

 public void applyGravitation (Vector3f Schwerkraft) Velocity.addLocal (Schwerkraft); 

Und nun zurück zur Hauptklasse, MonkeyBlasterMain. Ich gebe dir zuerst die Methode und erkläre die Schritte darunter:

 private void applyGravity (Spatial blackHole, Spatial target, float tpf) Vector3f difference = blackHole.getLocalTranslation (). subtract (target.getLocalTranslation ()); Vector3f Schwerkraft = Differenznormalize (). MultLocal (Tpf); Schwimmdistanz = Differenz.Länge (); 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)); 

Als erstes berechnen wir das Vektor zwischen dem schwarzen Loch und dem Ziel. Als nächstes berechnen wir die Gravitationskraft. Wichtig ist, dass wir die Kraft mit der Zeit multiplizieren, die seit dem letzten Update vergangen ist, tpf, um mit jeder Bildrate den gleichen Effekt zu erzielen. Zum Schluss berechnen wir den Abstand zwischen dem Ziel und dem Schwarzen Loch.

Für jeden Zieltyp müssen wir die Kraft auf eine etwas andere Weise anwenden. Für den Spieler und für die Kugeln wird die Kraft umso stärker, je näher sie dem Schwarzen Loch sind:

 Schwerkraft.multLocal (250f / Entfernung);

Kugeln müssen abgestoßen werden; Deshalb multiplizieren wir ihre Schwerkraft mit einer negativen Zahl.

Suchende und Wanderer erhalten einfach eine Kraft, die unabhängig vom Abstand vom Schwarzen Loch immer gleich ist.

Nun sind wir mit der Umsetzung der Schwarzen Löcher fertig. Wir werden in den nächsten Kapiteln einige coole Effekte hinzufügen, aber für den Moment können Sie es ausprobieren!

Spitze: Beachten Sie, dass dies ist Ihre Spiel; Fühlen Sie sich frei, beliebige Parameter zu ändern! Sie können den Effektbereich für das Schwarze Loch, die Geschwindigkeit der Gegner oder den Spieler ändern… Diese Dinge haben einen enormen Einfluss auf das Gameplay. Manchmal lohnt es sich, ein bisschen mit den Werten zu spielen.

Das Head-Up-Display

Es gibt einige Informationen, die verfolgt und dem Player angezeigt werden müssen. Dafür ist das HUD (Head-Up-Display) da. Wir möchten die Leben der Spieler, den aktuellen Punktemultiplikator und natürlich die Punktzahl selbst verfolgen und dies dem Spieler zeigen.

Wenn der Spieler 2.000 Punkte (oder 4.000 oder 6.000 oder…) erzielt, erhält der Spieler ein anderes Leben. Zusätzlich möchten wir die Punktzahl nach jedem Spiel speichern und mit dem aktuellen Highscore vergleichen. Der Multiplikator erhöht sich jedes Mal, wenn der Spieler einen Gegner tötet, und springt zu einem zurück, wenn der Spieler in einiger Zeit nichts tötet.

Wir werden eine neue Klasse erstellen, genannt Hud. Im Hud Wir haben gleich zu Beginn einige Dinge zu initialisieren:

 öffentliche Klasse Hud private AssetManager assetManager; privater Knoten guiNode; private int screenWidth, screenHeight; private final int fontSize = 30; private final int multiplierExpiryTime = 2000; private final int maxMultiplier = 25; öffentliches int Leben; public int score; public int multiplier; privater langer MultiplikatorAktivierungszeit; private int scoreForExtraLife; private BitmapFont guiFont; private BitmapText livesText; private BitmapText scoreText; private BitmapText multiplierText; privater Knoten gameOverNode; public Hud (AssetManager assetManager, Node guiNode, int screenWidth, int screenHeight) this.assetManager = assetManager; this.guiNode = guiNode; this.screenWidth = screenWidth; this.screenHeight = screenHeight; setupText (); 

Das sind ziemlich viele Variablen, aber die meisten sind ziemlich selbsterklärend. Wir brauchen einen Hinweis auf die Vermögensverwalter um Text zu laden guiNode um es der Szene hinzuzufügen, und so weiter.

Als nächstes gibt es einige Variablen, die wir kontinuierlich verfolgen müssen, wie zB die Multiplikator, die Verfallszeit, der maximal mögliche Multiplikator und das Leben des Spielers.

Und zum Schluss haben wir noch welche BitmapText Objekte, die den eigentlichen Text speichern und auf dem Bildschirm anzeigen. Dieser Text wird in der Methode eingerichtet setupText (), welches am Ende des Konstruktors aufgerufen wird.

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

Um Text laden zu können, muss zuerst die Schrift geladen werden. In unserem Beispiel verwenden wir eine Standardschriftart, die mit der jMonkeyEngine geliefert wird.

Spitze: Natürlich können Sie Ihre eigenen Schriftarten erstellen und sie irgendwo in der Vermögenswerte Verzeichnis-vorzugsweise Vermögenswerte / Schnittstelle-und laden sie. Wenn Sie mehr wissen möchten, lesen Sie dieses Tutorial zum Laden von Schriftarten in jME.

Als Nächstes benötigen wir eine Methode, um alle Werte zurückzusetzen, damit wir neu beginnen können, wenn der Spieler zu oft stirbt:

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

Das Zurücksetzen der Werte ist einfach, aber wir müssen auch die Änderungen der Variablen auf das HUD anwenden. Wir machen das in einer separaten Methode:

 private void updateHUD () livesText.setText ("Lives:" + Leben); scoreText.setText ("Score:" + Score); multiplierText.setText ("Multiplier:" + Multiplikator); 

Während der Schlacht gewinnt der Spieler Punkte und verliert Leben. Wir nennen diese Methoden von MonkeyBlasterMain:

 public void addPoints (int basePoints) score + = basePoints * Multiplikator; if (score> = scoreForExtraLife) scoreForExtraLife + = 2000; lebt ++; plusMultiplier (); updateHUD ();  private void riseMultiplier () multiplierActivationTime = System.currentTimeMillis (); wenn (Multiplikator) < maxMultiplier)  multiplier++;   public boolean removeLife()  if (lives == 0) return false; lives--; updateHUD(); return true; 

Bemerkenswerte Konzepte in diesen Methoden sind:

  • Wann immer wir Punkte hinzufügen, prüfen wir, ob wir bereits die erforderliche Punktzahl erreicht haben, um ein zusätzliches Leben zu erhalten.
  • Wann immer wir Punkte hinzufügen, müssen wir auch den Multiplikator durch Aufrufen einer separaten Methode erhöhen.
  • Immer wenn wir den Multiplikator erhöhen, müssen wir uns des maximal möglichen Multiplikators bewusst sein und dürfen nicht darüber hinausgehen.
  • Immer wenn der Spieler einen Feind trifft, müssen wir das zurücksetzen multiplierActivationTime.
  • Wenn der Spieler keine zu entfernenden Leben mehr hat, kehren wir zurück falsch damit die Hauptklasse entsprechend handeln kann.

Es gibt zwei Dinge, die wir noch erledigen müssen.

Zuerst müssen wir den Multiplikator zurücksetzen, wenn der Spieler für eine Weile nichts tötet. Wir werden ein implementieren aktualisieren() Methode, die prüft, ob es Zeit ist, dies zu tun:

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

Das letzte, was wir tun müssen, ist das Spiel zu beenden. Wenn der Spieler sein ganzes Leben aufgebraucht hat, ist das Spiel vorbei und der Endstand sollte in der Mitte des Bildschirms angezeigt werden. Wir müssen auch prüfen, ob der aktuelle High Score niedriger ist als der aktuelle Score des Spielers, und wenn dies der Fall ist, speichern Sie den aktuellen Score als neuen High Score. (Beachten Sie, dass Sie eine Datei erstellen müssen highscore.txt oder Sie können keine Partitur laden.)

So beenden wir das Spiel in Hud:

 public void endGame () // init gameOverNode gameOverNode = neuer Knoten (); gameOverNode.setLocalTranslation (screenWidth / 2 - 180, screenHeight / 2 + 100,0); guiNode.attachChild (gameOverNode); // check highscore int highscore = loadHighscore (); if (score> highscore) saveHighscore (); // Init und Anzeigetext BitmapText gameOverText = new BitmapText (guiFont, false); gameOverText.setLocalTranslation (0,0,0); gameOverText.setSize (fontSize); gameOverText.setText ("Game Over"); gameOverNode.attachChild (gameOverText); BitmapText yourScoreText = neuer BitmapText (guiFont, false); yourScoreText.setLocalTranslation (0, -50,0); yourScoreText.setSize (fontSize); yourScoreText.setText ("Your Score:" + Score); gameOverNode.attachChild (yourScoreText); BitmapText highscoreText = neuer BitmapText (guiFont, false); highscoreText.setLocalTranslation (0, -100,0); highscoreText.setSize (fontSize); highscoreText.setText ("Highscore:" + Highscore); gameOverNode.attachChild (HighscoreText); 

Schließlich brauchen wir zwei letzte Methoden: loadHighscore () und saveHighscore ():

 private int loadHighscore () try FileReader fileReader = neuer FileReader (neue Datei ("highscore.txt")); BufferedReader reader = neuer BufferedReader (fileReader); String line = reader.readLine (); return Integer.valueOf (Zeile);  catch (FileNotFoundException e) e.printStackTrace ();  catch (IOException e) e.printStackTrace (); return 0;  private void saveHighscore () try FileWriter writer = neuer FileWriter (neue Datei ("highscore.txt"), false); writer.write (score + System.getProperty ("line.separator")); writer.close ();  catch (IOException e) e.printStackTrace ();
Spitze: Wie Sie vielleicht bemerkt haben, habe ich das nicht benutzt Vermögensverwalter den Text laden und speichern. Wir haben es benutzt, um alle Sounds und Grafiken zu laden richtig JME Weg zum Laden und Speichern von Texten ist eigentlich die Verwendung von Vermögensverwalter da es aber das Laden von Textdateien nicht selbst unterstützt, müssen wir ein registrieren TextLoader mit dem Vermögensverwalter. Sie können dies tun, wenn Sie möchten, aber in diesem Tutorial habe ich der Einfachheit halber die Standard-Java-Methode zum Laden und Speichern von Text beibehalten.

Jetzt haben wir eine große Klasse, die alle unsere HUD-Probleme behandelt. Das einzige, was wir jetzt tun müssen, ist, es dem Spiel hinzuzufügen.

Wir müssen das Objekt am Anfang deklarieren:

 privater Hud Hud;

... initialisieren Sie es in simpleInitApp ():

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

… Das HUD in aktualisieren simpleUpdate (Float-Tpf) (unabhängig davon, ob der Spieler lebt):

 hud.update ();

… Punkte hinzufügen, wenn der Spieler Gegner trifft (in checkCollisions ()):

 // füge Punkte hinzu, abhängig von der Art des Feindes if (enemyNode.getChild (i) .getName (). equals ("Seeker")) hud.addPoints (2);  else if (enemyNode.getChild (i) .getName (). equals ("Wanderer")) hud.addPoints (1); 
Achtung! Sie müssen die Punkte hinzufügen Vor Sie lösen die Feinde von der Szene, oder Sie haben Probleme mit enemyNode.getChild (i).

... und Leben entfernen, wenn der Spieler stirbt (in killPlayer ()):

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

Sie haben vielleicht bemerkt, dass wir auch eine neue Variable eingeführt haben, Spiel ist aus. Wir setzen es auf falsch am Anfang:

 private boolean gameOver = false;

Der Spieler sollte nach dem Spiel nicht mehr laichen, also fügen wir diese Bedingung hinzu simpleUpdate (Float-Tpf)

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

Jetzt können Sie das Spiel starten und prüfen, ob Sie etwas verpasst haben! Und dein Spiel hat ein neues Ziel: den Highscore zu schlagen. Ich wünsche Dir viel Glück!

Benutzerdefinierter Cursor

Da wir ein 2D-Spiel haben, müssen Sie noch etwas hinzufügen, um unser HUD zu perfektionieren: einen benutzerdefinierten Mauszeiger.
Es ist nichts Besonderes; Fügen Sie einfach diese Zeile ein simpleInitApp ():

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

Fazit

Das Gameplay ist jetzt vollständig abgeschlossen. In den verbleibenden zwei Teilen dieser Serie werden wir einige coole grafische Effekte hinzufügen. Dies macht das Spiel tatsächlich etwas schwieriger, da die Feinde nicht mehr so ​​leicht zu finden sind!