Faceți un shooter vector neon în jMonkeyEngine dușmani și sunete

În prima parte a acestei serii despre construirea unui joc inspirat de Geometry Wars în jMonkeyEngine, am implementat nava jucătorului și l-am lăsat să se miște și să tragă. De data aceasta, vom adăuga dușmani și efecte sonore.


Prezentare generală

Iată la ce lucrăm în întreaga serie:


... și iată ce vom avea până la sfârșitul acestei părți:


Vom avea nevoie de câteva clase noi pentru a implementa noile caracteristici:

  • SeekerControl: Aceasta este o clasă de comportament pentru inamicul căutător.
  • WandererControl: Aceasta este și o clasă de comportament, de data aceasta pentru inamicul rătăcitor.
  • Sunet: Vom gestiona încărcarea și redarea de efecte sonore și muzică cu acest lucru.

Așa cum ați fi ghicit, vom adăuga două tipuri de dușmani. Primul este numit a căutător; acesta va urmări în mod activ jucătorul până când acesta moare. Cealaltă, rătăcitor, doar roams în jurul ecranului într-un model aleatoare.


Adăugarea dușmanilor

Vom sparge dușmanii în poziții aleatorii de pe ecran. Pentru a oferi jucătorului un timp pentru a reacționa, inamicul nu va fi activ imediat, ci va încetini încet. După ce a dispărut complet, va începe să se miște în lume. Când se ciocnește cu playerul, jucătorul moare; când se ciocnește cu un glonț, se moare.

Îndrăgostiți de reproducere

În primul rând, trebuie să creăm câteva noi variabile în MonkeyBlasterMain clasă:

 inamic privat lungSpawnCooldown; inamicul plutitor privatSpawnChance = 80; nodul privat enemyNode;

Vom folosi primele două destul de repede. Înainte de aceasta, trebuie să inițializăm enemyNode în simpleInitApp ():

 // configura inamicul inamic inamic = nou Nod ("inamici"); guiNode.attachChild (enemyNode);

Bine, acum la codul adevărat de reproducere: vom suprascrie simpleUpdate (float tpf). Această metodă este chemată de motor de peste și peste din nou, și pur și simplu păstrează de asteptare funcția de reproducere inamicul atâta timp cât jucătorul este în viață. (Am setat deja userdata în viaţă la Adevărat în ultimul tutorial.)

 @Override public void simpleUpdate (float tpf) dacă ((Boolean) player.getUserData ("viu")) spawnEnemies (); 

Iată cum ne dăm de fapt pe inamici:

 private void spawnEnemies () dacă (System.currentTimeMillis () - enemySpawnCooldown> = 17) enemySpawnCooldown = System.currentTimeMillis (); dacă (enemyNode.getQuantity () < 50)  if (new Random().nextInt((int) enemySpawnChance) == 0)  createSeeker();  if (new Random().nextInt((int) enemySpawnChance) == 0)  createWanderer();   //increase Spawn Time if (enemySpawnChance >= 1.1f) enemySpawnChance - = 0.005f; 

Nu te confunda cu enemySpawnCooldown variabil. Nu este acolo pentru a face inamicii să se dezvolte la o frecvență decentă - 17ms ar fi mult prea scurt de un interval.

enemySpawnCooldown este de fapt acolo pentru a se asigura că cantitatea de inamici noi este aceeași pe fiecare mașină. Pe calculatoare mai rapide, simpleUpdate (float tpf) se numește mult mai des decât pe cele mai lente. Cu această variabilă verificăm la fiecare 17 ms dacă ar trebui să aruncăm pe noi inamici.
Dar vrem să le ardem la fiecare 17ms? Vrem ca ei să se înmulțească la intervale aleatorii, așa că introducem un dacă afirmație:

 dacă (new Random (). nextInt ((int) enemySpawnChance) == 0) 

Cu cât este mai mică valoarea enemySpawnChance, cu atât este mai probabil ca un nou inamic să apară în acest interval de 17 ms, deci cu atât mai mulți dușmani cu care va trebui să se ocupe. De aceea scădem puțin enemySpawnChance fiecare tick: înseamnă că jocul va deveni mai greu în timp.

