Lag en Neon Vector Shooter i jMonkeyEngine Enemies and Sounds

I den første delen av denne serien om å bygge et Geometry Wars-inspirert spill i jMonkeyEngine, implementerte vi spillerens skip og la det bevege seg og skyte. Denne gangen legger vi til fiender og lydeffekter.


Oversikt

Her jobber vi over hele serien:


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


Vi trenger noen nye klasser for å kunne implementere de nye funksjonene:

  • SeekerControl: Dette er en oppførselsklasse for den søkende fienden.
  • WandererControl: Dette er også en oppførselsklasse, denne gangen for vandreren fienden.
  • Lyd: Vi klarer å laste inn og spille av lydeffekter og musikk med dette.

Som du kanskje har gjettet, legger vi til to typer fiender. Den første er kalt a seeker; Det vil aktivt jage spilleren til den dør. Den andre, den vandreren, bare streife rundt skjermen i et tilfeldig mønster.


Legge til fienden

Vi vil gi fiender til tilfeldige stillinger på skjermen. For å gi spilleren litt tid til å reagere, vil fienden ikke være aktiv umiddelbart, men vil forsvinne sakte. Etter at den har bleknet helt, begynner den å bevege seg gjennom verden. Når den kolliderer med spilleren, dør spilleren; når den kolliderer med en kulde, dør den selv.

Spawning Fiender

Først av alt, må vi opprette noen nye variabler i MonkeyBlasterMain klasse:

 privat lang fiendeSpawnCooldown; privat float enemySpawnChance = 80; privat node enemyNode;

Vi kommer snart til å bruke de to første. Før det må vi initialisere enemyNode i simpleInitApp ():

 // sette opp enemyNode enemyNode = ny knutepunkt ("fiender"); guiNode.attachChild (enemyNode);

Ok, nå til den virkelige gytekoden: Vi overstyrer simpleUpdate (float tpf). Denne metoden blir kalt av motoren igjen og igjen, og fortsetter å ringe til fiendens gytefunksjon så lenge spilleren er i live. (Vi har allerede satt userdataene i live til ekte i den siste opplæringen.)

 @Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("live")) spawnEnemies (); 

