Maak een Neon Vector Shooter met jME HUD en zwarte gaten

Tot nu toe hebben we in deze serie over het bouwen van een op Geometry Wars geïnspireerde game in jMonkeyEngine het grootste deel van de gameplay en audio geïmplementeerd. In dit deel voltooien we het spel door zwarte gaten toe te voegen en voegen we een gebruikersinterface toe om de score van de spelers weer te geven.


Overzicht

Dit is waar we naartoe werken in de hele serie:


... en hier is wat we zullen hebben aan het einde van dit deel:


Naast het aanpassen van bestaande klassen, voegen we er twee nieuwe toe:

  • BlackHoleControl: Onnodig te zeggen dat dit het gedrag van onze zwarte gaten zal behandelen.
  • hud: Hier zullen we de scores, levens en andere UI-elementen van de speler opslaan en weergeven.

Laten we beginnen met de zwarte gaten.


Zwarte gaten

Het zwarte gat is een van de meest interessante vijanden in Geometry Wars. In MonkeyBlaster, onze kloon, is het vooral gaaf als we in de volgende twee hoofdstukken partikeleffecten en het warping-net toevoegen.

Basisfunctionaliteit

De zwarte gaten zullen het schip van de speler, nabije vijanden en (na de volgende tutorial) deeltjes trekken, maar zullen kogels afstoten.

Er zijn veel mogelijke functies die we kunnen gebruiken voor aantrekking of afstoting. De eenvoudigste is om een ​​constante kracht te gebruiken, zodat het zwarte gat met dezelfde kracht trekt, ongeacht de afstand van het object. Een andere optie is om de kracht lineair te laten toenemen van nul, op een maximale afstand tot de volledige sterkte, voor objecten direct boven op het zwarte gat. En als we de zwaartekracht meer realistisch willen modelleren, kunnen we het inverse kwadraat van de afstand gebruiken, wat betekent dat de zwaartekracht evenredig is aan 1 / (afstand * afstand).

We zullen eigenlijk elk van deze drie functies gebruiken om verschillende objecten te behandelen. De kogels worden met een constante kracht afgestoten, de vijanden en het schip van de speler worden aangetrokken met een lineaire kracht, en de deeltjes zullen een omgekeerde vierkante functie gebruiken.

Implementatie

We beginnen met het spawnen van onze zwarte gaten. Om dat te bereiken hebben we nog een varibale nodig MonkeyBlasterMain:

 privé lange spawnCooldown Black Hole;

Vervolgens moeten we een knooppunt voor de zwarte gaten aangeven; laten we het noemen blackHoleNode. U kunt het declareren en initialiseren net zoals wij dat deden enemyNode in de vorige tutorial.

We zullen ook een nieuwe methode maken, spawnBlackHoles, die we meteen noemen spawnEnemies in simpleUpdate (float tpf). De eigenlijke spawning is redelijk vergelijkbaar met het spawnen van vijanden:

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

Het maken van het zwarte gat volgt ook onze standaardprocedure:

 private void createBlackHole () Spatial blackHole = getSpatial ("Black Hole"); blackHole.setLocalTranslation (getSpawnPosition ()); blackHole.addControl (nieuwe BlackHoleControl ()); blackHole.setUserData ( "actief", false); blackHoleNode.attachChild (blackhole); 

Nogmaals, we laden het ruimtelijke in, stellen de positie in, voegen een controle toe, stellen het in op niet-actief en verbinden het uiteindelijk met het juiste knooppunt. Wanneer je naar kijkt BlackHoleControl, je zult merken dat het ook niet veel anders is.

We zullen de aantrekking en afstoting later implementeren, in MonkeyBlasterMain, maar er is één ding dat we nu moeten aanpakken. Omdat het zwarte gat een sterke vijand is, willen we niet dat het gemakkelijk naar beneden gaat. Daarom voegen we een variabele toe, hitpoints, naar de BlackHoleControl, en stel de beginwaarde in op 10 zodat het na tien slagen zal sterven.

 public class BlackHoleControl breidt AbstractControl uit private long spawnTime; intieme privépunten; openbare BlackHoleControl () spawnTime = System.currentTimeMillis (); hitpoints = 10;  @Override beschermde ongeldige controleUpdate (float tpf) if ((Boolean) spatial.getUserData ("active")) // we zullen deze plek later gebruiken ... else // omgaan met de "actieve" -status lange dif = System.currentTimeMillis () - spawnTime; if (dif> = 1000f) spatial.setUserData ("active", true);  ColorRGBA-kleur = nieuwe ColorRGBA (1,1,1, dif / 1000f); Node spatialNode = (Node) spatial; Picture pic = (Afbeelding) spatialNode.getChild ("Black Hole"); pic.getMaterial () setColor ( "Kleur", kleur).;  @Override beschermd void controlRender (RenderManager rm, ViewPort vp)  public void wasShot () hitpoints--;  public boolean isDead () keer hitpoints terug <= 0;  

