Gör en Neon Vector Shooter i jMonkeyEngine Enemies and Sounds

I den första delen av serien om att bygga ett Geometry Wars-inspirerat spel i jMonkeyEngine genomförde vi spelarens skepp och lät det röra sig och skjuta. Den här gången lägger vi till fiender och ljudeffekter.


Översikt

Här är vad vi arbetar mot över hela serien:


... och här är vad vi får i slutet av den här delen:


Vi behöver några nya klasser för att kunna implementera de nya funktionerna:

  • SeekerControl: Detta är en beteendeklass för sökandens fiende.
  • WandererControl: Detta är också en beteende klass, den här gången för vandrare fienden.
  • Ljud: Vi hanterar laddning och spelning av ljudeffekter och musik med detta.

Som du kanske har gissat lägger vi till två typer av fiender. Den första kallas a seeker; det kommer aktivt att jaga spelaren tills den dör. Den andra, den vandrare, strömmar bara runt skärmen i ett slumpmässigt mönster.


Lägger till fiender

Vi ska kasta fienderna i slumpmässiga positioner på skärmen. För att ge spelaren lite tid att reagera, kommer fienden inte att vara aktiv omedelbart, utan kommer att blekna in långsamt. Efter att det har blivit helt blekt, börjar det röra sig genom världen. När den kolliderar med spelaren dör spelaren. när den kolliderar med en kula, dör den själv.

Spawning fienden

Först av allt måste vi skapa några nya variabler i MonkeyBlasterMain klass:

 privat lång fiendeSpawnCooldown; privat float enemySpawnChance = 80; privat node enemyNode;

Vi kommer snart att använda de två första. Innan det måste vi initiera enemyNode i simpleInitApp ():

 // ställa in fiendenNode enemyNode = ny nod ("fiender"); guiNode.attachChild (enemyNode);

Okej, nu vidare till den riktiga gyckkoden: Vi åsidosätter simpleUpdate (float tpf). Denna metod kallas av motorn om och om igen, och fortsätter helt enkelt att ringa fiendens gyningsfunktion så länge spelaren lever. (Vi har redan ställt in userdata Levande till Sann i den sista handledningen.)

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

Och så här spenderar vi faktiskt fienderna:

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

Bli inte förvirrad av enemySpawnCooldown variabel. Det är inte där för att få fiender att spawna på en anständig frekvens-17ms skulle vara mycket för kort av ett intervall.

enemySpawnCooldown är faktiskt där för att se till att mängden nya fiender är densamma på varje maskin. På snabbare datorer, simpleUpdate (float tpf) kallas mycket oftare än på långsammare. Med denna variabel kontrollerar vi varje 17mms om vi ska kasta nya fiender.
Men vill vi gissa dem var 17mars? Vi vill faktiskt att de ska gissa i slumpmässiga intervaller, så vi presenterar en om påstående:

 om (ny slumpmässig (). nextInt ((int) enemySpawnChance) == 0) 

Ju mindre värdet av enemySpawnChance, ju mer sannolikt det är att en ny fiende kommer att spawna i detta 17ms intervall, och så ju fler fiender spelaren behöver hantera. Det är därför vi subtraherar lite av enemySpawnChance varje kryssning: det betyder att spelet blir svårare över tiden.

Att skapa sökare och vandrare liknar att skapa något annat objekt:

 privat tomt skapaSeeker () Spatial sökare = getSpatial ("sökare"); seeker.setLocalTranslation (getSpawnPosition ()); sökare.addControl (ny SeekerControl (spelare)); seeker.setUserData ( "aktiv", false); enemyNode.attachChild (sökare);  privat tomt skapaWanderer () Spatial wanderer = getSpatial ("Wanderer"); wanderer.setLocalTranslation (getSpawnPosition ()); wanderer.addControl (nya WandererControl ()); wanderer.setUserData ( "aktiv", false); enemyNode.attachChild (vandrare); 

Vi skapar det rumsliga, vi flyttar det, vi lägger till en anpassad kontroll, vi ställer den in aktiv, och vi bifogar den till vår enemyNode. Vad? Varför inte aktiv? Det beror på att vi inte vill att fienden ska börja jaga spelaren så snart den springer; vi vill ge spelaren lite tid att reagera.

Innan vi kommer in i kontrollerna måste vi genomföra metoden getSpawnPosition (). Fienden ska kasta slumpmässigt, men inte direkt bredvid spelaren:

 privat Vector3f getSpawnPosition () Vector3f pos; gör pos = ny vektor3f (ny slumpmässig (). nextInt (settings.getWidth ()), ny slumpmässig (). nextInt (settings.getHeight ()), 0);  medan (pos.distanceSquared (player.getLocalTranslation ()) < 8000); return pos; 