Og slik får vi faktisk fiender:

 private void spawnEnemies () if (System.currentTimeMillis () - enemySpawnCooldown> = 17) enemySpawnCooldown = System.currentTimeMillis (); hvis (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; 

Ikke bli forvirret av enemySpawnCooldown variabel. Det er ikke der for å få fiender til å leke på en anstendig frekvens. 17 ms ville være mye for kort av et intervall.

enemySpawnCooldown er faktisk der for å sikre at mengden nye fiender er den samme på hver maskin. På raskere datamaskiner, simpleUpdate (float tpf) blir kalt mye oftere enn på langsommere. Med denne variabelen ser vi om hvert 17m om vi skal gyte nye fiender.
Men vil vi gyte dem hver 17m? Vi ønsker faktisk at de skal gyte i tilfeldige intervaller, så vi introduserer en hvis uttalelse:

 hvis (nytt tilfeldig (). nextInt ((int) enemySpawnChance) == 0) 

Jo mindre verdien av enemySpawnChance, jo mer sannsynlig er det at en ny fiende vil gyte i dette 17ms intervallet, og så jo flere fiender spilleren må håndtere. Derfor trekker vi litt av enemySpawnChance hvert kryss: det betyr at spillet blir vanskeligere over tid.

Opprette søkere og vandrere ligner på å skape et annet objekt:

 privat tomrom createSeeker () Spatial seeker = getSpatial ("Søker"); seeker.setLocalTranslation (getSpawnPosition ()); seeker.addControl (ny SeekerControl (spiller)); seeker.setUserData ( "aktiv", false); enemyNode.attachChild (søkeren);  privat tomt createWanderer () Spatial wanderer = getSpatial ("Wanderer"); wanderer.setLocalTranslation (getSpawnPosition ()); wanderer.addControl (ny WandererControl ()); wanderer.setUserData ( "aktiv", false); enemyNode.attachChild (vandrer); 

Vi lager det romlige, vi flytter det, vi legger til en egendefinert kontroll, vi setter den ikke aktiv, og vi legger den til vår enemyNode. Hva? Hvorfor ikke-aktiv? Det er fordi vi ikke vil at fienden skal begynne å jage spilleren så snart den går, Vi ønsker å gi spilleren litt tid til å reagere.

Før vi kommer inn i kontrollene, må vi implementere metoden getSpawnPosition (). Fienden skal gyte tilfeldig, men ikke rett ved siden av spilleren:

 privat Vector3f getSpawnPosition () Vector3f pos; gjør pos = ny Vector3f (new Random (). nextInt (settings.getWidth ()), ny tilfeldig (). nextInt (settings.getHeight ()), 0);  mens (pos.distanceSquared (player.getLocalTranslation ()) < 8000); return pos; 

Vi beregner en ny tilfeldig posisjon pos. Hvis det er for nært til spilleren, beregner vi en ny posisjon, og gjenta til det er en anstendig avstand unna.

Nå må vi bare gjøre fienderne aktive og begynne å bevege seg. Vi gjør det i sine kontroller.

Kontrollerer fiendens oppførsel

Vi skal håndtere SeekerControl først:

 offentlig klasse SeekerControl utvider AbstractControl private Spatial player; privat Vector3f hastighet; privat lang spawntime; offentlig SeekerControl (Spatial Player) this.player = player; hastighet = ny vektor3f (0,0,0); spawnTime = System.currentTimeMillis ();  @Override protected void controlUpdate (float tpf) hvis ((Boolean) spatial.getUserData ("aktiv")) // oversette søkeren Vector3f playerDirection = player.getLocalTranslation (). Trekke fra (spatial.getLocalTranslation ()); playerDirection.normalizeLocal (); playerDirection.multLocal (1000f); velocity.addLocal (playerDirection); velocity.multLocal (0.8f); spatial.move (velocity.mult (TPF * 0,1f)); // rotere søkeren hvis (hastighet! = Vector3f.ZERO) spatial.rotateUpTo (speed.normalize ()); spatial.rotate (0,0, FastMath.PI / 2f);  annet // håndtere "aktiv" -status lang dif = 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 ("Søker"); pic.getMaterial () SetColor ( "Farge", farge).;  @Override protected void controlRender (RenderManager rm, ViewPort vp) 

La oss fokusere på controlUpdate (float tpf):

Først må vi sjekke om fienden er aktiv. Hvis det ikke er det, må vi sakte fade det inn.
Vi sjekker da tiden som har gått siden vi hevdet fienden, og hvis det er lenge nok, satte vi det aktivt på.

Uansett om vi nettopp har satt den aktiv, må vi justere fargen. Den lokale variabelen romlig inneholder det romlige som kontrollen er festet til, men du kan huske at vi ikke feste kontrollen til selve bildet - bildet er et barn av noden vi knytter kontrollen til. (Hvis du ikke vet hva jeg snakker om, ta en titt på metoden getSpatial (Strenge navn) Vi implementerte siste opplæring.)

Så; vi får bildet som et barn av romlig, få materialet og sett fargen til riktig verdi. Ikke noe spesielt når du er vant til rom, materialer og noder.

info: Du lurer kanskje på hvorfor vi setter materialfargen til hvit. (RGB-verdiene er alle 1 i vår kode). Vi ønsker ikke en gul og en rød fiende?
Det er fordi materialet blander materialfargen med teksturfarger, så hvis vi ønsker å vise fiendens tekstur som det er, må vi blande det med hvitt.

Nå må vi ta en titt på hva vi gjør når fienden er aktiv. Denne kontrollen er oppkalt SeekerControl Av en grunn: Vi vil ha fiender med denne kontrollen knyttet til å følge spilleren.

For å oppnå det, beregner vi retningen fra søkeren til spilleren og legger denne verdien til hastigheten. Etter det reduserer vi hastigheten med 80% slik at den ikke kan vokse uendelig, og beveg søkeren tilsvarende.

Rotasjonen er ikke noe spesielt: hvis søkeren ikke står stille, roterer vi den i retning av spilleren. Vi roterer det litt mer fordi søkeren i Seeker.png peker ikke oppover, men til høyre.

info: De rotateUpTo (Vector3f retning) Metode av Romlig roterer et romlig slik at dets y-akse peker i den riktige retningen.

Så det var den første fienden. Koden til den andre fienden, vandreren, er ikke mye forskjellig:

 offentlig klasse WandererControl utvider AbstractControl private int screenWidth, screenHeight; privat Vector3f hastighet; privat float directionAngle; privat lang spawntime; offentlig WandererControl (int screenWidth, int screenHeight) this.screenWidth = screenWidth; this.screenHeight = screenHeight; hastighet = ny Vector3f (); directionAngle = new Random (). nextFloat () * FastMath.PI * 2f; spawnTime = System.currentTimeMillis ();  @Override protected void controlUpdate (float tpf) if ((Boolean) spatial.getUserData ("aktiv")) // oversette wanderer // endre retningenAngle litt retningAngle + = (nytt Tilfeldig (). NextFloat () * 20f - 10f) * tpf; System.out.println (directionAngle); Vector3f directionVector = MonkeyBlasterMain.getVectorFromAngle (directionAngle); directionVector.multLocal (1000f); velocity.addLocal (directionVector); // redusere hastigheten litt og flytte wandererhastigheten.multLocal (0.8f); spatial.move (velocity.mult (TPF * 0,1f)); // la vandreren hoppe av skjermgrensene Vector3f loc = spatial.getLocalTranslation (); hvis (loc.x screenWidth || loc.y> screenHeight) Vector3f newDirectionVector = ny Vector3f (screenWidth / 2, screenHeight / 2,0) .subtrahere (loc); directionAngle = MonkeyBlasterMain.getAngleFromVector (newDirectionVector);  // rotere wanderer spatial.rotate (0,0, tpf * 2);  ellers // håndtere "aktiv" -status lang dif = 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 ("Wanderer"); pic.getMaterial () SetColor ( "Farge", farge).;  @Override protected void controlRender (RenderManager rm, ViewPort vp) 

Den enkle ting først: Fading fienden i er den samme som i søkerkontrollen. I konstruktøren velger vi en tilfeldig retning for vandreren, der den vil fly en gang aktivert.

Tips: Hvis du har mer enn to fiender, eller bare vil strukturere spillet mer rent, kan du legge til en tredje kontroll: EnemyControl Det ville håndtere alt som alle fiender hadde til felles: flytte fienden, blekner den, gjør den aktiv ...

Nå til de store forskjellene:

Når fienden er aktiv, endrer vi først retningen litt, slik at vandreren ikke beveger seg i en rett linje hele tiden. Vi gjør dette ved å endre vår directionAngle litt og legge til directionVector til hastighet. Vi bruker så hastigheten som vi gjør i SeekerControl.

Vi må sjekke om vandreren er utenfor skjermgrensene, og i så fall endrer vi directionAngle til en mer hensiktsmessig retning slik at den blir brukt i neste oppdatering.

Til slutt roterer vi vandreren litt. Dette er bare fordi en spinnende fiende ser kulere ut.

Nå som vi har fullført begge fiender, kan du starte spillet og spille litt. Det gir deg et lite blikk på hvordan spillet vil spille, selv om du ikke kan drepe fiender, og de kan heller ikke drepe deg. La oss legge til det neste.

Kollisjonsdeteksjon

For å få fiender til å drepe spilleren, må vi vite om de kolliderer. For dette legger vi til en ny metode, handleCollisions, innkalt simpleUpdate (float tpf):

 @Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("live")) spawnEnemies (); handleCollisions (); 