We zijn bijna klaar met de basiscode voor de zwarte gaten. Voordat we de zwaartekracht kunnen implementeren, moeten we voor de botsingen zorgen.

Wanneer de speler of een vijand te dicht bij het zwarte gat komt, gaat hij dood. Maar wanneer een kogel erin slaagt om het te raken, verliest het zwarte gat één hitpoint.

Bekijk de volgende code. Het is van handleCollisions (). Het is in principe hetzelfde als bij alle andere botsingen:

 // botst er iets met een zwart gat? voor (i = 0; i 

Welnu, je kunt nu het zwarte gat doden, maar dat is niet de enige keer dat het zou verdwijnen. Wanneer de speler sterft, verdwijnen alle vijanden en dat geldt ook voor het zwarte gat. Om dit aan te pakken, voegt u gewoon de volgende regel toe aan onze killPlayer () methode:

 blackHoleNode.detachAllChildren ();

Nu is het tijd om de coole dingen te implementeren. We zullen een andere methode maken, handleGravity (float tpf). Noem het gewoon met de andere methoden in simplueUpdate (float tpf).

In deze methode controleren we alle entiteiten (spelers, kogels en vijanden) om te zien of ze in de buurt van een zwart gat zijn - laten we zeggen binnen 250 pixels - en als ze dat zijn, passen we het juiste effect toe:

 private ongeldige handleGravity (float tpf) for (int i = 0; i 

Om te controleren of twee entiteiten zich op een bepaalde afstand van elkaar bevinden, maken we een methode genaamd isNearby () die de locaties van de twee ruimtelijkheden vergelijkt:

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

Nu we elke entiteit hebben gecontroleerd, als deze actief is en zich binnen de opgegeven afstand van een zwart gat bevindt, kunnen we eindelijk het effect van de zwaartekracht toepassen. Om dat te doen, zullen we gebruik maken van de controls: we creëren een methode in elk besturingselement, genaamd applyGravity (Vector3f gravity).

Laten we een kijkje nemen naar elk van hen:

PlayerControl:

 openbare ongeldige toepassingGravity (Vector3f gravity) spatial.move (gravity); 

BulletControl:

 openbare ongeldige toepassingGravity (Vector3f gravity) direction.addLocal (gravity); 

SeekerControl en WandererControl:

 openbare nietige toepassingGravity (Vector3f gravity) velocity.addLocal (gravity); 

En nu terug naar de hoofdklasse, MonkeyBlasterMain. Ik zal je eerst de methode geven en de stappen eronder uitleggen:

 private ongeldige toepassingGravity (Spatial blackHole, Spatial target, float tpf) Vector3f difference = blackHole.getLocalTranslation (). subtract (target.getLocalTranslation ()); Vector3f zwaartekracht = difference.normalize (). MultLocal (tpf); vlotterafstand = difference.length (); 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) .apply Gravity (gravity.mult (150000));  else if (target.getName (). equals ("Wanderer")) target.getControl (WandererControl.class) .apply Gravity (gravity.mult (150000)); 

Het eerste wat we doen is het berekenen van de Vector tussen het zwarte gat en het doelwit. Vervolgens berekenen we de zwaartekracht. Het belangrijkste om op te merken is dat we - nogmaals - de kracht vermenigvuldigen met de tijd die is verstreken sinds de laatste update, tpf, om hetzelfde effect te bereiken met elke framesnelheid. Uiteindelijk berekenen we de afstand tussen het doel en het zwarte gat.

Voor elk type doelwit moeten we de kracht op een enigszins andere manier toepassen. Voor de speler en voor kogels wordt de kracht sterker naarmate ze dichter bij het zwarte gat zijn:

 gravity.multLocal (250F / afstand);

Kogels moeten worden afgestoten; daarom vermenigvuldigen we hun zwaartekracht met een negatief getal.

Zoekers en zwervers krijgen gewoon een kracht die altijd hetzelfde is, ongeacht hun afstand tot het zwarte gat.

We zijn nu klaar met de implementatie van de zwarte gaten. We zullen een aantal coole effecten toevoegen in de volgende hoofdstukken, maar voor nu kun je het uittesten!

Tip: Merk op dat dit zo is jouw spel; voel je vrij om parameters aan te passen die jij leuk vindt! Je kunt het effectgebied veranderen voor het zwarte gat, de snelheid van de vijanden of de speler ... Deze dingen hebben een enorm effect op de gameplay. Soms is het de moeite waard om een ​​beetje met de waarden te spelen.

