Maak een Neon Vector Shooter in jMonkeyEngine Enemies and Sounds

In het eerste deel van deze serie over het bouwen van een op Geometry Wars geïnspireerd spel in jMonkeyEngine hebben we het schip van de speler geïmplementeerd en het laten bewegen en schieten. Deze keer voegen we de vijanden en geluidseffecten toe.


Overzicht

Dit is waar we naartoe werken in de hele serie:


... en hier is wat we zullen hebben aan het einde van dit deel:


We hebben nieuwe klassen nodig om de nieuwe functies te implementeren:

  • SeekerControl: Dit is een gedragsklasse voor de zoekende vijand.
  • WandererControl: Dit is ook een gedragsklasse, deze keer voor de zwerfvijand.
  • Geluid: We zullen hiermee het laden en afspelen van geluidseffecten en muziek beheren.

Zoals je misschien al geraden hebt, voegen we twee soorten vijanden toe. De eerste heet a zoeker; het zal de speler actief achtervolgen tot hij sterft. De andere, de zwerver, zwerft gewoon rond het scherm in een willekeurig patroon.


Vijanden toevoegen

We zullen de vijanden op willekeurige posities op het scherm spawnen. Om de speler wat tijd te geven om te reageren, zal de vijand niet onmiddellijk actief zijn, maar eerder langzaam vervagen. Nadat het volledig is vervaagd, zal het zich door de wereld bewegen. Wanneer het botst met de speler, sterft de speler; wanneer het botst met een kogel, sterft het zelf.

Het kuit schieten van vijanden

Allereerst moeten we een aantal nieuwe variabelen maken in de MonkeyBlasterMain klasse:

 privé lange vijandSpawnCooldown; private float enemySpawnChance = 80; private Node enemyNode;

We zullen de eerste twee snel genoeg gaan gebruiken. Voordien moeten we het enemyNode in simpleInitApp ():

 // de vijand instellenNode enemyNode = nieuwe Node ("vijanden"); guiNode.attachChild (enemyNode);

Oké, nu naar de echte spawning-code: we zullen negeren simpleUpdate (float tpf). Deze methode wordt steeds opnieuw door de engine aangeroepen en blijft gewoon de vijandelijke spawnfunctie aanroepen zolang de speler nog leeft. (We hebben de userdata al ingesteld levend naar waar in de laatste zelfstudie.)

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

En dit is hoe we de vijanden daadwerkelijk spawnen:

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

Raak niet in de war door de enemySpawnCooldown variabel. Het is er niet om vijanden met een fatsoenlijke frequentie te laten spawnen - 17ms zou veel te kort zijn voor een interval.

enemySpawnCooldown is er eigenlijk om ervoor te zorgen dat de hoeveelheid nieuwe vijanden op elke machine hetzelfde is. Op snellere computers, simpleUpdate (float tpf) wordt veel vaker gebeld dan op langzamere. Met deze variabele controleren we elke 17 ms of we nieuwe vijanden moeten spawnen.
Maar willen we ze elke 17ms spawnen? We willen eigenlijk dat ze in willekeurige intervallen spawnen, dus introduceren we een als uitspraak:

 if (nieuw Willekeurig (). nextInt ((int) enemySpawnChance) == 0) 

Hoe kleiner de waarde van enemySpawnChance, hoe waarschijnlijker het is dat een nieuwe vijand in dit interval van 17 ms zal spawnen, en dus hoe meer vijanden de speler moet afhandelen. Dat is waarom we een klein beetje aftrekken enemySpawnChance elke vink: dit betekent dat het spel na verloop van tijd moeilijker zal worden.

Het creëren van zoekers en zwervers lijkt op het maken van een ander object:

 private void createSeeker () Spatial seeker = getSpatial ("Seeker"); seeker.setLocalTranslation (getSpawnPosition ()); seeker.addControl (nieuwe SeekerControl (speler)); seeker.setUserData ( "actief", false); enemyNode.attachChild (zoeker);  private void createWanderer () Spatial wanderer = getSpatial ("Wanderer"); wanderer.setLocalTranslation (getSpawnPosition ()); wanderer.addControl (nieuwe WandererControl ()); wanderer.setUserData ( "actief", false); enemyNode.attachChild (zwerver); 