Og nå den faktiske metoden:

 privat ugyldig håndtakKollisjoner () // skal spilleren dø? for (int i = 0; i 

Vi gjenkjenner gjennom alle fiender ved å gjøre mengden av knutens barn og deretter få hver enkelt av dem. Videre må vi bare sjekke hvor fienden dreper spilleren når fienden faktisk er aktiv. Hvis det ikke er det, trenger vi ikke bry oss om det. Så hvis han er aktiv, kontrollerer vi hvor spilleren og fienden kolliderer. Vi gjør det i en annen metode, checkCollisoin (romlig a, romlig b):

 privat boolsk kontrollCollision (romlig a, romlig b) float distance = a.getLocalTranslation () .avstand (b.getLocalTranslation ()); float maxDistance = (Float) a.getUserData ("radius") + (Float) b.getUserData ("radius"); returavstand <= maxDistance; 

Konseptet er ganske enkelt: først beregner vi avstanden mellom de to romlige. Deretter må vi vite hvor nær de to romlige må være for å bli vurdert å ha kollidert, så vi får radiusen til hvert romlig og legger til dem. (Vi angir brukerdata "radius" i getSpatial (Strenge navn) i den forrige opplæringen.) Så, hvis den faktiske avstanden er kortere enn eller lik denne maksimale avstanden, returnerer metoden ekte, noe som betyr at de kolliderte.