Vi beräknar en ny slumpmässig position pos. Om det är för nära spelaren beräknar vi en ny position, och upprepa tills det är ett anständigt avstånd bort.

Nu behöver vi bara göra fienderna aktiva och börja flytta. Vi gör det i sina kontroller.

Kontrollerar fiendens beteende

Vi tar itu med SeekerControl först:

 allmän klass SeekerControl utökar AbstractControl privat rumslig spelare; privat vektor3f hastighet; privat lång spawnTime; Officiell SeekerControl (rumslig spelare) this.player = player; hastighet = ny vektor3f (0,0,0); spawnTime = System.currentTimeMillis ();  @Override protected void controlUpdate (float tpf) if ((Boolean) spatial.getUserData ("aktiv")) // översätt sökaren Vector3f playerDirection = player.getLocalTranslation (). Subtrahera (spatial.getLocalTranslation ()); playerDirection.normalizeLocal (); playerDirection.multLocal (1000F); velocity.addLocal (playerDirection); velocity.multLocal (0.8f); spatial.move (velocity.mult (TPF * 0.1f)); // rotera sökaren om (hastighet! = Vector3f.ZERO) spatial.rotateUpTo (hastighet.normalize ()); spatial.rotate (0,0, FastMath.PI / 2f);  else // hantera "aktiv" -status long dif = System.currentTimeMillis () - spawnTime; om (dif> = 1000f) spatial.setUserData ("aktiv", true);  ColorRGBA color = new ColorRGBA (1,1,1, dif / 1000f); Node spatialNode = (Node) spatial; Picture pic = (Bild) spatialNode.getChild ("sökare"); pic.getMaterial () setColor ( "Färg", färg).  @Override protected void controlRender (RenderManager rm, ViewPort vp) 

Låt oss fokusera på controlUpdate (float tpf):

Först måste vi kontrollera om fienden är aktiv. Om det inte är så måste vi sakta in det.
Vi kontrollerar då tiden som har förflutit sedan vi hämtade fienden och om det är tillräckligt länge satte vi det aktivt.

Oavsett om vi just har aktiverat det, måste vi justera färgen. Den lokala variabeln rumslig innehåller det rumsliga som kontrollen har kopplats till, men du kan komma ihåg att vi inte bifogade kontrollen till den faktiska bilden - bilden är ett barn av noden som vi bifogade kontrollen till. (Om du inte vet vad jag pratar om, ta en titt på metoden getSpatial (strängnamn) vi genomförde sista handledningen.)

Så; vi får bilden som ett barn av rumslig, få sitt material och sätt dess färg till lämpligt värde. Inget speciellt när du är van vid rumsformerna, materialen och noderna.

Info: Du kanske undrar varför vi ställer in materialfärgen till vit. (RGB-värdena är alla 1 i vår kod). Vill vi inte ha en gul och en röd fiende?
Det beror på att materialet blandar materialfärgen med texturfärgerna, så om vi vill visa fiendens textur som det är, måste vi blanda den med vit.

Nu måste vi titta på vad vi gör när fienden är aktiv. Denna kontroll heter SeekerControl av en anledning: vi vill att fiender med denna kontroll är kopplade till att följa spelaren.

För att uppnå det beräknar vi riktningen från sökaren till spelaren och lägger till detta värde till hastigheten. Därefter sänker vi hastigheten med 80% så att den inte kan växa oändligt och flytta sökaren i enlighet därmed.

Rotationen är inget speciellt: om sökaren inte står stilla roterar vi den i spelarens riktning. Vi roterar det lite mer eftersom sökaren i Seeker.png pekar inte uppåt, men till höger.

Info: De roteraUpTo (Vector3f-riktning) metod av Rumslig roterar ett rumsligt så att dess y-axel pekar i den givna riktningen.

Så det var den första fienden. Koden för den andra fienden, vandraren, är inte mycket annorlunda:

 public class WandererControl utökar AbstractControl private int screenWidth, screenHeight; privat vektor3f hastighet; privat float directionAngle; privat lång spawnTime; public WandererControl (int skärmvidd, int skärmhöjd) this.screenWidth = screenWidth; this.screenHeight = screenHeight; hastighet = ny vektor3f (); directionAngle = new Random (). nextFloat () * FastMath.PI * 2f; spawnTime = System.currentTimeMillis ();  @Override protected void controlUpdate (float tpf) if ((Boolean) spatial.getUserData ("aktiv")) // översätt wanderer // ändra riktningAngla en bitriktningAngle + = (nytt slumpmässigt (). NextFloat () * 20f - 10f) * tpf; System.out.println (directionAngle); Vector3f directionVector = MonkeyBlasterMain.getVectorFromAngle (directionAngle); directionVector.multLocal (1000F); velocity.addLocal (directionVector); // minska hastigheten en bit och flytta vandringshastigheten.multLocal (0.8f); spatial.move (velocity.mult (TPF * 0.1f)); // göra vandraren studsa av skärmgränserna Vector3f loc = spatial.getLocalTranslation (); om (loc.x screenWidth || loc.y> screenHeight) Vector3f newDirectionVector = ny Vector3f (screenWidth / 2, screenHeight / 2,0) .subtrahera (loc); directionAngle = MonkeyBlasterMain.getAngleFromVector (newDirectionVector);  // rotera wanderer spatial.rotate (0,0, tpf * 2);  annat // hantera "aktiv" -status long dif = System.currentTimeMillis () - spawnTime; om (dif> = 1000f) spatial.setUserData ("aktiv", true);  ColorRGBA color = new ColorRGBA (1,1,1, dif / 1000f); Node spatialNode = (Node) spatial; Bild pic = (Bild) spatialNode.getChild ("Wanderer"); pic.getMaterial () setColor ( "Färg", färg).  @Override protected void controlRender (RenderManager rm, ViewPort vp) 

