Lag en Neon Vector Shooter Med jME HUD og Black Holes

Så langt, i denne serien om å bygge et Geometry Wars-inspirert spill i jMonkeyEngine, har vi implementert det meste av spill og lyd. I denne delen vil vi fullføre spillingen ved å legge til svarte hull, og vi legger til noen brukergrensesnitt for å vise spillerne poengsummen.


Oversikt

Her jobber vi over hele serien:


... og her er hva vi får ved slutten av denne delen:


I tillegg til å endre eksisterende klasser, legger vi til to nye:

  • BlackHoleControl: Unødvendig å si, dette vil håndtere oppførselen til våre svarte hull.
  • Hud: Her lagrer og viser vi spillerne poeng, liv og andre UI-elementer.

La oss starte med de svarte hullene.


Svarte hull

Det svarte hullet er en av de mest interessante fiender i Geometry Wars. I MonkeyBlaster, vår klone, er det spesielt kult når vi legger til partikkeleffekter og vridningsnettet i de neste to kapitlene.

Grunnleggende funksjonalitet

De svarte hullene trekker inn spillerens skip, nærliggende fiender og (etter neste opplæring) partikler, men vil avvise kuler.

Det er mange mulige funksjoner vi kan bruke for tiltrekning eller frastøt. Det enkleste er å bruke en konstant kraft, slik at det svarte hullet trekker med samme styrke uavhengig av objektets avstand. Et annet alternativ er å få kraften til å øke lineært fra null, i noen maksimal avstand, til full styrke, for objekter direkte på toppen av det svarte hullet. Og hvis vi ønsker å modellere tyngdekraften mer realistisk, kan vi bruke den inverse firkanten av avstanden, noe som betyr at tyngdekraften er proporsjonal med 1 / (distanse * avstand).

Vi vil faktisk bruke hver av disse tre funksjonene til å håndtere forskjellige objekter. Kulene vil bli avstøtet med konstant kraft, fiender og spillerens skip vil bli tiltrukket med en lineær kraft, og partiklene vil bruke en invers firkantfunksjon.

Gjennomføring

Vi starter med å gyte våre svarte hull. For å oppnå det trenger vi en annen varibal i MonkeyBlasterMain:

 privat lang spawnCooldownBlackHole;

Deretter må vi deklarere en knutepunkt for de svarte hullene; la oss kalle det blackHoleNode. Du kan deklarere og initialisere det akkurat som vi gjorde enemyNode i den forrige opplæringen.

Vi lager også en ny metode, spawnBlackHoles, som vi ringer rett etter spawnEnemies i simpleUpdate (float tpf). Den faktiske gytingen er ganske lik gytende fiender:

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

Å lage det svarte hullet følger også vår standard prosedyre:

 privat tomrom createBlackHole () Spatial blackHole = getSpatial ("Black Hole"); blackHole.setLocalTranslation (getSpawnPosition ()); blackHole.addControl (ny BlackHoleControl ()); blackHole.setUserData ( "aktiv", false); blackHoleNode.attachChild (black); 

Igjen, laster vi romlig, setter sin posisjon, legger til en kontroll, setter den til ikke-aktiv, og til slutt legger den til riktig knutepunkt. Når du tar en titt på BlackHoleControl, Du vil legge merke til at det ikke er mye forskjellig heller.

