Gör en Neon Vector Shooter Med jME HUD och Black Holes

Hittills, i denna serie om att bygga ett Geometry Wars-inspirerat spel i jMonkeyEngine, har vi genomfört det mesta av spel och ljud. I den här delen kommer vi att avsluta spelet genom att lägga till svarta hål, och vi lägger till lite användargränssnitt för att visa spelarnas poäng.


Översikt

Här är vad vi arbetar mot över hela serien:


... och här är vad vi får i slutet av den här delen:


Förutom att ändra befintliga klasser lägger vi till två nya:

  • BlackHoleControl: Det är självklart att detta kommer att hantera beteendet hos våra svarta hål.
  • Hud: Här lagras och spelas spelarna poäng, liv och andra UI-element.

Låt oss börja med de svarta hålen.


Svarta hål

Det svarta hålet är en av de mest intressanta fienderna i Geometry Wars. I MonkeyBlaster, vår klon är det speciellt coolt när vi lägger till partikeleffekter och vridningsnätet i de kommande två kapitlen.

Grundfunktionalitet

De svarta hålen kommer att dra in spelarens skepp, närliggande fiender, och (efter nästa handledning) partiklar, men kommer att avvärja kulor.

Det finns många möjliga funktioner vi kan använda för attraktion eller avstängning. Det enklaste är att använda en konstant kraft, så att det svarta hålet drar med samma styrka oavsett objektets avstånd. Ett annat alternativ är att kraften ökar linjärt från noll, vid ett visst maximalt avstånd, till full styrka, för objekt direkt ovanför det svarta hålet. Och om vi skulle vilja modellera gravitationen mer realistiskt kan vi använda det inversa kvadratet av avståndet, vilket innebär att tyngdkraften är proportionell mot 1 / (distans * avstånd).

Vi använder faktiskt alla dessa tre funktioner för att hantera olika objekt. Kulorna kommer att avstötas med en konstant kraft, fienderna och spelarens skepp kommer att lockas med en linjär kraft och partiklarna kommer att använda en invers kvadratfunktion.

Genomförande

Vi börjar med att gyta våra svarta hål. För att uppnå det behöver vi en annan varibal i MonkeyBlasterMain:

 privat lång spawnCooldownBlackHole;

Nästa måste vi förklara en nod för de svarta hålen; låt oss kalla det blackHoleNode. Du kan deklarera och initialisera det precis som vi gjorde enemyNode i den tidigare handledningen.

Vi skapar också en ny metod, spawnBlackHoles, som vi kallar strax efter spawnEnemies i simpleUpdate (float tpf). Den faktiska gytningen är ungefär som att gyta fiender:

 private void spawn BlackHoles () if (blackHoleNode.getQuantity () < 2)  if (System.currentTimeMillis() - spawnCooldownBlackHole > 10f) spawnCooldownBlackHole = System.currentTimeMillis (); om (ny slumpmässig (). nextInt (1000) == 0) createblackHole (); 

Att skapa det svarta hålet följer också vår standardprocedur:

 privat tomt skapa BlackHole () Spatial blackHole = getSpatial ("Black Hole"); blackHole.setLocalTranslation (getSpawnPosition ()); blackHole.addControl (nya BlackHoleControl ()); blackHole.setUserData ( "aktiv", false); blackHoleNode.attachChild (Blackhole); 

Återigen laddar vi rumsliga, ställer in sin position, lägger till en kontroll, ställer den till icke-aktiv och slutligen bifogar den till lämplig nod. När du tittar på BlackHoleControl, du märker att det inte heller är mycket annorlunda.

Vi ska genomföra attraktionen och avstängningen senare, i MonkeyBlasterMain, men det finns en sak vi behöver adressera nu. Eftersom det svarta hålet är en stark fiende, vill vi inte att den ska gå ner lätt. Därför lägger vi till en variabel, träffpoäng, till BlackHoleControl, och ställa in dess ursprungliga värde till 10 så att den kommer att dö efter tio träffar.

 offentlig klass BlackHoleControl utökar AbstractControl privat long spawnTime; privata int hitpoints; offentliga BlackHoleControl () spawnTime = System.currentTimeMillis (); hitpoints = 10;  @Override protected void controlUpdate (float tpf) if ((Boolean) spatial.getUserData ("aktiv")) // vi använder den här platsen senare ... annat // hantera "aktiv" status = System.currentTimeMillis () - spawnTime; om (dif> = 1000f) spatial.setUserData ("aktiv", true);  ColorRGBA color = new ColorRGBA (1,1,1, dif / 1000f); Node spatialNode = (Node) spatial; Picture pic = (Bild) spatialNode.getChild ("Black Hole"); pic.getMaterial () setColor ( "Färg", färg).  @Override protected void controlRender (RenderManager rm, ViewPort vp)  public void wasShot () hitpoints--;  offentliga boolean isDead () return hitpoints <= 0;  