De enkla sakerna först: bleknar fienden i är detsamma som i sökarens kontroll. I konstruktören väljer vi en slumpmässig riktning för vandraren, där den kommer att flyga en gång aktiverad.

Tips: Om du har mer än två fiender, eller helt enkelt vill strukturera spelet mer ren, kan du lägga till en tredje kontroll: EnemyControl Det skulle hantera allt som alla fiender hade gemensamt: flytta fienden, blekna det och göra det aktivt ...

Nu till de stora skillnaderna:

När fienden är aktiv, ändrar vi först sin riktning lite, så att vandraren inte rör sig i en rak linje hela tiden. Vi gör detta genom att ändra vår directionAngle lite och lägga till directionVector till hastighet. Vi applicerar sedan hastigheten precis som vi gör i SeekerControl.

Vi måste kontrollera om vandraren är utanför skärmens gränser och om så är fallet ändras vi directionAngle till en mer lämplig riktning så att den tillämpas i nästa uppdatering.

Slutligen roterar vi vandraren lite. Detta beror bara på att en snurrande fiende ser svalare ut.

Nu när vi har genomfört båda fienderna kan du starta spelet och spela lite. Det ger dig en liten blick på hur spelet kommer att spela, även om du inte kan döda fienderna och de inte heller kan döda dig. Låt oss lägga till det nästa.

Kollisionsdetektering

För att få fiender att döda spelaren behöver vi veta om de kolliderar. För detta lägger vi till en ny metod, handleCollisions, ringde in simpleUpdate (float tpf):

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

Och nu själva metoden:

 privat tomt handtagKollisioner () // ska spelaren dö? för (int i = 0; i 

Vi repeterar genom alla fiender genom att ge kvantiteten av knutens barn och sedan få var och en av dem. Dessutom behöver vi bara kontrollera huruvida fienden dödar spelaren när fienden faktiskt är aktiv. Om det inte är, behöver vi inte bry oss om det. Så om han är aktiv kontrollerar vi när spelaren och fienden kolliderar. Vi gör det i en annan metod, checkCollisoin (rumslig a, rumslig b):

 privat booleansk checkCollision (rumslig a, rumslig b) float distance = a.getLocalTranslation () .distans (b.getLocalTranslation ()); float maxDistance = (Float) a.getUserData ("radien") + (Float) b.getUserData ("radie"); returavstånd <= maxDistance; 

Konceptet är ganska enkelt: först beräknar vi avståndet mellan de två rumsliga områdena. Därefter måste vi veta hur nära de två rumsliga egenskaperna måste vara för att kunna anses ha kolliderat, så vi får varje rums radie och lägger till dem. (Vi ställer in användardata "radie" i getSpatial (strängnamn) i föregående handledning.) Så, om det faktiska avståndet är kortare än eller lika med det maximala avståndet, returnerar metoden Sann, vilket innebär att de kolliderade.

Vad nu? Vi måste döda spelaren. Låt oss skapa en annan metod:

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

För det första tar vi bort spelaren från dess föräldraknapp, som automatiskt tar bort den från scenen. Därefter måste vi återställa rörelsen i PlayerControl-annars kan spelaren fortfarande röra sig när den springer igen.

Vi ställer sedan in userdata Levande till falsk och skapa en ny användardata dieTime. (Vi behöver det för att respawn spelaren när den är död.)

Slutligen avlägsnar vi alla fiender, eftersom spelaren skulle ha svårt att kämpa mot de redan befintliga fienderna direkt när det springer.

Vi nämnde redan respawning, så låt oss hantera det nästa. Vi kommer än en gång att ändra simpleUpdate (float tpf) metod:

 @Override public void simpleUpdate (float tpf) om ((Boolean) player.getUserData ("live")) spawnEnemies (); handleCollisions ();  annars om (System.currentTimeMillis () - (Lång) player.getUserData ("dieTime")> 4000f &&! gameOver) // spawn player player.setLocalTranslation (500.500,0); guiNode.attachChild (spelare); player.setUserData ( "levande", true); 

