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.
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.
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.
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.
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.
1
i vår kod). Vill vi inte ha en gul och en röd fiende?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.
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.
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; iVi 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 metodenSann
, 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
tillfalsk
och skapa en ny användardatadieTime
. (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
tillSann
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; iHä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 avassetManager
. 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 tillSann
för musiken och tillfalsk
för något annat ljud.Att spela ljudet är ganska enkelt: vi ringer bara
Info: När du bara ringersoundX.play ()
.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 vadplayInstance ()
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), ochrom()
på lämpliga platser i vår huvudklassMonkeyBlasterMain ()
.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.