Vi är nästan färdiga med grundkoden för de svarta hålen. Innan vi börjar implementera tyngdkraften måste vi ta hand om kollisionerna.

När spelaren eller en fiende kommer för nära det svarta hålet, kommer det att dö. Men när en kula lyckas slå den, kommer det svarta hålet att förlora ett träffpunkt.

Ta en titt på följande kod. Den tillhör handleCollisions (). Det är i princip detsamma som för alla andra kollisioner:

 // är det något som kolliderar med ett svart hål? för (i = 0; i 

Nåväl, du kan döda det svarta hålet nu, men det är inte den enda gången när det ska gå ut. När spelaren dör, kommer alla fiender att försvinnas och det ska också vara det svarta hålet. För att hantera detta, lägg bara till följande rad till vår killPlayer () metod:

 blackHoleNode.detachAllChildren ();

Nu är det dags att genomföra de coola sakerna. Vi skapar en annan metod, handtagGravity (float tpf). Bara kalla det med de andra metoderna i simplueUpdate (float tpf).

I denna metod kontrollerar vi alla enheter (spelare, kulor och fiender) för att se om de ligger nära ett svart hål - låt oss säga inom 250 pixlar - och om de är så tillämpar vi lämplig effekt:

 privat tomt handtagGravity (float tpf) for (int i = 0; i 

För att kontrollera om två enheter ligger inom ett visst avstånd från varandra, skapar vi en metod som heter isNearby () som jämför platserna för de två rumsliga områdena:

 privat booleansk isNearby (rumslig a, rumslig b, flytavstånd) Vector3f pos1 = a.getLocalTranslation (); Vector3f pos2 = b.getLocalTranslation (); returnera pos1.distanceSquared (pos2) <= distance * distance; 

Nu när vi har kontrollerat varje enhet, om den är aktiv och inom det angivna avståndet till ett svart hål kan vi äntligen tillämpa gravitationens effekt. För att göra det använder vi kontrollerna: vi skapar en metod i varje kontroll, kallad applyGravity (Vector3f gravity).

Låt oss ta en titt på var och en av dem:

PlayerControl:

 public void applyGravity (Vector3f gravity) spatial.move (gravitation); 

BulletControl:

 public void applyGravity (Vector3f gravity) direction.addLocal (gravitation); 

SeekerControl och WandererControl:

 public void applyGravity (Vector3f gravity) hastighet.addLokal (gravitation); 

Och nu tillbaka till huvudklassen, MonkeyBlasterMain. Jag ska ge dig metoden först och förklara stegen under den:

 privata tomrummetGravity (Spatial blackHole, Spatial target, float tpf) Vector3f skillnad = blackHole.getLocalTranslation (). subtrahera (target.getLocalTranslation ()); Vector3f gravitation = difference.normalize (). MultLocal (tpf); float distance = difference.length (); om (target.getName (). equals ("Player")) gravitation.multLocal (250f / distans); target.getControl (PlayerControl.class) .applyGravity (gravity.mult (80f));  annars om (target.getName (). equals ("Bullet")) gravitation.multLocal (250f / distance); target.getControl (BulletControl.class) .applyGravity (gravity.mult (-0.8f));  annars om (target.getName (). equals ("sökare")) target.getControl (SeekerControl.class) .applyGravity (gravity.mult (150000));  annat om (target.getName (). equals ("Wanderer")) target.getControl (WandererControl.class) .applyGravity (gravity.mult (150000)); 

Det första vi gör är att beräkna Vektor mellan det svarta hålet och målet. Därefter beräknar vi gravitationsstyrkan. Det viktiga att notera är att vi-återigen-multiplicera kraften vid den tid som har gått sedan senaste uppdateringen, TPF, för att uppnå samma effekt med varje bildhastighet. Slutligen beräknar vi avståndet mellan målet och det svarta hålet.

För varje typ av mål måste vi tillämpa kraften på ett något annorlunda sätt. För spelaren och för kulor blir kraften starkare ju närmare de är i det svarta hålet:

 gravity.multLocal (250F / avstånd);

Kulor måste avstötas; det är därför vi multiplicerar deras gravitationskraft med ett negativt tal.

Sökare och vandrare får helt enkelt en kraft som alltid är densamma, oavsett deras avstånd från det svarta hålet.

Vi är nu färdiga med genomförandet av de svarta hålen. Vi lägger till några coola effekter i nästa kapitel, men för närvarande kan du testa det!

Tips: Observera att detta är din spel; gärna ändra några parametrar du gillar! Du kan ändra effektområdet för det svarta hålet, fiendernas eller spelarens hastighet ... Dessa saker har en enorm effekt på gameplayen. Ibland är det värt att spela lite med värdena.

Huvudbilden

Det finns viss information som måste spåras och visas till spelaren. Det är vad HUD (Head-Up Display) finns för. Vi vill spåra spelarnas liv, nuvarande poängmultiplikator, och givetvis poängen själv och visa allt detta till spelaren.

När spelaren får 2 000 poäng (eller 4 000 eller 6 000 eller ...) kommer spelaren att få ett nytt liv. Dessutom vill vi spara poängen efter varje spel och jämföra det med nuvarande highscore. Multiplikatorn ökar varje gång spelaren dödar en fiende och hoppar tillbaka till en när spelaren inte dödar någonting på en tid.

Vi skapar en ny klass för allt som heter Hud. I Hud vi har en hel del saker att initialisera rätt i början:

 offentlig klass Hud privat AssetManager assetManager; privat nod guiNode; privat int skärmvidd, skärmvåg; privat slutlig int fontSize = 30; privat slutlig int multiplicatorExpiryTime = 2000; privat slutlig int maxMultiplier = 25; offentliga int liv; offentliga int poäng allmän int multiplikator; privat lång multiplikatorActivationTime; privat int poängFörExtraLife; privat BitmapFont guiFont; privat BitmapText livesText; privat BitmapText scoreText; privat BitmapText multiplicerText; privat nod GameOverNode; offentliga Hud (AssetManager assetManager, Node guiNode, int screenWidth, int screenHeight) this.assetManager = assetManager; this.guiNode = guiNode; this.screenWidth = screenWidth; this.screenHeight = screenHeight; setupText (); 

Det är ganska många variabler, men de flesta är ganska självförklarande. Vi behöver hänvisa till AssetManager att ladda text till guiNode att lägga till den till scenen och så vidare.

Därefter finns det några variabler som vi behöver spåra kontinuerligt, som multiplikator, dess utgångstid, maximal multiplikator och spelarens liv.

Och äntligen har vi lite BitmapText objekt som lagrar den faktiska texten och visar den på skärmen. Denna text är upprättad i metoden setupText (), som kallas i slutet av konstruktören.

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

För att ladda text måste vi ladda in teckensnittet först. I vårt exempel använder vi en standard typsnitt som levereras med jMonkeyEngine.

Tips: Självklart kan du skapa egna teckensnitt, placera dem någonstans i tillgångar katalog företrädesvis tillgångar / gränssnitts-och ladda dem. Om du vill veta mer, kolla in den här handledningen om att ladda typsnitt i jME.

Därefter behöver vi en metod för att återställa alla värden så att vi kan börja om spelaren dör för många gånger:

 public void reset () score = 0; multiplikatorn = 1; bor = 4; multiplikatorActivationTime = System.currentTimeMillis (); scoreForExtraLife = 2000; updateHUD (); 

Återställa värdena är enkelt, men vi måste också tillämpa ändringarna av variablerna till HUD. Vi gör det i en separat metod:

 privat tomt uppdateringHUD () livesText.setText ("Liv:" + liv); scoreText.setText ("Score:" + score); multiplikatorText.setText ("Multiplikator:" + multiplikator); 

Under matchen vinner spelaren poäng och förlorar liv. Vi ringer dessa metoder från MonkeyBlasterMain:

 public void addPoints (int basPoäng) poäng + = basPoäng * multiplikator; om (poäng> = scoreForExtraLife) scoreForExtraLife + = 2000; lever ++;  increaseMultiplier (); updateHUD ();  privat void increaseMultiplier () multiplicierActivationTime = System.currentTimeMillis (); om (multiplikatorn < maxMultiplier)  multiplier++;   public boolean removeLife()  if (lives == 0) return false; lives--; updateHUD(); return true; 

Anmärkningsvärda begrepp i dessa metoder är:

  • När vi lägger till poäng kontrollerar vi om vi redan har uppnått det nödvändiga värdet för att få ett extra liv.
  • När vi lägger till poäng måste vi också öka multiplikatorn genom att ange en separat metod.
  • När vi ökar multiplikatorn måste vi vara medvetna om den maximala multiplikatorn och inte gå längre än det.
  • När spelaren träffar en fiende, måste vi återställa multiplierActivationTime.
  • När spelaren inte har några liv kvar att tas bort, återkommer vi falsk så att huvudklassen kan agera i enlighet därmed.

Det finns två saker kvar som vi behöver hantera.

Först måste vi återställa multiplikatorn om spelaren inte dödar någonting för ett tag. Vi genomför en uppdatering() metod som kontrollerar om det är dags att göra det här:

 public void update () if (multiplikator> 1) om (System.currentTimeMillis () - multiplicerActivationTime> multiplicerExpiryTime) multiplikator = 1; multiplikatorActivationTime = System.currentTimeMillis (); updateHUD (); 

Det sista vi behöver ta hand om slutar spelet. När spelaren har utnyttjat hela sitt liv är spelet över och slutresultatet ska visas mitt på skärmen. Vi måste också kontrollera om den nuvarande högsta poängen är lägre än spelarens nuvarande poäng och, om så, spara nuvarande poäng som den nya highscoreen. (Observera att du måste skapa en fil highscore.txt först, eller du kan inte ladda ett betyg.)

Så här avslutar vi spelet Hud:

 public void endGame () // init gameOverNode gameOverNode = ny nod (); gameOverNode.setLocalTranslation (screenWidth / 2 - 180, screenHeight / 2 + 100,0); guiNode.attachChild (gameOverNode); // kolla highscore int highscore = loadHighscore (); om (poäng> highscore) saveHighscore (); // init och displaytext BitmapText gameOverText = ny BitmapText (guiFont, false); gameOverText.setLocalTranslation (0,0,0); gameOverText.setSize (fontSize); gameOverText.setText ("Game Over"); gameOverNode.attachChild (gameOverText); BitmapText yourScoreText = ny BitmapText (guiFont, false); yourScoreText.setLocalTranslation (0, -50,0); yourScoreText.setSize (fontSize); yourScoreText.setText ("Ditt betyg:" + poäng); gameOverNode.attachChild (yourScoreText); BitmapText highscoreText = ny BitmapText (guiFont, false); highscoreText.setLocalTranslation (0, -100,0); highscoreText.setSize (fontSize); highscoreText.setText ("Highscore:" + highscore); gameOverNode.attachChild (highscoreText); 

Slutligen behöver vi två sista metoder: loadHighscore () och saveHighscore ():

 privat int loadHighscore () försök FileReader fileReader = ny FileReader (ny fil ("highscore.txt")); BufferedReader reader = Ny BufferedReader (fileReader); String line = reader.readLine (); returnera Integer.valueOf (linje);  fånga (FileNotFoundException e) e.printStackTrace ();  fånga (IOException e) e.printStackTrace (); returnera 0;  privat void saveHighscore () försök FileWriter writer = new FileWriter (ny fil ("highscore.txt"), false); writer.write (poäng + System.getProperty ( "line.separator")); writer.close ();  fånga (IOException e) e.printStackTrace ();
Tips: Som du kanske har märkt använde jag inte assetManager att ladda och spara texten. Vi använde den för att ladda alla ljud och grafik, och rätt jME sätt att ladda och spara texter använder faktiskt assetManager för det, men eftersom det inte stöder textfilläsning på egen hand, skulle vi behöva registrera en TextLoader med assetManager. Du kan göra det om du vill, men i denna handledning fastnade jag på standard Java-sätt att ladda och spara text, för enkelhets skull.

Nu har vi en stor klass som hanterar alla våra HUD-relaterade problem. Det enda vi behöver göra nu är att lägga till det i spelet.

Vi måste förklara objektet i början:

 privat hud hud;

... initiera den i simpleInitApp ():

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

... uppdatera HUD i simpleUpdate (float tpf) (oavsett om spelaren lever):

 hud.update ();

... lägg till poäng när spelaren träffar fiender (i checkCollisions ()):

 // lägg till poäng beroende på vilken typ av fiende om (enemyNode.getChild (i) .getName (). equals ("Seeker")) hud.addPoints (2);  annars om (enemyNode.getChild (i) .getName (). equals ("Wanderer")) hud.addPoints (1); 
Se upp! Du måste lägga till poängen innan du avlägsnar fienderna från scenen, eller du kommer att stöta på problem med enemyNode.getChild (i).

... och ta bort liv när spelaren dör (i killPlayer ()):

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

Du kanske har märkt att vi introducerade en ny variabel också, gameover. Vi ställer in det på falsk i början:

 privat booleansk gameOver = false;

Spelaren ska inte hämta mer när spelet är över, så lägger vi till detta villkor till simpleUpdate (float tpf)

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

Nu kan du starta spelet och kontrollera om du har missat något! Och ditt spel har fått ett nytt mål: slår highscore. Jag önskar dig lycka till!

Anpassad markör

Eftersom vi har ett 2D-spel finns det ytterligare en sak att lägga till för att göra vår HUD perfekt: en anpassad muspekare.
Det är inget speciellt; skriv bara in den här raden simpleInitApp ():

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

Slutsats

Spelet är nu helt klart. I de återstående två delarna av denna serie lägger vi till några fina grafiska effekter. Detta kommer faktiskt göra spelet lite hårdare, eftersom fienderna kanske inte är lika lätta att upptäcka längre!