Het head-up display

Er is wat informatie die moet worden bijgehouden en weergegeven aan de speler. Dat is waar de HUD (Head-Up Display) voor is. We willen de spelerslevens, de huidige scorevermenigvuldiger en natuurlijk de score zelf bijhouden en dit aan de speler laten zien.

Wanneer de speler 2.000 punten scoort (of 4.000, of 6.000, of ...) krijgt de speler een nieuw leven. Daarnaast willen we de score na elk spel opslaan en vergelijken met de huidige highscore. De vermenigvuldiger neemt toe telkens wanneer de speler een vijand doodt en terug springt naar een speler wanneer de speler in een bepaalde tijd niets doodt.

We zullen een nieuwe klasse maken voor dat allemaal, genaamd hud. In hud we hebben nogal wat dingen die we in het begin moeten initialiseren:

 public class Hud private AssetManager assetManager; private Node guiNode; privé int scherm Breedte, scherm Hoogte; private final int fontSize = 30; private finale int multiplierExpiryTime = 2000; private finale int maxMultiplier = 25; openbare int-levens; openbare int-score; openbare int vermenigvuldiger; private long multiplierActivationTime; privé int scoreForExtraLife; privé BitmapFont guiFont; privé BitmapText livesText; privé BitmapText scoreText; private BitmapText multiplierText; private Node gameOverNode; public Hud (AssetManager assetManager, Node guiNode, int screenWidth, int screenHeight) this.assetManager = assetManager; this.guiNode = guiNode; this.screenWidth = screenWidth; this.screenHeight = screenHeight; setupText (); 

Dat zijn nogal wat variabelen, maar de meeste zijn vrij duidelijk. We moeten een verwijzing naar de hebben Vermogensbeheerder om tekst te laden, naar de guiNode om het aan de scène toe te voegen, enzovoort.

Vervolgens zijn er een paar variabelen die we continu moeten bijhouden, zoals de multiplier, de vervaltijd, de maximaal mogelijke vermenigvuldiger en de levensduur van de speler.

En tot slot hebben we er een paar BitmapText objecten, die de eigenlijke tekst opslaan en op het scherm weergeven. Deze tekst is ingesteld in de methode setupText (), die aan het einde van de constructor wordt genoemd.

 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 ("Lives:" + lives); 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); 

Om tekst te laden, moeten we eerst het lettertype laden. In ons voorbeeld gebruiken we een standaardlettertype dat wordt meegeleverd met de jMonkeyEngine.

Tip: Je kunt natuurlijk je eigen lettertypen maken, deze ergens in de middelen directory-voorkeur activa / Interface-en laad ze. Als je meer wilt weten, bekijk dan deze tutorial over het laden van lettertypen in jME.

Vervolgens hebben we een methode nodig om alle waarden opnieuw in te stellen, zodat we opnieuw kunnen beginnen als de speler te vaak overlijdt:

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

Het opnieuw instellen van de waarden is eenvoudig, maar we moeten ook de wijzigingen van de variabelen toepassen op de HUD. We doen dat op een aparte manier:

 private void update HUD () livesText.setText ("Lives:" + lives); scoreText.setText ("Score:" + score); multiplierText.setText ("Multiplier:" + vermenigvuldiger); 

Tijdens het gevecht krijgt de speler punten en verliest hij levens. We zullen deze methoden noemen MonkeyBlasterMain:

 openbare void addPoints (int basePoints) score + = basePoints * multiplier; if (score> = scoreForExtraLife) scoreForExtraLife + = 2000; woont ++;  increaseMultiplier (); updateHUD ();  private void increase increaseMultiplier () multiplierActivationTime = System.currentTimeMillis (); if (vermenigvuldiger < maxMultiplier)  multiplier++;   public boolean removeLife()  if (lives == 0) return false; lives--; updateHUD(); return true; 

Bekende concepten in die methoden zijn:

  • Telkens we punten toevoegen, controleren we of we al de benodigde score hebben bereikt om een ​​extra leven te krijgen.
  • Wanneer we punten toevoegen, moeten we ook de vermenigvuldiger verhogen door een afzonderlijke methode aan te roepen.
  • Telkens wanneer we de vermenigvuldiger verhogen, moeten we ons bewust zijn van de maximaal mogelijke vermenigvuldiger en niet verder gaan.
  • Wanneer de speler een vijand raakt, moeten we de multiplierActivationTime.
  • Als de speler geen levens meer heeft om te worden verwijderd, keren we terug vals zodat de hoofdklasse dienovereenkomstig kan handelen.

