Până acum, în această serie despre construirea unui joc inspirat de Geometry Wars în jMonkeyEngine, am implementat cea mai mare parte a gameplay-ului și a sunetului. În această parte, vom termina jocul prin adăugarea de găuri negre și vom adăuga un anumit UI pentru a afișa scorul jucătorilor.
Iată la ce lucrăm în întreaga serie:
... și iată ce vom avea până la sfârșitul acestei părți:
Pe lângă modificarea claselor existente, vom adăuga două noi:
BlackHoleControl
: Inutil de spus, acest lucru se va ocupa de comportamentul găurilor noastre negre.Hud
: Aici vom stoca și afișa scorurile jucătorilor, vieți și alte elemente ale UI.Să începem cu găurile negre.
Gaura neagra este unul dintre cei mai interesati dusmani din Geometry Wars. În MonkeyBlaster, clona noastră, este deosebit de interesantă odată ce adăugăm efectele particulelor și grila de deformare în următoarele două capitole.
Găurile negre vor trage în nava jucătorului, inamicii din apropiere și (după următorul tutorial) particule, dar vor respinge gloanțele.
Există multe funcții posibile pe care le putem folosi pentru atracție sau repulsie. Cea mai simplă este folosirea unei forțe constante, astfel încât gaura neagră să tragă cu aceeași forță indiferent de distanța obiectului. O altă opțiune este ca forța să crească liniar de la zero, la o anumită distanță maximă, până la putere maximă, pentru obiectele direct deasupra găurii negre. Și dacă vrem să modelăm gravitatea mai realist, putem folosi pătratul invers al distanței, ceea ce înseamnă că forța gravitației este proporțională cu 1 / (distanța * distanta)
.
De fapt, vom folosi fiecare dintre aceste trei funcții pentru a gestiona diferite obiecte. Gloanțele vor fi respinse cu forță constantă, dușmanii și nava jucătorului vor fi atrase cu o forță liniară, iar particulele vor folosi o funcție pătrată inversă.
Vom începe prin a da nastere găurilor noastre negre. Pentru a obține acest lucru avem nevoie de un alt varibale în MonkeyBlasterMain
:
private long spawnCooldownBlackHole;
Apoi trebuie să declare un nod pentru găurile negre; să spunem asta blackHoleNode
. Puteți să o declarați și să o inițializați exact așa cum am făcut-o enemyNode
în tutorialul anterior.
Vom crea, de asemenea, o nouă metodă, spawnBlackHoles
, pe care o numim imediat după spawnEnemies
în simpleUpdate (float tpf)
. Reproducerea reală este destul de asemănătoare cu cea a inamicilor:
privat void spawnBlackHoles () if (blackHoleNode.getQuantity () < 2) if (System.currentTimeMillis() - spawnCooldownBlackHole > 10f) spawnCooldownBlackHole = System.currentTimeMillis (); dacă (new Random (). nextInt (1000) == 0) createBlackHole ();
Crearea gaurii neagră urmează procedura standard, de asemenea:
void privat createBlackHole () Spatial blackHole = getSpatial ("Hole negru"); blackHole.setLocalTranslation (getSpawnPosition ()); blackHole.addControl (noul BlackHoleControl ()); blackHole.setUserData ( "active", false); blackHoleNode.attachChild (Blackhole);
Încă o dată, încărcăm spațiul, ne fixăm poziția, adăugăm un control, îl punem pe neactiv și, în final, îl atașăm la nodul corespunzător. Când te uiți la BlackHoleControl
, veți observa că nu este deloc diferită.
Vom implementa atracția și repulsia mai târziu, în MonkeyBlasterMain
, dar este un lucru pe care trebuie să-l abordăm acum. Deoarece gaura neagră este un inamic puternic, nu vrem să se ducă ușor. Prin urmare, adăugăm o variabilă, puncte de sănătate
, la BlackHoleControl
, și setați valoarea sa inițială la 10
ca să moară după zece lovituri.
clasa publică BlackHoleControl extinde AbstractControl private long spawnTime; private hitpoints int; public BlackHoleControl () spawnTime = System.currentTimeMillis (); hitpoints = 10; @Override protejat void controlUpdate (float tpf) if ((Boolean) spatial.getUserData ("activ")) // vom folosi acest loc mai tarziu ... altceva // manipulati " = System.currentTimeMillis () - spawnTime; dacă (dif> = 1000f) spatial.setUserData ("activ", adevărat); ColorRGBA culoare = nou ColorRGBA (1,1,1, dif / 1000f); Nod spatialNode = (Nod) spatial; Imagine pic = (Imagine) spatialNode.getChild ("Hole Negru"); pic.getMaterial () setcolor ( "Color", culoare).; @Override protejat void controlRender (RenderManager rm, ViewPort vp) void public wasShot () hitpoints--; booleanul public esteDead () returnează punctele de interes <= 0;
Aproape că am terminat codul de bază pentru găurile negre. Înainte de a ajunge la punerea în aplicare a gravității, trebuie să avem grijă de coliziuni.
Când jucătorul sau un inamic ajunge prea aproape de gaura neagră, va muri. Dar când un glonț reușește să-l lovească, gaura neagră va pierde un punct de lovitură.
Uitați-vă la următorul cod. Apartine handleCollisions ()
. Este practic acelasi lucru ca si in cazul celorlalte coliziuni:
/ / este ceva ce se ciocnește cu o gaură neagră? pentru (i = 0; iPăi, poți ucide acum gaura neagră, dar asta nu e singura dată când ar trebui să dispară. Ori de câte ori jucătorul moare, toți dușmanii dispar și așa ar trebui să fie și gaura neagră. Pentru a face acest lucru, trebuie doar să adăugați următoarea linie la adresa noastră
killPlayer ()
metodă:blackHoleNode.detachAllChildren ();Acum este timpul să pună în aplicare lucrurile interesante. Vom crea o altă metodă,
mânerGravity (float tpf)
. Doar apelați-l cu celelalte metode dinsimplueUpdate (float tpf)
.În această metodă, verificăm toate entitățile (jucători, gloanțe și dușmani) pentru a vedea dacă se află aproape de o gaură neagră - să zicem în limitele a 250 de pixeli - și, dacă sunt, aplicăm efectul adecvat:
void private voidGravity (float tpf) pentru (int i = 0; iPentru a verifica dacă două entități se află într-o anumită distanță unul de altul, vom crea o metodă numită
isNearby ()
care compară locațiile celor două spații:boolean privat esteNearby (Spațial a, Spațial b, distanța flotantă) Vector3f pos1 = a.getLocalTranslation (); Vector3f pos2 = b.getLocalTranslație (); retur pos1.distanceSquared (pos2) <= distance * distance;Acum, că am verificat fiecare entitate, dacă este activă și în distanța specificată a unei găuri negre, putem aplica în cele din urmă efectul gravitației. Pentru a face acest lucru, vom folosi controalele: vom crea o metodă în fiecare comandă, numită
applyGravity (Vector3f gravity)
.Să aruncăm o privire la fiecare dintre ele:
PlayerControl
:public void applyGravity (Vector3f gravitate) spatial.move (gravitate);
BulletControl
:public void applyGravity (Vector3f gravitate) direction.addLocal (gravitate);
SeekerControl
șiWandererControl
:public void applyGravity (Vector3f gravitate) velocity.addLocal (gravitate);Și acum înapoi la clasa principală,
MonkeyBlasterMain
. Vă dau mai întâi metoda și explicați pașii de dedesubt:private void applyGravity (Spatial blackHole, spațiu țintă, float tpf) Vector3f difference = blackHole.getLocalTranslation () scădea (target.getLocalTranslation ()); Vector3f gravitate = diferență.normalizare () multLocal (tpf); distanța flotant = diferența.untime (); dacă (target.getName () este egal ("Player")) gravity.multLocal (250f / distanță); target.getControl (PlayerControl.class) .applyGravity (gravity.mult (80f)); altfel dacă (target.getName () este egal ("Bullet")) gravity.multLocal (250f / distanță); target.getControl (BulletControl.class) .applyGravity (gravity.mult (-0.8f)); altfel dacă (target.getName () este egal ("Seeker")) target.getControl (SeekerControl.class) .applyGravity (gravity.mult (150000)); altfel dacă (target.getName () este egal ("Wanderer")) target.getControl (WandererControl.class) .applyGravity (gravity.mult (150000));Primul lucru pe care îl facem este să calculam
Vector
între gaura neagră și țintă. Apoi, calculam forța gravitațională. Este important să observăm că noi - din nou - înmulțim forța cu timpul trecut de la ultima actualizare,TPF
, pentru a obține același efect cu fiecare rată de cadre. În cele din urmă, se calculează distanța dintre țintă și gaura neagră.Pentru fiecare tip de țintă, trebuie să aplicăm forța într-un mod ușor diferit. Pentru jucător și pentru gloanțe, forța devine mai puternică cu cât sunt mai aproape de gaura neagră:
gravity.multLocal (250F / distanta);Gloanțele trebuie respinse; de aceea le multiplicăm forța gravitațională cu un număr negativ.
Căutătorii și Wanderers pur și simplu obține o forță aplicată thats totdeauna la fel, indiferent de distanța lor de la gaura neagră.
Acum suntem terminați cu implementarea găurilor negre. Vom adăuga câteva efecte interesante în următoarele capitole, dar deocamdată puteți să le testați!
Bacsis: Rețineți că aceasta este ta joc; nu ezitați să modificați parametrii doriți! Puteți schimba zona de efect pentru gaura neagră, viteza dușmanilor sau a jucătorului ... Aceste lucruri au un efect extraordinar asupra gameplay-ului. Uneori merită să jucăm un pic cu valorile.
Afișajul capului-sus
Există câteva informații care trebuie urmărite și afișate jucătorului. Acesta este ceea ce este pentru HUD (Head-Up Display). Vrem să urmărim viața jucătorilor, multiplicatorul actual al scorului și, bineînțeles, scorul în sine și să arătăm toate acestea jucătorului.
Când jucătorul obține scorul de 2.000 de puncte (sau 4.000, sau 6.000, sau ...), jucătorul va obține o altă viață. În plus, dorim să salvăm punctajul după fiecare meci și să îl comparăm cu scorul curent. Multiplicatorul crește de fiecare dată când jucătorul ucide un inamic și sare înapoi la unul atunci când jucătorul nu ucide nimic din timp.
Vom crea o nouă clasă pentru toate acestea, numite
Hud
. ÎnHud
avem foarte puține lucruri de inițializare chiar de la început:clasa publică Hud privat AssetManager assetManager; Nodul privat guiNode; privat ecran intro, screenHeight; finale private int fontSize = 30; ultimul int multiplicator finitExpiryTime = 2000; final int fin maxMultiplier = 25; viata int int national; scor public int; public multiplicator int; private multiplicator lungActivationTime; int private scoreForExtraLife; privat BitmapFont guiFont; privat BitmapText livesText; privat BitmapText scoreText; BitmapText privat multiplicatorText; Nodul privat gameOverNode; publicul Hud (AssetManager assetManager, nodul guiNode, int screenWidth, int screenHeight) this.assetManager = assetManager; this.guiNode = guiNode; this.screenWidth = ecranWidth; this.screenHeight = screenHeight; setupText ();Sunt destul de multe variabile, dar majoritatea sunt destul de explicative. Trebuie să avem o referință la
AssetManager
pentru a încărca text, laguiNode
pentru ao adăuga la scenă și așa mai departe.Apoi, există câteva variabile pe care trebuie să le urmărim continuu, cum ar fi
coeficient
, timpul de expirare, multiplicatorul maxim posibil și viața jucătorului.Și în sfârșit avem câteva
BitmapText
obiecte, care stochează textul real și îl afișează pe ecran. Acest text este configurat în metodăsetupText ()
, care se numește la sfârșitul constructorului.private void setupText () guiFont = assetManager.loadFont ("Interfață / Fonturi / Default.fnt"); livesText = BitmapText nou (guiFont, false); livesText.setLocalTranslation (30, screenHeight-30,0); livesText.setSize (FONTSIZE); livesText.setText ("Locuiește:" + vieți); guiNode.attachChild (livesText); scoreText = BitmapText nou (guiFont, true); scoreText.setLocalTranslation (ecranWidth - 200, screenHeight-30,0); scoreText.setSize (FONTSIZE); scoreText.setText ("Scor:" + scor); guiNode.attachChild (scoreText); multiplicerText = BitmapText nou (guiFont, true); multiplierText.setLocalTranslation (screenWidth-200, screenHeight-100,0); multiplierText.setSize (FONTSIZE); multiplierText.setText ("Multiplicator:" + vieți); guiNode.attachChild (multiplierText);Pentru a încărca textul, trebuie să încărcăm primul font. În exemplul nostru folosim un font implicit care vine cu jMonkeyEngine.
Bacsis: Desigur, puteți să creați propriile fonturi, să le plasați undeva în bunuri directory-preferabil active / interfață-și încărcați-le. Dacă doriți să aflați mai multe, consultați acest tutorial privind încărcarea fonturilor în jME.Apoi, vom avea nevoie de o metodă de resetare a tuturor valorilor, astfel încât să putem începe din nou dacă jucătorul moare de prea multe ori:
void reset public () scor = 0; multiplicator = 1; vieți = 4; multiplicatorActivationTime = System.currentTimeMillis (); scoreForExtraLife = 2000; updateHUD ();Resetarea valorilor este simplă, însă trebuie să aplicăm modificările variabilelor la HUD. Facem asta într-o metodă separată:
privat void updateHUD () livesText.setText ("Lives:" + vieți); scoreText.setText ("Scor:" + scor); multiplierText.setText ("Multiplicator:" + multiplicator);În timpul bătăliei, jucătorul câștigă puncte și pierde vieți. Vom numi aceste metode
MonkeyBlasterMain
:public void addPoints (int basePoints) scor + = basePoints * multiplicator; dacă (scorul> = scoreForExtraLife) scoreForExtraLife + = 2000; trăiește ++; increaseMultiplier (); updateHUD (); void private increaseMultiplier () multiplicerActivationTime = System.currentTimeMillis (); dacă (multiplicatorul < maxMultiplier) multiplier++; public boolean removeLife() if (lives == 0) return false; lives--; updateHUD(); return true;Concepte notabile în aceste metode sunt:
- Ori de câte ori adăugăm puncte, verificăm dacă am atins scorul necesar pentru a obține o viață suplimentară.
- Ori de câte ori adăugăm puncte, trebuie să creștem multiplicatorul apelând o metodă separată.
- Ori de câte ori creștem multiplicatorul, trebuie să fim conștienți de multiplicatorul maxim posibil și să nu depășim acest lucru.
- Ori de câte ori jucătorul lovește un inamic, trebuie să resetăm
multiplierActivationTime
.- Când jucătorul nu are nici o viață, trebuie să ne întoarcem
fals
astfel încât clasa principală să poată acționa în consecință.Există două lucruri pe care trebuie să le rezolvăm.
În primul rând, trebuie să resetăm multiplicatorul dacă jucătorul nu ucide nimic pentru o vreme. Vom implementa un
Actualizați()
care verifică dacă este timpul să faceți acest lucru:public void update () if (multiplicator> 1) if (System.currentTimeMillis () - multiplicatorActivationTime> multiplicatorExpiryTime) multiplicator = 1; multiplicatorActivationTime = System.currentTimeMillis (); updateHUD ();Ultimul lucru de care trebuie să avem grijă este încheierea jocului. Când jucătorul și-a epuizat toată viața, jocul se termină și scorul final va fi afișat în mijlocul ecranului. De asemenea, trebuie să verificăm dacă scorul curent ridicat este mai mic decât scorul actual al jucătorului și, dacă da, salvați scorul curent ca noul punctaj ridicat. (Rețineți că trebuie să creați un fișier highscore.txt în primul rând, sau nu veți putea încărca un scor.)
Așa terminăm jocul
Hud
:void public endGame () / / init gameOverNode gameOverNode = nou Nod (); gameOverNode.setLocalTranslation (ecranWidth / 2 - 180, screenHeight / 2 + 100,0); guiNode.attachChild (gameOverNode); // verificați highscore int highscore = loadHighscore (); dacă (scor> scoruri mari) saveHighscore (); // init și afișare text BitmapText gameOverText = BitmapText nou (guiFont, false); gameOverText.setLocalTranslation (0,0,0); gameOverText.setSize (FONTSIZE); gameOverText.setText ("Game Over"); gameOverNode.attachChild (gameOverText); BitmapText yourScoreText = BitmapText nou (guiFont, false); yourScoreText.setLocalTranslation (0, -50,0); yourScoreText.setSize (FONTSIZE); yourScoreText.setText ("Scorul dvs.:" + scor); gameOverNode.attachChild (yourScoreText); BitmapText highscoreText = BitmapText nou (guiFont, fals); highscoreText.setLocalTranslation (0, -100,0); highscoreText.setSize (FONTSIZE); highscoreText.setText ("Highscore:" + highscore); gameOverNode.attachChild (highscoreText);În cele din urmă, avem nevoie de două metode ultime:
loadHighscore ()
șisaveHighscore ()
:private int loadHighscore () încercați FileReader fileReader = noul FileReader (fișier nou ("highscore.txt")); BufferedReader cititor = nou BufferedReader (fileReader); Linia de linie = reader.readLine (); returnați Integer.valueOf (linie); captură (FileNotFoundException e) e.printStackTrace (); captură (IOException e) e.printStackTrace (); return 0; private void saveHighscore () încercați FileWriter writer = nou FileWriter (fișier nou ("highscore.txt"), false); writer.write (scor + System.getProperty ( "line.separator")); writer.close (); captură (IOException e) e.printStackTrace ();Bacsis: Așa cum ați observat, nu am folosit-oassetManager
pentru a încărca și a salva textul. Am folosit-o pentru a încărca toate sunetele și grafica, și propriu-zis Modul jME pentru încărcarea și salvarea textelor utilizeazăassetManager
pentru el, dar deoarece nu suportă încărcarea fișierului text pe cont propriu, ar trebui să ne înregistrăm aTextLoader
cuassetManager
. Puteți face acest lucru dacă doriți, dar în acest tutorial am rămas la modul Java implicit de încărcare și salvare a textului, de dragul simplității.Acum avem o clasă mare care se va ocupa de toate problemele legate de HUD. Singurul lucru pe care trebuie să-l facem acum este să-l adăugăm la joc.
Trebuie să declarăm obiectul la început:
privat Hud hud;... inițializați-o
simpleInitApp ()
:hud = noul Hud (assetManager, guiNode, settings.getWidth (), settings.getHeight ()); hud.reset ();... actualizați HUD în
simpleUpdate (float tpf)
(indiferent dacă jucătorul este în viață):hud.update ();... adăugați puncte atunci când jucătorul lovește inamicii (în
checkCollisions ()
):// adăuga puncte în funcție de tipul de inamic dacă (enemyNode.getChild (i) .getName () este egal ("Seeker")) hud.addPoints (2); altfel dacă (enemyNode.getChild (i) .getName () este egal ("Wanderer")) hud.addPoints (1);Ai grija! Trebuie să adăugați punctele inainte de vă detașați dușmanii de pe scenă sau vă veți confrunta cu problemeenemyNode.getChild (i)
.... și să elimine vieți când moare jucătorul (în
killPlayer ()
):dacă (! hud.removeLife ()) hud.endGame (); gameOver = adevărat;S-ar putea să fi observat că am introdus și o nouă variabilă,
joc încheiat
. Vom pune la dispozițiefals
la inceput:privat boolean gameOver = false;Jucătorul nu ar trebui să se mai hrănească după terminarea jocului, așa că adăugăm această condiție
simpleUpdate (float tpf)
altfel dacă (System.currentTimeMillis () - (Long) player.getUserData ("dieTime")> 4000f &&! gameOver)Acum puteți începe jocul și verificați dacă ați pierdut ceva! Și jocul tău are un nou scop: să batăm scorurile. iti urez noroc!
Cursor personalizat
Deoarece avem un joc 2D, există încă un lucru pe care să-l adăugăm pentru a ne perfecționa HUD-ul: un cursor personalizat al mouse-ului.
Nu este nimic special; introduceți doar această linie însimpleInitApp ()
:inputManager.setMouseCursor ((JmeCursor) activManager.loadAsset ("Texturi / Pointer.ico"));
Concluzie
Modul de joc este complet terminat. În celelalte două părți ale seriei, vom adăuga câteva efecte grafice grafice. Acest lucru va face jocul ușor mai greu, deoarece dușmanii nu pot fi la fel de ușor de observat!