Hva nå? Vi må drepe spilleren. La oss lage en annen metode:

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

Først løsner vi spilleren fra sin overordnede node, som automatisk fjerner den fra scenen. Deretter må vi tilbakestille bevegelsen i PlayerControl-Ellers kan spilleren fortsatt bevege seg når den gyter igjen.

Vi setter deretter userdataene i live til falsk og opprett en ny brukerdata dieTime. (Vi trenger det for å respawn spilleren når den er død.)

Til slutt fjerner vi alle fiender, da spilleren vil ha en vanskelig tid å bekjempe de eksisterende fiender av rett når det gis.

Vi har allerede nevnt respawning, så la oss håndtere det neste. Vi vil, igjen, modifisere simpleUpdate (float tpf) metode:

 @Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("live")) spawnEnemies (); handleCollisions ();  ellers hvis (System.currentTimeMillis () - (Long) player.getUserData ("dieTime")> 4000f &&! gameOver) // spawn player player.setLocalTranslation (500.500,0); guiNode.attachChild (spiller); player.setUserData ( "levende", true); 

Så, hvis spilleren ikke er i live og har vært død lenge nok, setter vi sin posisjon til midten av skjermen, legger den til scenen og til slutt setter sin brukerdata i live til ekte en gang til!

Nå kan det være en god tid å starte spillet og teste våre nye funksjoner. Du har en vanskelig tid som varer lengre enn tjue sekunder, men fordi pistolen din er verdiløs, så la oss gjøre noe med det.

For å få kuler til å drepe fiender, legger vi til noe kode i handleCollisions () metode:

 // skal en fiende dø? int i = 0; mens jeg < 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++; 

Prosedyren for å drepe fiender er stort sett den samme som for å drepe spilleren; vi itererer gjennom alle fiender og alle kuler, kontroller om de kolliderer, og hvis de gjør det, løsner vi begge.

Kjør spillet og se hvor langt du får!

info: Iterering gjennom hver fiende og sammenligning av posisjonen med hver kule posisjon er en veldig dårlig måte å sjekke for kollisjoner. Det er greit i dette eksemplet for enkelhets skyld, men i en ekte spill du må implementere bedre algoritmer for å gjøre det, som quadtree kollisjon gjenkjenning. Heldigvis bruker jMonkeyEngine Bullet Physics-motoren, så når du har komplisert 3D-fysikk, trenger du ikke å bekymre deg for dette.