Vi vil implementere tiltrekningen og frastøtingen senere, i MonkeyBlasterMain, men det er en ting vi trenger å ta opp nå. Siden det svarte hullet er en sterk fiende, vil vi ikke at den skal gå ned lett. Derfor legger vi til en variabel, Helse poeng, til BlackHoleControl, og sett inn initialverdien til 10 slik at den vil dø etter ti treff.

 offentlig klasse BlackHoleControl utvider AbstractControl private long spawnTime; private int hitpoints; offentlig BlackHoleControl () spawnTime = System.currentTimeMillis (); hitpoints = 10;  @Override protected void controlUpdate (float tpf) hvis ((Boolean) spatial.getUserData ("aktiv")) // vi bruker dette stedet senere ... annet // håndtere "aktiv" status = System.currentTimeMillis () - spawnTime; hvis (dif> = 1000f) spatial.setUserData ("active", true);  ColorRGBA color = new ColorRGBA (1,1,1, dif / 1000f); Node spatialNode = (Node) spatial; Bilde bilde = (Bilde) spatialNode.getChild ("Black Hole"); pic.getMaterial () SetColor ( "Farge", farge).;  @Override protected void controlRender (RenderManager rm, ViewPort vp)  offentlig tomgang wasShot () hitpoints--;  offentlige boolean isDead () return hitpoints <= 0;  

Vi er nesten ferdig med grunnkoden for de svarte hullene. Før vi kommer til å gjennomføre tyngdekraften, må vi ta vare på kollisjonene.

Når spilleren eller en fiende kommer for nær det svarte hullet, vil det dø. Men når en kule klarer å slå den, vil det sorte hullet miste et treffpunkt.

Ta en titt på følgende kode. Det tilhører handleCollisions (). Det er i utgangspunktet den samme som for alle de andre kollisjonene:

 // er det noe som kolliderer med et svart hull? for (i = 0; i 

Vel, du kan drepe det svarte hullet nå, men det er ikke den eneste gangen da det skal forsvinne. Når spilleren dør, forsvinner alle fiender og det skal også det svarte hullet. For å håndtere dette, legg bare til følgende linje til vår killPlayer () metode:

 blackHoleNode.detachAllChildren ();

Nå er det på tide å implementere de kule greiene. Vi lager en annen metode, handleGravity (float tpf). Bare ring det med de andre metodene i simplueUpdate (float tpf).

