Î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.
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.
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.
Î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.
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.
1
în codul nostru). Nu vrem un duș galben și un roșu?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.
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.
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; iNoi 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 revineAdevă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ţă
lafals
și să creați o nouă dată de utilizatordieTime
. (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ţă
laAdevă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; iAici, î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 ajutorulassetManager
. 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 laAdevărat
pentru muzică și pentrufals
pentru orice alt sunet.Redarea sunetelor este destul de simplă: sunăm doar
Info: Când pur și simplu sunațisoundX.play ()
.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 ceplayInstance ()
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) șiicre()
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.