Nå er vi ferdige med hovedspillet. Vi skal fremdeles implementere svarte hull og vise spillernes score og liv, og for å gjøre spillet morsommere og spennende, legger vi til lydeffekter og bedre grafikk. Sistnevnte vil bli oppnådd gjennom blomstring etterbehandling filter, noen partikkel effekter og en kul bakgrunnseffekt.

Før vi vurderer denne delen av serien ferdig, legger vi til litt lyd og blomstreffekten.


Spille av lyder og musikk

For å få litt lyd til vårt spill, oppretter vi en ny klasse, bare kalt Lyd:

 offentlig klasse Lyd privat AudioNode musikk; private lydnode [] skudd; private lydnode [] eksplosjoner; Private AudioNode [] spawns; privat AssetManager assetManager; offentlig lyd (AssetManager assetManager) this.assetManager = assetManager; skudd = ny AudioNode [4]; eksplosjoner = ny AudioNode [8]; spawns = ny AudioNode [8]; loadSounds ();  privat ugyldig lastSounds () music = new AudioNode (assetManager, "Sounds / Music.ogg"); music.setPositional (false); music.setReverbEnabled (false); music.setLooping (true); for (int i = 0; i 

Her begynner vi ved å sette opp det nødvendige AudioNode variabler og initialiser arrays.

Deretter laster vi lydene, og for hver lyd gjør vi stort sett det samme. Vi lager en ny AudioNode, med hjelp av kapitalforvalter. Da setter vi det ikke på plass og deaktiverer reverb. (Vi trenger ikke at lyden skal være posisjonell fordi vi ikke har stereoutgang i vårt 2D-spill, selv om du kunne implementere det hvis du likte.) Deaktivering av reverb gjør at lyden blir spilt akkurat som det er i selve lyden fil; hvis vi aktiverte det, kunne vi gjøre jME la lyden høres ut som om vi ville være i en hule eller fangehull, for eksempel. Deretter setter vi loopingen til ekte for musikken og til falsk for enhver annen lyd.

Å spille lydene er ganske enkelt: vi ringer bare soundX.play ().

info: Når du bare ringer spille() På en eller annen lyd spiller den bare lyden. Men noen ganger vil vi spille den samme lyden to ganger eller enda flere ganger samtidig. Det er hva playInstance () er der for: det skaper en ny forekomst for hver lyd slik at vi kan spille samme lyd flere ganger samtidig.

Jeg legger resten av arbeidet til deg: du må ringe startMusic, skyte(), eksplosjon() (for døende fiender), og gyte () på de riktige stedene i vår hovedklasse MonkeyBlasterMain ().

Når du er ferdig, ser du at spillet nå er mye morsommere. de få lydeffektene legger virkelig til atmosfæren. Men la oss også finpudse grafikken.


Legge til Bloom Post-Processing Filter

Aktivering av blomst er veldig enkelt i jMonkeyEngine, da alle nødvendige koden og shaders allerede er implementert for deg. Bare gå videre og lim inn disse linjene inn i simpleInitApp ():

 FilterPostProcessor fpp = ny FilterPostProcessor (assetManager); BloomFilter blomstre = ny BloomFilter (); bloom.setBloomIntensity (2f); bloom.setExposurePower (2); bloom.setExposureCutOff (0f); bloom.setBlurScale (1.5f); fpp.addFilter (bloom); guiViewPort.addProcessor (FPP); guiViewPort.setClearColor (true);

Jeg har konfigurert BloomFilter litt; hvis du vil vite hva alle disse innstillingene er der for, bør du sjekke ut jME-opplæringen på blomst.


Konklusjon

Gratulerer med å fullføre den andre delen. Det er tre deler å gå, så bli ikke distrahert ved å spille for lenge! Neste gang legger vi til GUI og de svarte hullene.