We maken het ruimtelijke, we verplaatsen het, we voegen een aangepast besturingselement toe, we plaatsen het niet-actief en we hechten dit aan ons enemyNode. Wat? Waarom niet-actief? Dat komt omdat we niet willen dat de vijand de speler begint te achtervolgen zodra hij spawnen; we willen de speler wat tijd geven om te reageren.

Voordat we aan de controles beginnen, moeten we de methode implementeren getSpawnPosition (). De vijand zou willekeurig moeten spawnen, maar niet direct naast de speler:

 private Vector3f getSpawnPosition () Vector3f pos; do pos = new Vector3f (nieuw Willekeurig (). nextInt (settings.getWidth ()), nieuw Random (). nextInt (settings.getHeight ()), 0);  while (pos.distanceSquared (player.getLocalTranslation ()) < 8000); return pos; 

We berekenen een nieuwe willekeurige positie pos. Als het te dicht bij de speler staat, berekenen we een nieuwe positie en herhalen totdat het een behoorlijke afstand verwijderd is.

Nu moeten we ervoor zorgen dat de vijanden zichzelf activeren en beginnen te bewegen. We doen dat in hun besturingselementen.

Beheersing van vijandig gedrag

We zullen omgaan met de SeekerControl eerste:

 public class SeekerControl breidt InteractiveControl uit private Spatial player; privé Vector3f-snelheid; privé lange spawnTime; public SeekerControl (Spatial player) this.player = player; velocity = new Vector3f (0,0,0); spawnTime = System.currentTimeMillis ();  @Override protected void controlUpdate (float tpf) if ((Boolean) spatial.getUserData ("active")) // translate the seeker Vector3f playerDirection = player.getLocalTranslation (). Aftrekken (spatial.getLocalTranslation ()); playerDirection.normalizeLocal (); playerDirection.multLocal (1000F); velocity.addLocal (playerDirection); velocity.multLocal (0.8f); spatial.move (velocity.mult (TPF * 0,1f)); // roteer de zoeker if (velocity! = Vector3f.ZERO) spatial.rotateUpTo (velocity.normalize ()); spatial.rotate (0,0, FastMath.PI / 2f);  else // omgaan met de "actieve" -status lang dif = System.currentTimeMillis () - spawnTime; if (dif> = 1000f) spatial.setUserData ("active", true);  ColorRGBA-kleur = nieuwe ColorRGBA (1,1,1, dif / 1000f); Node spatialNode = (Node) spatial; Picture pic = (Afbeelding) spatialNode.getChild ("Zoeker"); pic.getMaterial () setColor ( "Kleur", kleur).;  @Override protected void controlRender (RenderManager rm, ViewPort vp) 

Laten we ons concentreren op controlUpdate (float tpf):

Eerst moeten we controleren of de vijand actief is. Als dat niet zo is, moeten we het langzaam laten vervagen.
We controleren dan de tijd die is verstreken sinds we de vijand hebben voortgebracht en, als het lang genoeg is, stellen we het actief in.

Ongeacht of we het zojuist hebben ingesteld, we moeten de kleur ervan aanpassen. De lokale variabele ruimtelijke bevat het ruimtelijke gedeelte waaraan het besturingselement is gekoppeld, maar misschien herinner je je dat we het besturingselement niet aan het werkelijke plaatje hebben gekoppeld - het beeld is een kind van het knooppunt waaraan we het besturingselement hebben gekoppeld. (Als je niet weet waar ik het over heb, kijk eens naar de methode getSpatial (String naam) we hebben de laatste zelfstudie geïmplementeerd.)

Zo; we krijgen de foto als een kind van ruimtelijke, verkrijg het materiaal en stel de kleur in op de juiste waarde. Niets bijzonders als je eenmaal gewend bent aan de spatials, materialen en knooppunten.