Så, om spelaren inte lever och har varit död tillräckligt länge, ställer vi sin position till mitten av skärmen, lägger den till scenen och ställer slutligen sin användardata Levande till Sann igen!

Nu kan det vara en bra tid att starta spelet och testa våra nya funktioner. Du kommer dock ha svårt att vara längre än tjugo sekunder, eftersom din pistol är värdelös, så låt oss göra något åt ​​det.

För att få kulor att döda fiender lägger vi till en kod till handleCollisions () metod:

 // ska en fiende dö? int i = 0; medan jag < 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++; 

Förfarandet för att döda fiender är ungefär detsamma som för att döda spelaren; vi iterera genom alla fiender och alla kulor, kontrollera om de kolliderar och om de gör det, lossnar vi båda.

Nu kör spelet och se hur långt du får!

Info: Iterating genom varje fiende och jämföra sin position med varje punkts position är ett mycket dåligt sätt att kontrollera efter kollisioner. Det är okej i det här exemplet för enkelhetens skull, men i en verklig spel du måste implementera bättre algoritmer för att göra det, som quadtree kollisionsdetektering. Lyckligtvis använder jMonkeyEngine Bullet Physics-motorn, så när du har komplicerad 3D-fysik behöver du inte oroa dig för det här.

Nu är vi färdiga med huvudspelet. Vi ska fortfarande implementera svarta hål och visa spelarens poäng och liv, och för att göra spelet roligare och spännande lägger vi till ljudeffekter och bättre grafik. Det senare kommer att uppnås genom blombehandlingsfiltret, några partikeleffekter och en cool bakgrundseffekt.

Innan vi anser att den här delen av serien är klar, lägger vi till lite ljud och blomningseffekten.


Spelar ljud och musik

För att få lite ljud i vårt spel ska vi skapa en ny klass, helt enkelt kallad Ljud:

 offentlig klass Ljud privat AudioNode musik; privata AudioNode [] skott; privata AudioNode [] explosioner; privata AudioNode [] spawns; privat AssetManager assetManager; offentlig ljud (AssetManager assetManager) this.assetManager = assetManager; skott = ny AudioNode [4]; explosioner = ny AudioNode [8]; spawns = new AudioNode [8]; loadSounds ();  privat tomgångsljudSound () music = new AudioNode (assetManager, "Ljud / Music.ogg"); music.setPositional (false); music.setReverbEnabled (false); music.setLooping (true); för (int i = 0; i 

Här börjar vi med att sätta upp de nödvändiga AudioNode variabler och initiera arrayerna.

Nästa laddar vi ljuden, och för varje ljud gör vi ganska mycket samma sak. Vi skapar en ny AudioNode, med hjälp av assetManager. Då ställer vi det inte positionellt och inaktiverar reverb. (Vi behöver inte låten vara positionell eftersom vi inte har stereoutgång i vårt 2D-spel, men du kan implementera det om du tyckte om det.) Inaktivera ljudet gör ljudet att spela upp precis som det är i själva ljudet fil; om vi aktiverade det kunde vi göra jME låta ljudet låta som om vi skulle vara i en grotta eller fängelsehålan, till exempel. Därefter sätter vi loopingen till Sann för musiken och till falsk för något annat ljud.

Att spela ljudet är ganska enkelt: vi ringer bara soundX.play ().

Info: När du bara ringer spela() På något ljud spelar det bara ljudet. Men ibland vill vi spela samma ljud två gånger eller fler gånger samtidigt. Det är vad playInstance () finns för: det skapar en ny instans för varje ljud så att vi kan spela samma ljud flera gånger samtidigt.

Jag lämnar resten av arbetet upp till dig: du måste ringa startMusic, skjuta(), explosion() (för döende fiender), och rom() på lämpliga platser i vår huvudklass MonkeyBlasterMain ().

När du är klar ser du att spelet nu är mycket roligare. de få ljudeffekterna lägger verkligen till atmosfären. Men låt oss polera grafiken lite också.


Lägga till Bloom Post-Processing Filter

Att aktivera blom är mycket enkelt i jMonkeyEngine, eftersom alla nödvändiga kod och shaders redan är implementerade för dig. Bara fortsätt och klistra in dessa linjer i simpleInitApp ():

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

Jag har konfigurerat BloomFilter lite; om du vill veta vad alla dessa inställningar finns för, bör du kolla in jME-handledningen på blom.


Slutsats

Grattis för att avsluta andra delen. Det finns tre delar att gå, så bli inte distraherad genom att leka för länge! Nästa gång lägger vi till GUI och de svarta hålen.