Er zijn nog twee dingen die we moeten behandelen.

Eerst moeten we de multiplier resetten als de speler een tijdje niets doodt. We zullen een implementeren bijwerken() methode die controleert of het tijd is om dit te doen:

 openbare ongeldige update () if (vermenigvuldiger> 1) if (System.currentTimeMillis () - multiplierActivationTime> multiplierExpiryTime) multiplier = 1; multiplierActivationTime = System.currentTimeMillis (); updateHUD (); 

Het laatste waar we voor moeten zorgen is het beëindigen van het spel. Wanneer de speler zijn hele leven heeft opgebruikt, is het spel afgelopen en moet de uiteindelijke score midden op het scherm worden weergegeven. We moeten ook controleren of de huidige hoge score lager is dan de huidige score van de speler en, zo ja, de huidige score opslaan als de nieuwe hoogste score. (Merk op dat je een bestand moet maken highscore.txt eerst, of je kunt geen score laden.)

Dit is hoe we het spel beëindigen hud:

 public void endGame () // init gameOverNode gameOverNode = new Node (); gameOverNode.setLocalTranslation (screenWidth / 2 - 180, screenHeight / 2 + 100,0); guiNode.attachChild (gameOverNode); // bekijk highscore int highscore = loadHighscore (); if (score> highscore) saveHighscore (); // init and display text 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 ("Jouw 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); 

Ten slotte hebben we twee laatste methoden nodig: loadHighscore () en saveHighscore ():

 private int loadHighscore () try FileReader fileReader = nieuwe FileReader (nieuw bestand ("highscore.txt")); BufferedReader-lezer = nieuwe BufferedReader (fileReader); String line = reader.readLine (); return Integer.valueOf (regel);  catch (FileNotFoundException e) e.printStackTrace ();  catch (IOException e) e.printStackTrace (); retourneer 0;  private void saveHighscore () try FileWriter writer = nieuwe FileWriter (nieuw bestand ("highscore.txt"), false); writer.write (score + System.getProperty ( "line.separator")); writer.close ();  catch (IOException e) e.printStackTrace ();
Tip: Zoals je misschien hebt gemerkt, heb ik de vermogensbeheerder om de tekst te laden en op te slaan. We hebben het gebruikt voor het laden van alle geluiden en afbeeldingen, en de gepast jME manier om teksten te laden en op te slaan, gebruikt eigenlijk de vermogensbeheerder Maar omdat het het laden van tekstbestanden op zichzelf niet ondersteunt, moeten we een TextLoader met de vermogensbeheerder. U kunt dat doen als u wilt, maar in deze tutorial bleef ik voor de eenvoud bij de standaard Java-manier om tekst te laden en op te slaan.

Nu hebben we een grote klas die al onze HUD-gerelateerde problemen zal behandelen. Het enige wat we nu moeten doen is het toevoegen aan het spel.

We moeten het object aan het begin declareren:

 privé Hud hud;

... initialiseer het in simpleInitApp ():

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

... update de HUD in simpleUpdate (float tpf) (ongeacht of de speler nog in leven is):

 hud.update ();

... punten toevoegen wanneer de speler vijanden raakt (in checkCollisions ()):

 // voeg punten toe afhankelijk van het type vijand als (enemyNode.getChild (i) .getName (). equals ("Seeker")) hud.addPoints (2);  else if (enemyNode.getChild (i) .getName (). equals ("Wanderer")) hud.addPoints (1); 
Kijk uit! U moet de punten toevoegen voor je maakt de vijanden los van het toneel, anders kom je in de problemen enemyNode.getChild (i).

... en levens verwijderen wanneer de speler sterft (in killPlayer ()):

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

Je hebt misschien gemerkt dat we ook een nieuwe variabele hebben geïntroduceerd, spel is over. We zullen het regelen vals in het begin:

 private boolean gameOver = false;

De speler mag niet meer spawnen nadat het spel is afgelopen, dus we voegen deze voorwaarde toe aan simpleUpdate (float tpf)

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

Nu kun je het spel opstarten en controleren of je iets hebt gemist! En je game heeft een nieuw doel: de hoogste score verslaan. ik wens je veel succes!

Aangepaste cursor

Omdat we een 2D-game hebben, is er nog één ding om toe te voegen om onze HUD te perfectioneren: een aangepaste muiscursor.
Het is niets bijzonders; plaats deze regel gewoon in simpleInitApp ():

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

Conclusie

De gameplay is nu helemaal klaar. In de resterende twee delen van deze serie zullen we een aantal coole grafische effecten toevoegen. Dit maakt het spel eigenlijk iets moeilijker, omdat de vijanden misschien niet zo gemakkelijk meer te herkennen zijn!