info: Je vraagt ​​je misschien af ​​waarom we de materiaalkleur op wit zetten. (De RGB-waarden zijn allemaal 1 in onze code). Willen we niet een gele en een rode vijand??
Omdat het materiaal de materiaalkleur combineert met de textuurkleuren, dus als we de textuur van de vijand willen weergeven zoals die is, moeten we deze mengen met wit.

Nu moeten we eens kijken naar wat we doen als de vijand actief is. Dit besturingselement is genoemd SeekerControl met een reden: we willen dat vijanden met deze controle verbonden zijn om de speler te volgen.

Om dat te bereiken, berekenen we de richting van de zoeker naar de speler en voegen deze waarde toe aan de snelheid. Daarna verlagen we de snelheid met 80% zodat deze niet oneindig kan groeien en de zoeker dienovereenkomstig kan bewegen.

De rotatie is niets bijzonders: als de zoeker niet stilstaat, draaien we hem in de richting van de speler. We draaien het dan een beetje meer omdat de zoeker erin zit Seeker.png is niet naar boven gericht, maar naar rechts.

info: De rotateUpTo (Vector3f-richting) methode van ruimtelijke roteert een spatie zodat de y-as in de gegeven richting wijst.

Dus dat was de eerste vijand. De code van de tweede vijand, de zwerver, is niet veel anders:

 public class WandererControl breidt AbstractControl uit private int screenWidth, screenHeight; privé Vector3f-snelheid; privévlotterrichting Angle; privé lange spawnTime; public WandererControl (int screenWidth, int screenHeight) this.screenWidth = screenWidth; this.screenHeight = screenHeight; velocity = new Vector3f (); directionAngle = new Random (). nextFloat () * FastMath.PI * 2f; spawnTime = System.currentTimeMillis ();  @Override beschermde ongeldige controleUpdate (float tpf) if ((Boolean) spatial.getUserData ("active")) // vertaal de zwerver // verander de richting Angle een beetje directionAngle + = (nieuw Random (). NextFloat () * 20f - 10f) * tpf; System.out.println (directionAngle); Vector3f directionVector = MonkeyBlasterMain.getVectorFromAngle (directionAngle); directionVector.multLocal (1000F); velocity.addLocal (directionVector); // verlaag de snelheid een beetje en verplaats de snelheid van de zwerver.multLocal (0.8f); spatial.move (velocity.mult (TPF * 0,1f)); / Laat de zwerver terugkaatsen van de randen van het scherm. Vector3f loc = spatial.getLocalTranslation (); if (loc.x screenWidth || loc.y> screenHeight) Vector3f newDirectionVector = new Vector3f (screenWidth / 2, screenHeight / 2,0) .subtract (loc); directionAngle = MonkeyBlasterMain.getAngleFromVector (newDirectionVector);  // roteer de zwerver space.rotate (0,0, tpf * 2);  else // omgaan met de "actieve" -status lang dif = System.currentTimeMillis () - spawnTime; if (dif> = 1000f) spatial.setUserData ("active", true);  ColorRGBA-kleur = nieuwe ColorRGBA (1,1,1, dif / 1000f); Node spatialNode = (Node) spatial; Picture pic = (Afbeelding) spatialNode.getChild ("Wanderer"); pic.getMaterial () setColor ( "Kleur", kleur).;  @Override protected void controlRender (RenderManager rm, ViewPort vp) 

De makkelijke dingen eerst: het vervagen van de vijand is hetzelfde als bij de zoekerbesturing. In de constructor kiezen we een willekeurige richting voor de zwerver, waarin hij zal vliegen zodra hij geactiveerd is.

Tip: Als je meer dan twee vijanden hebt, of je wilt gewoon het spel overzichtelijker structureren, dan kun je een derde controle toevoegen: EnemyControl Het zou omgaan met alles wat alle vijanden gemeen hadden: de vijand verplaatsen, in laten vervagen, actief instellen ...

Nu naar de belangrijkste verschillen:

Wanneer de vijand actief is, veranderen we eerst zijn richting een beetje, zodat de zwerver niet steeds in een rechte lijn beweegt. We doen dit door onze directionAngle een beetje en het toevoegen van de directionVector naar de snelheid. Vervolgens passen we de snelheid toe, net als in de SeekerControl.