I denne metoden kontrollerer vi alle enheter (spillere, kuler og fiender) for å se om de nærmer seg et svart hull - la oss si innen 250 piksler - og hvis de er, bruker vi den riktige effekten:

 privat tomt håndtakGravity (float tpf) for (int i = 0; i 

For å sjekke om to enheter ligger innenfor en bestemt avstand fra hverandre, oppretter vi en metode som kalles isNearby () som sammenligner plasseringen av de to romlige områdene:

 privat booleansk isNearby (romlig a, romlig b, flyteavstand) Vector3f pos1 = a.getLocalTranslation (); Vector3f pos2 = b.getLocalTranslation (); returnere pos1.distanceSquared (pos2) <= distance * distance; 

Nå som vi har sjekket hver enhet, hvis den er aktiv og innenfor den angitte avstanden til et svart hull, kan vi endelig bruke effekten av tyngdekraften. For å gjøre det, bruker vi kontrollene: Vi lager en metode i hver kontroll, kalt applyGravity (Vector3f gravity).

La oss ta en titt på hver av dem:

PlayerControl:

 offentlig ugyldig applyGravity (Vector3f gravity) spatial.move (gravity); 

BulletControl:

 offentlig ugyldig applyGravity (Vector3f gravity) direction.addLocal (gravity); 

SeekerControl og WandererControl:

 offentlig ugyldig applyGravity (Vector3f gravity) hastighet.addLokal (tyngdekraften); 

Og nå tilbake til hovedklassen, MonkeyBlasterMain. Jeg vil gi deg metoden først og forklare trinnene under den:

 Privat ugyldigGravity (Spatial blackHole, Spatial target, float tpf) Vector3f differanse = blackHole.getLocalTranslation (). trekke seg (target.getLocalTranslation ()); Vector3f gravity = difference.normalize (). MultLocal (tpf); flyteavstand = difference.length (); hvis (target.getName (). equals ("Player")) gravity.multLocal (250f / avstand); target.getControl (PlayerControl.class) .applyGravity (gravity.mult (80f));  ellers hvis (target.getName (). likestiller ("Bullet")) gravity.multLocal (250f / avstand); target.getControl (BulletControl.class) .applyGravity (gravity.mult (-0.8f));  ellers hvis (target.getName (). equals ("Seeker")) target.getControl (SeekerControl.class) .applyGravity (gravity.mult (150000));  ellers hvis (target.getName (). equals ("Wanderer")) target.getControl (WandererControl.class) .applyGravity (gravity.mult (150000)); 

Det første vi gjør er å beregne Vector mellom det svarte hullet og målet. Deretter beregner vi gravitasjonskraften. Det viktige å merke seg er at vi - igjen - multipliserer kraften etter den tiden som har gått siden siste oppdatering, TPF, for å oppnå den samme effekten med hver bildefrekvens. Til slutt beregner vi avstanden mellom målet og det svarte hullet.

For hver type mål må vi bruke kraften på en litt annen måte. For spilleren og for kuler blir kraften sterkere jo nærmere de er til det svarte hullet:

 gravity.multLocal (250F / avstand);

Kuler må avstøtes; det er derfor vi multipliserer deres gravitasjonskraft med et negativt tall.

Søkere og vandrere får bare en kraft påført det er alltid det samme, uavhengig av avstanden fra det svarte hullet.

Vi er nå ferdige med implementeringen av de svarte hullene. Vi legger til noen kule effekter i de neste kapitlene, men for nå kan du teste det ut!

Tips: Merk at dette er din spill; gjerne endre eventuelle parametere du liker! Du kan endre effektområdet for det svarte hullet, fienderens eller spillerenes fart ... Disse tingene har stor effekt på spillingen. Noen ganger er det verdt å spille litt med verdiene.

The Head-Up Display

Det er litt informasjon som må spores og vises til spilleren. Det er hva HUD (Head-Up Display) er der for. Vi ønsker å spore spillernes liv, nåværende poengmultiplikator, og selvfølgelig score selv og vise alt dette til spilleren.

Når spilleren scorer 2000 poeng (eller 4.000, eller 6.000, eller ...), vil spilleren få et nytt liv. I tillegg vil vi lagre poengsummen etter hvert spill og sammenligne det med dagens highscore. Multiplikatoren øker hver gang spilleren dreper en fiende og hopper tilbake til en når spilleren ikke dreper noe om noen gang.

Vi lager en ny klasse for alt det som kalles Hud. I Hud vi har ganske mange ting å initialisere rett i begynnelsen:

 offentlig klasse Hud privat AssetManager assetManager; privat node guiNode; privat int skjermbredde, skjermhøyde; privat endelig int fontSize = 30; privat endelig int multiplierExpiryTime = 2000; privat endelig int maxMultiplier = 25; offentlige int liv; offentlig int score; offentlig int multiplikator; privat lang multiplikatorActivationTime; privat int scoreForExtraLife; privat BitmapFont guiFont; privat BitmapText livesText; privat BitmapText scoreText; privat BitmapText multiplierText; privat node gameOverNode; offentlig Hud (AssetManager assetManager, Node guiNode, int screenWidth, int screenHeight) this.assetManager = assetManager; this.guiNode = guiNode; this.screenWidth = screenWidth; this.screenHeight = screenHeight; setupText (); 

Det er ganske mange variabler, men de fleste er ganske selvforklarende. Vi må ha en referanse til Kapitalforvalter å laste inn tekst, til guiNode å legge den til scenen, og så videre.

Deretter er det noen få variabler vi trenger å spore kontinuerlig, som multiplikator, dets utløpstid, maksimalt mulig multiplikator og spillerens liv.

Og til slutt har vi noen BitmapText objekter, som lagrer den faktiske teksten og viser den på skjermen. Denne teksten er satt opp i metoden setupText (), som kalles på enden 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 (skrift); livesText.setText ("Liv:" + liv); guiNode.attachChild (livesText); scoreText = ny BitmapText (guiFont, true); scoreText.setLocalTranslation (screenWidth - 200, screenHeight-30,0); scoreText.setSize (skrift); scoreText.setText ("Score:" + score); guiNode.attachChild (scoreText); multiplierText = ny BitmapText (guiFont, true); multiplierText.setLocalTranslation (screenWidth-200, screenHeight-100,0); multiplierText.setSize (skrift); multiplikatorText.setText ("Multiplikator:" + liv); guiNode.attachChild (multiplierText); 

For å laste inn tekst må vi først laste inn skriftstedet. I vårt eksempel bruker vi en standard skrifttype som følger med jMonkeyEngine.

Tips: Selvfølgelig kan du lage dine egne skrifter, plassere dem et sted i eiendeler katalogfortrinn eiendeler / Interface-og last dem. Hvis du vil vite mer, sjekk ut denne veiledningen om lasting av fonter i jME.

Deretter trenger vi en metode for å tilbakestille alle verdiene slik at vi kan starte om spilleren dør for mange ganger:

 offentlig ugyldig tilbakestilling () score = 0; multiplikator = 1; lever = 4; multiplierActivationTime = System.currentTimeMillis (); scoreForExtraLife = 2000; updateHUD (); 

Tilbakestilling av verdiene er enkel, men vi må også bruke endringene av variablene til HUD. Vi gjør det i en egen metode:

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

Under kampen slår spilleren poeng og mister liv. Vi kaller disse metodene fra MonkeyBlasterMain:

 Offentlig tomt addPoints (int basePoints) score + = basePoints * multiplikator; hvis (score> = scoreForExtraLife) scoreForExtraLife + = 2000; lever ++;  increaseMultiplier (); updateHUD ();  privat void increaseMultiplier () multiplierActivationTime = System.currentTimeMillis (); hvis (multiplikator < maxMultiplier)  multiplier++;   public boolean removeLife()  if (lives == 0) return false; lives--; updateHUD(); return true; 

Merkbare konsepter i disse metodene er:

  • Når vi legger til poeng, kontrollerer vi om vi allerede har nådd den nødvendige poeng for å få et ekstra liv.
  • Når vi legger til poeng, må vi også øke multiplikatoren ved å ringe en egen metode.
  • Når vi øker multiplikatoren, må vi være oppmerksomme på den maksimale mulige multiplikator og ikke gå utover det.
  • Når spilleren treffer en fiende, må vi nullstille multiplierActivationTime.
  • Når spilleren ikke har noen liv igjen for å bli fjernet, kommer vi tilbake falsk slik at hovedklassen kan handle tilsvarende.

Det er to ting igjen som vi trenger å håndtere.

Først må vi nullstille multiplikatoren hvis spilleren ikke dreper noe for en stund. Vi implementerer en Oppdater() metode som sjekker om det er på tide å gjøre dette:

 offentlig ugyldig oppdatering () if (multiplikator> 1) if (System.currentTimeMillis () - multiplikatorActivationTime> multiplikatorExpiryTime) multiplikator = 1; multiplierActivationTime = System.currentTimeMillis (); updateHUD (); 

Det siste vi må ta vare på, er å avslutte spillet. Når spilleren har brukt opp alle sine liv, er spillet over, og sluttresultatet skal vises midt på skjermen. Vi må også sjekke om nåværende høy poengsum er lavere enn spillerens nåværende poengsum, og i så fall lagre nåværende poengsum som den nye høy poengsummen. (Merk at du må opprette en fil highscore.txt først, eller du vil ikke kunne laste inn en score.)

Slik avslutter vi spillet Hud:

 offentlig ugyldig endGame () // init gameOverNode gameOverNode = ny Node (); gameOverNode.setLocalTranslation (skjermbredde / 2 - 180, screenHeight / 2 + 100,0); guiNode.attachChild (gameOverNode); // sjekk highscore int highscore = loadHighscore (); hvis (score> highscore) saveHighscore (); // init og displaytekst BitmapText gameOverText = ny BitmapText (guiFont, false); gameOverText.setLocalTranslation (0,0,0); gameOverText.setSize (skrift); gameOverText.setText ("Spill over"); gameOverNode.attachChild (gameOverText); BitmapText yourScoreText = ny BitmapText (guiFont, false); yourScoreText.setLocalTranslation (0, -50,0); yourScoreText.setSize (skrift); yourScoreText.setText ("Ditt poeng:" + score); gameOverNode.attachChild (yourScoreText); BitmapText highscoreText = ny BitmapText (guiFont, false); highscoreText.setLocalTranslation (0, -100,0); highscoreText.setSize (skrift); highscoreText.setText ("Highscore:" + highscore); gameOverNode.attachChild (highscoreText); 

Til slutt trenger vi to siste metoder: loadHighscore () og saveHighscore ():

 privat int loadHighscore () prøv FileReader fileReader = ny FileReader (ny fil ("highscore.txt")); BufferedReader Reader = Ny BufferedReader (FileReader); String line = reader.readLine (); returnere Integer.valueOf (linje);  fangst (FileNotFoundException e) e.printStackTrace ();  fangst (IOException e) e.printStackTrace (); return 0;  privat void saveHighscore () prøv FileWriter writer = new FileWriter (ny fil ("highscore.txt"), false); writer.write (poengsum + System.getProperty ( "line.separator")); writer.close ();  fangst (IOException e) e.printStackTrace ();
Tips: Som du kanskje har lagt merke til, brukte jeg ikke kapitalforvalter å laste inn og lagre teksten. Vi brukte den til å laste alle lydene og grafikkene, og ordentlig jME måte å laste inn og lagre tekst faktisk bruker kapitalforvalter for det, men siden det ikke støtter tekstfilinnlasting på egenhånd, trenger vi å registrere en TextLoader med kapitalforvalter. Du kan gjøre det hvis du vil, men i denne opplæringen stakk jeg til standard Java-måte for lasting og lagring av tekst, for enkelhets skyld.

Nå har vi en stor klasse som vil håndtere alle våre HUD-relaterte problemer. Det eneste vi trenger å gjøre nå er å legge det til spillet.

Vi må deklarere objektet i starten:

 privat hud hud;

... initialiser den i simpleInitApp ():

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

... oppdater HUD i simpleUpdate (float tpf) (uansett om spilleren er i live):

 hud.update ();

... legg til poeng når spilleren treffer fiender (i checkCollisions ()):

 // legg til poeng avhengig av typen fiende hvis (enemyNode.getChild (i) .getName () .likes ("Seeker")) hud.addPoints (2);  ellers hvis (enemyNode.getChild (i) .getName (). equals ("Wanderer")) hud.addPoints (1); 
Pass på! Du må legge til poengene før du løsner fiender fra scenen, eller du får problemer med enemyNode.getChild (i).

... og fjern liv når spilleren dør (i killPlayer ()):

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

Du har kanskje lagt merke til at vi introduserte en ny variabel også, spillet er slutt. Vi stiller den til falsk i begynnelsen:

 privat booleansk spillOver = false;

Spilleren skal ikke gyte mer når spillet er over, så vi legger til denne tilstanden til simpleUpdate (float tpf)

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

Nå kan du starte spillet og sjekke om du har gått glipp av noe! Og spillet ditt har fått et nytt mål: å slå highscore. Jeg ønsker deg lykke til!

Tilpasset markør

Siden vi har et 2D-spill, er det en ting å legge til for å perfeksjonere våre HUD: en egendefinert musemarkør.
Det er ikke noe spesielt; bare sett inn denne linjen simpleInitApp ():

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

Konklusjon

Spillet er nå fullstendig ferdig. I de resterende to delene av denne serien legger vi til noen kule grafiske effekter. Dette vil faktisk gjøre spillet litt vanskeligere, siden fiender kanskje ikke er så enkle å få øye på noe mer!