Crearea căutătorilor și a celor rătăcitori este similară cu crearea oricărui alt obiect:

 void privat createSeeker () Căutător spațial = getSpatial ("Seeker"); seeker.setLocalTranslation (getSpawnPosition ()); seeker.addControl (noul SeekerControl (player)); seeker.setUserData ( "active", false); enemyNode.attachChild (solicitant);  void privat createWanderer () Spatial wanderer = getSpatial ("Wanderer"); wanderer.setLocalTranslation (getSpawnPosition ()); wanderer.addControl (noul WandererControl ()); wanderer.setUserData ( "active", false); enemyNode.attachChild (rătăcitor); 

Creăm spațiul, îl mutăm, adăugăm un control personalizat, l-am setat neactiv, și îl atașăm enemyNode. Ce? De ce nu este activ? Asta pentru ca nu vrem ca inamicul sa inceapa sa il urmareasca pe jucator de indata ce incepe; vrem să oferim jucătorului ceva timp să reacționeze.

Înainte de a intra în controale, trebuie să implementăm metoda getSpawnPosition (). Inamicul ar trebui să provoace la întâmplare, dar nu chiar lângă jucator:

 privat vector3f getSpawnPosition () Vector3f pos; do pos = new Vector3f (new Random (), următorulInt (settings.getWidth ()), new Random () nextInt (settings.getHeight ());  în timp ce (pos.distanceSquared (player.getLocalTranslation ()) < 8000); return pos; 

Calculăm o nouă poziție aleatorie pos. Dacă este prea aproape de jucător, noi calculam o nouă poziție și repetăm ​​până când este o distanță decentă.

Acum trebuie doar să-i facem pe inamici să se activeze și să se miște. Vom face asta în controalele lor.

Controlarea comportamentului inamicului

Ne vom ocupa de SeekerControl primul:

 clasa publică SeekerControl extinde AbstractControl player privat Spatial; Viteza Vector3f privată; private long spawnTime; public SeekerControl (jucator spatial) this.player = jucator; velocity = new Vector3f (0,0,0); spawnTime = System.currentTimeMillis ();  @Override protejat void controlUpdate (float tpf) if ((Boolean) spatial.getUserData ("activ")) // traduce cautatorul Vector3f playerDirection = player.getLocalTranslation (). playerDirection.normalizeLocal (); playerDirection.multLocal (1000F); velocity.addLocal (playerDirection); velocity.multLocal (0.8f); spatial.move (velocity.mult (TPF * 0.1f)); // rotiți căutătorul dacă (velocity! = Vector3f.ZERO) spatial.rotateUpTo (viteza.normalizare ()); spatial.rotate (0,0, FastMath.PI / 2f);  altfel // manipulați "active" -status long dif = 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 ("Seeker"); pic.getMaterial () setcolor ( "Color", culoare).;  @Override protejat void controlRender (RenderManager rm, ViewPort vp) 

Să ne concentrăm controlUpdate (float tpf):

În primul rând, trebuie să verificăm dacă inamicul este activ. Dacă nu este, trebuie să-l dispari încet.
Apoi verificăm timpul care a trecut de când am dat naștere inamicului și, dacă este suficient de lung, l-am pus activ.

Indiferent dacă tocmai l-am activat, trebuie să-i ajustăm culoarea. Variabila locală spațial conține spațiul pe care a fost atașat controlul, dar ați putea să vă amintiți că nu am atașat controlul imaginii reale - imaginea este un copil al nodului la care am atașat controlul. (Dacă nu știți despre ce vorbesc, aruncați o privire asupra metodei getSpatial (Nume șir) am implementat ultimul tutorial.)

Asa de; avem imaginea de copil spațial, obțineți materialul și setați culoarea la valoarea corespunzătoare. Nimic deosebit odată ce sunteți obișnuit cu spațiul, materialele și nodurile.

Info: S-ar putea să vă întrebați de ce am setat culoarea materialului la alb. (Valorile RGB sunt toate 1 în codul nostru). Nu vrem un duș galben și un roșu?
Este pentru ca materialul amesteca culoarea materialului cu culorile texturii, asa ca daca dorim sa afisem textura inamicului ca atare, trebuie sa il amestecam cu alb.

Acum trebuie să aruncăm o privire la ceea ce facem atunci când inamicul este activ. Acest control este numit SeekerControl pentru un motiv: vrem ca dușmanii cu acest control să fie atașați de următorul jucător.

Pentru a realiza acest lucru, calculam direcția de la căutător la player și adăugăm această valoare la viteză. După aceea, diminuăm viteza cu 80%, astfel încât să nu se poată dezvolta infinit și să se miște în mod corespunzător solicitantul.

Rotația nu este nimic special: dacă căutătorul nu este în picioare, îl rotim în direcția jucătorului. Apoi îl rotim puțin mai mult, deoarece căutătorul în Seeker.png nu este orientat în sus, ci în dreapta.

Info: rotateUpTo (direcția Vector3f) Metodă de spațial rotește un spațiu astfel încât axa lui y să se situeze în direcția dată.

Așa a fost primul dușman. Codul celui de-al doilea dușman, cel rătăcitor, nu este foarte diferit:

 clasa publică WandererControl extinde AbstractControl private int screenWidth, screenHeight; Viteza Vector3f privată; direcția float privatăAngle; private long spawnTime; publicul WandererControl (int screenWidth, int screenHeight) this.screenWidth = ecranWidth; this.screenHeight = screenHeight; velocitate = nou Vector3f (); direcțieAngle = nou Random (). nextFloat () * FastMath.PI * 2f; spawnTime = System.currentTimeMillis ();  @Override protejat void controlUpdate (float tpf) if ((Boolean) spatial.getUserData ("activ")) // traduce wanderer // schimba directiaAngle bit directionAngle + = (new Random * 20f - 10f) * tpf; System.out.println (directionAngle); Vector3f direcțieVector = MonkeyBlasterMain.getVectorFromAngle (direcțiaAngle); directionVector.multLocal (1000F); velocity.addLocal (directionVector); // micșorați viteza un pic și deplasați viteza de deplasare velocity.multLocal (0,8f); spatial.move (velocity.mult (TPF * 0.1f)); // a face ca bobocul să sări de pe marginea ecranului Vector3f loc = spatial.getLocalTranslation (); dacă (loc.x screenWidth || loc.y> screenHeight) Vector3f newDirectionVector = nou Vector3f (screenWidth / 2, screenHeight / 2,0) .subtract (loc); direcțieAngle = MonkeyBlasterMain.getAngleFromVector (newDirectionVector);  // rotiți spatiul.rotate (0,0, tpf * 2);  altfel // manipulați "active" -status lung dif = 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 ("Wanderer"); pic.getMaterial () setcolor ( "Color", culoare).;  @Override protejat void controlRender (RenderManager rm, ViewPort vp) 

Cele mai ușoare lucruri: eliminarea dușmanului este aceeași ca în controlul căutătorului. În constructor, alegem o direcție aleatorie pentru călător, în care va zbura odată activată.

Bacsis: Dacă aveți mai mult de doi dușmani sau pur și simplu doriți să structurați mai curând jocul, puteți adăuga un al treilea control: EnemyControl S-ar ocupa de tot ce aveau în comun toți dușmanii: mișcarea dușmanului, estomparea lui, stabilirea lui activă ...

Acum la diferențele majore:

Atunci când dușmanul este activ, mai întâi ne schimbăm puțin direcția, astfel încât rătăcitorul să nu se miște în linie dreaptă tot timpul. Noi facem asta prin schimbarea noastră directionAngle un pic și adăugarea directionVector la viteză. Apoi aplicăm viteza la fel ca și noi SeekerControl.

Trebuie să verificăm dacă rătăcitorul se află în afara marginilor ecranului și, dacă da, schimbăm directionAngle la o direcție mai adecvată, astfel încât să fie aplicată în următoarea actualizare.

În cele din urmă, rătăcește un rătăcitor. Acest lucru este doar pentru că un dușman spinning pare mai rece.

Acum că am terminat implementarea celor doi dușmani, puteți începe jocul și puteți juca un pic. Vă dă o mică privire la modul în care va juca jocul, chiar dacă nu puteți ucide dușmanii și nici nu vă pot ucide. Să adăugăm următorul lucru.

Detectarea coliziunii

Pentru a face inamici să omoare jucătorul, trebuie să știm dacă se ciocnesc. Pentru aceasta, vom adăuga o nouă metodă, handleCollisions, a sunat simpleUpdate (float tpf):

 @Override public void simpleUpdate (float tpf) dacă ((Boolean) player.getUserData ("viu")) spawnEnemies (); handleCollisions (); 

Și acum metoda reală:

 private void handleCollisions () // ar trebui ca jucătorul să moară? pentru (int i = 0; i 

Noi iterăm prin toți dușmanii prin obținerea cantității copiilor nodului și apoi prin luarea fiecăruia dintre ei. În plus, trebuie doar să verificăm dacă inamicul ucide jucătorul când adversarul este de fapt activ. Dacă nu, nu trebuie să ne pasă de asta. Deci, dacă este activ, verificăm când jucătorul și dușmanul se ciocnesc. Facem asta printr-o altă metodă, checkCollisoin (Spațial a, Spațial b):

 privat boolean checkCollision (Spatial a, Spatial b) distanta float = a.getLocalTranslation () distanta (b.getLocalTranslation ()); float maxDistance = (float) a.getUserData ("rază") + (float) b.getUserData ("rază"); distanța de retur <= maxDistance; 

Conceptul este destul de simplu: în primul rând, se calculează distanța dintre cele două spații. Apoi, trebuie să știm cât de aproape trebuie să fie cele două spații pentru a fi considerate ca având o coliziune, astfel încât să obținem raza fiecărui spațiu și să le adăugăm. (Am setat datele de utilizator "radius" în getSpatial (Nume șir) în tutorialul anterior). Deci, dacă distanța efectivă este mai mică sau egală cu această distanță maximă, metoda revine Adevărat, ceea ce înseamnă că s-au ciocnit.

Ce acum? Trebuie să ucidem jucătorul. Să creați o altă metodă:

 private void killPlayer () player.removeFromParent (); player.getControl (PlayerControl.class) .reset (); player.setUserData ("viu", fals); player.setUserData ("dieTime", System.currentTimeMillis ()); enemyNode.detachAllChildren (); 

Mai întâi, detașăm playerul de nodul părinte, care îl elimină automat din scenă. Apoi, trebuie să resetăm mișcarea în PlayerControl-în caz contrar, jucătorul s-ar putea mișca în continuare când se va reproduce din nou.

Apoi stabilim userdata în viaţă la fals și să creați o nouă dată de utilizator dieTime. (Avem nevoie de asta pentru a respawn jucătorul atunci când este mort.)

În cele din urmă, detașăm toți dușmanii, deoarece jucătorul ar fi avut un timp dificil de a lupta împotriva dușmanilor deja existenți în afara dreptului atunci când se produce.

Am menționat deja respawning, așa că să ne ocupăm de asta. Vom modifica din nou simpleUpdate (float tpf) metodă:

 @Override public void simpleUpdate (float tpf) dacă ((Boolean) player.getUserData ("viu")) spawnEnemies (); handleCollisions ();  altfel dacă (System.currentTimeMillis () - (Long) player.getUserData ("dieTime")> 4000f &&! jocOver) // player player.setLocalTranslation (500,500,0); guiNode.attachChild (jucator); player.setUserData ( "viu", adevărat); 

Deci, dacă jucătorul nu este în viață și a murit suficient de mult, am stabilit poziția sa în mijlocul ecranului, l-am adăugat la scenă și, în final, am setat userdata în viaţă la Adevărat din nou!

Acum poate fi un moment bun pentru a începe jocul și pentru a testa noile noastre caracteristici. Veți avea o perioadă dificilă de durată mai mare de douăzeci de secunde, pentru că arma ta nu are valoare, deci hai să facem ceva despre asta.

În scopul de a face gloanțe ucide dușmani, vom adăuga un cod la handleCollisions () metodă:

 // ar trebui să moară un dușman? int i = 0; in timp ce eu < enemyNode.getQuantity())  int j=0; while (j < bulletNode.getQuantity())  if (checkCollision(enemyNode.getChild(i),bulletNode.getChild(j)))  enemyNode.detachChildAt(i); bulletNode.detachChildAt(j); break;  j++;  i++; 

Procedura de ucidere a inamicilor este aproape identică cu cea a uciderii jucătorului; vom repeta prin toți dușmanii și toate gloanțele, verificăm dacă se ciocnesc și, dacă se întâmplă, le detașăm pe amândouă.

Acum rulați jocul și vedeți cât de departe ajungeți!

Info: Iterând prin fiecare dușman și comparându-i poziția cu poziția fiecărui glonț este o modalitate foarte proastă de a verifica coliziunile. Este în regulă în acest exemplu, de dragul simplității, dar în a real ar trebui să implementați algoritmi mai buni pentru a face acest lucru, cum ar fi detectarea coliziunii quadtree. Din fericire, jMonkeyEngine utilizează motorul de fizică Bullet, deci ori de câte ori aveți complicat fizica 3D, nu trebuie să vă faceți griji în legătură cu acest lucru.

Acum am terminat cu modul de joc principal. Încă vom implementa găuri negre și vom afișa scorul și viețile jucătorului, iar pentru a face jocul mai distractiv și mai interesant, vom adăuga efecte sonore și o grafică mai bună. Acestea din urmă vor fi realizate prin filtrul de post-procesare a bloomului, unele efecte de particule și un efect de fund rece.

Înainte de a considera că această parte a seriei a terminat, vom adăuga un efect audio și efectul de inflorire.


Redarea sunetelor și a muzicii

Pentru a obține ceva audio în jocul nostru vom crea o nouă clasă, pur și simplu numită Sunet:

 clase publice de sunet audio privat AudioNode; cadre audio private []; exploziile audio private []; AudioNode privat [] spawns; AssetManager AssetManager privat; sunet public (AssetManager assetManager) this.assetManager = assetManager; fotografii = noul AudioNode [4]; explozii = noul AudioNode [8]; spawns = noul AudioNode [8]; loadSounds ();  private void loadSounds () muzică = noul AudioNode (assetManager, "Sounds / Music.ogg"); music.setPositional (false); music.setReverbEnabled (false); music.setLooping (true); pentru (int i = 0; i 

Aici, începem prin stabilirea necesarului AudioNode variabile și să inițializeze matricele.

Apoi, încărcăm sunetele și pentru fiecare sunet facem cam același lucru. Noi creăm un nou AudioNode, cu ajutorul assetManager. Apoi, nu l-am stabilit pozitiv și nu am reverb. (Nu avem nevoie ca sunetul să fie pozițional deoarece nu avem o ieșire stereo în jocul nostru 2D, deși ați putea să îl implementați dacă v-ați plăcut.) Dezactivarea reverbului face ca sunetul să fie redat la fel ca în cazul sunetului real fişier; dacă l-am activa, am putea face jME să lase sunetul audio ca și cum am fi într-o peșteră sau în temniță, de exemplu. După aceea, am setat buclele la Adevărat pentru muzică și pentru fals pentru orice alt sunet.

Redarea sunetelor este destul de simplă: sunăm doar soundX.play ().

Info: Când pur și simplu sunați Joaca() pe un sunet, doar cântă sunetul. Dar, uneori, dorim să jucăm același sunet de două ori sau chiar mai multe ori simultan. Asta e ceea ce playInstance () există pentru: creează o nouă instanță pentru fiecare sunet, astfel încât să putem reda același sunet de mai multe ori în același timp.

Voi lăsa restul lucrării până la tine: trebuie să sunați startMusic, trage(), explozie() (pentru dușmanii morți) și icre() la locurile potrivite din clasa principală MonkeyBlasterMain ().

Când ați terminat, veți vedea că jocul este acum mult mai distractiv; cele câteva efecte sonore adaugă într-adevăr atmosfera. Dar să lăsăm puțin și grafica.


Adăugarea filtrului post-procesare Bloom

Activarea bloomului este foarte simplă în jMonkeyEngine, deoarece toate codurile și shaderele necesare sunt deja implementate pentru dvs. Doar continuați și lipiți aceste linii simpleInitApp ():

 FilterPostProcessor fpp = noul FilterPostProcessor (assetManager); BloomFilter floare = nou BloomFilter (); bloom.setBloomIntensity (2f); bloom.setExposurePower (2); bloom.setExposureCutOff (0f); bloom.setBlurScale (1.5f); fpp.addFilter (floare); guiViewPort.addProcessor (fpp); guiViewPort.setClearColor (true);

Am configurat BloomFilter putin; dacă doriți să aflați ce sunt toate aceste setări, ar trebui să consultați tutorialul jME înflorit.


Concluzie

Felicitări pentru finalizarea celei de-a doua părți. Există încă trei părți pentru a merge, așa că nu te distra de joc pentru prea mult timp! Data viitoare vom adăuga GUI și găurile negre.