We moeten controleren of de zwerver zich buiten de randen van het scherm bevindt en, zo ja, we veranderen de directionAngle naar een meer geschikte richting, zodat het wordt toegepast in de volgende update.

Eindelijk draaien we de zwerver een beetje. Dit komt alleen maar omdat een draaiende vijand er cooler uitziet.

Nu we allebei de vijanden hebben geïmplementeerd, kun je het spel starten en een beetje spelen. Het geeft je een kleine blik op hoe het spel zal spelen, ook al kun je de vijanden niet doden en kunnen ze jou ook niet doden. Laten we dat volgende toevoegen.

Collision Detection

Om vijanden de speler te laten doden, moeten we weten of ze in botsing komen. Hiervoor zullen we een nieuwe methode toevoegen, handleCollisions, opgeroepen simpleUpdate (float tpf):

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

En nu de daadwerkelijke methode:

 private ongeldige handleCollisions () // moet de speler doodgaan? voor (int i = 0; i 

We herhalen alle vijanden door de hoeveelheid van de kinderen van de node te halen en vervolgens een ieder van hen te krijgen. Verder hoeven we alleen maar te controleren of de vijand de speler doodt wanneer de vijand daadwerkelijk actief is. Als dat niet zo is, hoeven we er niet om te geven. Dus als hij actief is, controleren we of de speler en de vijand botsen. Dat doen we op een andere manier, checkCollisoin (Spatial a, Spatial b):

 private boolean checkCollision (Spatial a, Spatial b) float distance = a.getLocalTranslation (). distance (b.getLocalTranslation ()); float maxDistance = (Float) a.getUserData ("radius") + (Float) b.getUserData ("radius"); terugkeer afstand <= maxDistance; 

Het concept is vrij eenvoudig: eerst berekenen we de afstand tussen de twee ruimtelijkheden. Vervolgens moeten we weten hoe dicht de twee ruimtelijkheden moeten zijn om te worden beschouwd als botsingen, dus we krijgen de straal van elk ruimtelijk gebied en voegen deze toe. (We stellen de gebruikersgegevens in "radius" in getSpatial (String naam) in de vorige zelfstudie.) Dus als de werkelijke afstand korter is dan of gelijk is aan deze maximale afstand, keert de methode terug waar, wat betekent dat ze botsten.

Wat nu? We moeten de speler doden. Laten we een andere methode maken:

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

Eerst ontkoppelen we de speler van zijn bovenliggend knooppunt, waardoor deze automatisch van de scène wordt verwijderd. Vervolgens moeten we de beweging opnieuw instellen PlayerControl-anders kan de speler nog steeds bewegen wanneer hij opnieuw spawnt.

Vervolgens stellen we de userdata in levend naar vals en maak een nieuwe userdata aan dieTime. (We hebben dit nodig om de speler opnieuw in de wacht te slepen als het dood is.)

Eindelijk, we maken alle vijanden los, omdat de speler het moeilijk zou hebben om de reeds bestaande vijanden te bevechten, juist als het spawnt.

We noemden al respawnen, dus laten we het volgende aanpakken. We zullen, opnieuw, de simpleUpdate (float tpf) methode:

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

Dus, als de speler niet leeft en al lang genoeg dood is, zetten we zijn positie naar het midden van het scherm, voegen we hem toe aan de scène en stellen uiteindelijk zijn userdata in levend naar waar nog een keer!

Dit is misschien een goed moment om het spel te starten en onze nieuwe functies te testen. Je zult het echter moeilijk hebben om langer dan twintig seconden te duren, omdat je wapen waardeloos is, dus laten we daar iets aan doen.

Om kogels vijanden te laten doden, zullen we wat code toevoegen aan de handleCollisions () methode:

 // Moet een vijand sterven? int i = 0; terwijl ik < 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++; 

De procedure voor het doden van vijanden is vrijwel hetzelfde als voor het doden van de speler; we herhalen alle vijanden en alle kogels, controleren of ze botsen en als ze dat doen, maken we ze allebei los.

Nu voer het spel uit en kijk hoe ver je komt!

info: Door elke vijand heengaan en zijn positie vergelijken met de positie van elke kogel is een zeer slechte manier om te controleren op botsingen. Het is oke in dit voorbeeld omwille van de eenvoud, maar in een echt game zou je betere algoritmen moeten implementeren om dat te doen, zoals quadtree collision detection. Gelukkig gebruikt de jMonkeyEngine de Bullet physics-engine, dus als u ingewikkelde 3D-fysica hebt, hoeft u zich hier geen zorgen over te maken.

Nu zijn we klaar met de belangrijkste gameplay. We gaan nog steeds zwarte gaten implementeren en de score en het leven van de speler weergeven en om het spel leuker en spannender te maken, zullen we geluidseffecten en betere graphics toevoegen. Dit laatste wordt bereikt door het bloom post processing filter, sommige deeltjeseffecten en een koel achtergrondeffect.

Voordat we dit deel van de serie afmaken, voegen we wat audio en het bloom-effect toe.


Geluiden en muziek afspelen

Om een ​​beetje geluid aan onze game te geven, maken we een nieuwe klasse, gewoon genaamd Geluid:

 public class Sound private AudioNode music; privé opnamen met AudioNode []; privé AudioNode [] explosies; private AudioNode [] spawnt; privé AssetManager assetManager; public Sound (AssetManager assetManager) this.assetManager = assetManager; shots = nieuwe AudioNode [4]; explosies = nieuwe AudioNode [8]; spawns = nieuwe AudioNode [8]; loadSounds ();  private void loadSounds () music = new AudioNode (assetManager, "Sounds / Music.ogg"); music.setPositional (false); music.setReverbEnabled (false); music.setLooping (true); voor (int i = 0; i 

Hier beginnen we met het instellen van het nodige AudioNode variabelen en initialiseer de arrays.

Vervolgens laden we de geluiden en voor elk geluid doen we vrijwel hetzelfde. We creëren een nieuw AudioNode, met behulp van de vermogensbeheerder. Vervolgens stellen we het niet positioneel in en uitschakelen we reverb. (We hebben het geluid niet nodig om positioneel te zijn, omdat we geen stereo-uitvoer hebben in onze 2D-game, hoewel je het zou kunnen implementeren als je dat wilt.) Als je de nagalm uitschakelt, wordt het geluid gespeeld alsof het in de daadwerkelijke audio is het dossier; als we het hadden ingeschakeld, konden we jME laten klinken, zodat het geluid klinkt alsof we in een grot of een kerker zijn, bijvoorbeeld. Daarna hebben we de lus ingesteld op waar voor de muziek en voor vals voor elk ander geluid.

Het spelen van de geluiden is vrij simpel: we bellen gewoon soundX.play ().

info: Wanneer je gewoon belt spelen() bij enig geluid speelt het gewoon het geluid. Maar soms willen we hetzelfde geluid twee keer of zelfs meerdere keren tegelijk spelen. Dat is wat playInstance () is er voor: het creëert een nieuw exemplaar voor elk geluid, zodat we hetzelfde geluid meerdere keren tegelijkertijd kunnen spelen.

Ik laat de rest van het werk aan jou over: je moet bellen startMusic, schieten(), explosie() (voor stervende vijanden), en paaien() op de juiste plaatsen in onze hoofdklasse MonkeyBlasterMain ().

Als je klaar bent, zie je dat het spel nu veel leuker is; die paar geluidseffecten dragen echt bij aan de atmosfeer. Maar laten we de afbeeldingen ook een beetje polijsten.


Het Bloom Post-Processing Filter toevoegen

Bloei inschakelen is heel eenvoudig in de jMonkeyEngine, omdat alle benodigde code en shaders al voor u zijn geïmplementeerd. Ga gewoon door en plak deze regels in simpleInitApp ():

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

Ik heb de BloomFilter een beetje; als je wilt weten waar al deze instellingen voor zijn, bekijk dan de jME-tutorial over bloeien.


Conclusie

Gefeliciteerd met het voltooien van het tweede deel. Er zijn nog drie delen te gaan, dus laat je niet afleiden door te lang te spelen! De volgende keer voegen we de GUI en de zwarte gaten toe.