Lag en Neon Vector Shooter i jMonkeyEngine Grunnleggende

I denne opplæringsserien forklarer jeg hvordan du lager et spill inspirert av Geometry Wars, ved hjelp av jMonkeyEngine. JMonkeyEngine ("jME" for kort) er en open source 3D Java-spillmotor - finn ut mer på deres nettside eller i vår Hvordan lære jMonkeyEngine guide.

Mens jMonkeyEngine er i utgangspunktet en 3D-spillmotor, er det også mulig å lage 2D-spill med det.

Relaterte innlegg
Denne opplæringsserien er basert på Michael Hoffmans serie som forklarer hvordan man lager det samme spillet i XNA:
  • Lag en Neon Vector Shooter i XNA

De fem kapitlene i opplæringen vil være dedikert til visse komponenter i spillet:

  1. Initialiser 2D-scenen, last og vis litt grafikk, håndter innspill.
  2. Legg til fiender, kollisjoner og lydeffekter.
  3. Legg til GUI og sorte hull.
  4. Legg til noen spektakulære partikkeleffekter.
  5. Legg til bakgrunnsrammen for krumning.

Som en liten visuell forsmak, her er det endelige resultatet av vår innsats:


... Og her er våre resultater etter dette første kapittelet:


Musikken og lydeffekter du kan høre i disse videoene ble laget av RetroModular, og du kan lese om hvordan han gjorde det her.

De sprites er av Jacob Zinman-Jeanes, vår bosatt Tuts + designer. Alt kunstverket kan bli funnet i kildefilen last ned zip.


Skriften er Nova Square, av Wojciech Kalinowski.

Opplæringen er laget for å hjelpe deg med å lære grunnleggende om jMonkeyEngine og lage ditt første spill med det. Mens vi vil dra nytte av motorens egenskaper, vil vi ikke bruke kompliserte verktøy for å forbedre ytelsen. Når det er et mer avansert verktøy for å implementere en funksjon, knytter jeg til de riktige jME-opplæringene, men holder seg til den enkle måten i opplæringen selv. Når du ser på jME mer, vil du senere kunne bygge videre på og forbedre din versjon av MonkeyBlaster.

Her går vi!


Oversikt

Det første kapittelet vil inkludere lasting av nødvendige bilder, håndtering av inngang og å gjøre spillerens skip flytte og skyte.

For å oppnå dette trenger vi tre klasser:

  • MonkeyBlasterMain: Vår hovedklasse inneholder spillsløyfen og den grunnleggende gameplayen.
  • PlayerControl: Denne klassen bestemmer hvordan spilleren oppfører seg.
  • BulletControl: I likhet med det ovenfor definerte dette oppførelsen for våre kuler.

I løpet av opplæringen vil vi kaste den generelle spillkoden i MonkeyBlasterMain og administrer objekter på skjermen, hovedsakelig gjennom kontroller og andre klasser. Spesielle funksjoner, som lyd, vil også ha sine egne klasser.


Laster spillerens skip

Hvis du ikke har lastet ned jME SDK ennå, er det på tide! Du finner den på jMonkeyEngines hjemmeside.

Opprett et nytt prosjekt i jME SDK. Det vil automatisk generere hovedklassen, som vil se ut som denne:

pakke monkeyblaster; importer com.jme3.app.SimpleApplication; importer com.jme3.renderer.RenderManager; offentlig klasse MonkeyBlasterMain utvider SimpleApplication public static void main (String [] args) Main app = new Main (); app.start ();  @Override public void simpleInitApp ()  @Override public void simpleUpdate (float tpf)  @Override public void simpleRender (RenderManager rm) 

Vi begynner med å overstyre simpleInitApp (). Denne metoden blir kalt når programmet starter. Dette er stedet å sette alle komponenter opp:

 @Override public void simpleInitApp () // setup kamera for 2D spill cam.setParallelProjection (true); cam.setLocation (ny Vector3f (0,0,0,5f)); getFlyByCamera () setEnabled (false).; // slå av statistikkvisning (du kan legge den på, hvis du vil) setDisplayStatView (false); setDisplayFps (false); 

Først må vi justere kameraet litt siden jME er i utgangspunktet en 3D-spillmotor. Statistikkvisningen i andre ledd kan være veldig interessant, men slik slår du av.

Når du starter spillet nå, kan du se ... ingenting.

Vel, vi må laste spilleren inn i spillet! Vi lager en liten metode for å håndtere laste våre enheter:

 privat romlig getSpatial (strengnavn) node node = ny node (navn); // laste bilde Bilde bilde = nytt Bilde (navn); Texture2D tex = (Texture2D) assetManager.loadTexture ("Textures /" + name + ".png"); pic.setTexture (assetManager, tex, true); // juster bilde float width = tex.getImage (). getWidth (); flytehøyde = tex.getImage (). getHeight (); pic.setWidth (bredde); pic.setHeight (høyde); pic.move (-Bredde / 2f, -Høyde / 2f, 0); // legge til et materiale på bildet Material picMat = nytt materiale (assetManager, "Common / MatDefs / Gui / Gui.j3md"); . PicMat.getAdditionalRenderState () setBlendMode (BlendMode.AlphaAdditive); node.setMaterial (picMat); // angi radiusen til det romlige // (bruk kun bredde som en enkel tilnærming) node.setUserData ("radius", bredde / 2); // legge bildet til noden og returner det node.attachChild (pic); returknutepunkt; 

Ved starten oppretter vi en node som vil inneholde bildet vårt.

Tips: JME-scenen består av spatials (noder, bilder, geometrier, og så videre). Når du legger til et romlig noe til guiNode, det blir synlig i scenen. Vi vil bruke guiNode fordi vi lager et 2D-spill. Du kan legge til romlige områder til andre romlige områder og organisere derfor scenen din. For å bli en ekte mester på scenediagrammet, anbefaler jeg denne jME-scenen grafveiledningen.

Etter at du har opprettet noden, laster vi bildet og bruker riktig tekstur. Å bruke riktig størrelse på bildet er ganske, er lett å forstå, men hvorfor trenger vi å flytte den?

Når du laster et bilde i jME, er rotasjonssenteret ikke i midten, men heller i et hjørne av bildet. Men vi kan flytte bildet med halvparten av bredden til venstre og halvparten av høyden oppover, og legg den til en annen knute. Da, når vi roterer overordnet noden, roteres selve bildet rundt sentrum.

Det neste trinnet er å legge til et materiale på bildet. Et materiale bestemmer hvordan bildet skal vises. I dette eksemplet bruker vi standard GUI-materiale og angir BlendMode til AlphaAdditive. Dette betyr at overlappende transparente deler av flere bilder blir lysere. Dette vil være nyttig senere å lage eksplosjoner "skinnere".

Til slutt legger vi bildet til noden og returnerer det.

Nå må vi legge til spilleren til guiNode. Vi strekker oss ut simpleInitApp litt mer:

// oppsett spilleren = getSpatial ("Player"); player.setUserData ( "levende", true); player.move (settings.getWidth () / 2, settings.getHeight () / 2, 0); guiNode.attachChild (spiller);

Kort sagt: Vi laster spilleren, konfigurerer noen data, flytter den til midten av skjermen, og legger den til guiNode for å få det til å vises.

Brukerdata er bare noen data du kan knytte til noen romlige. I dette tilfellet legger vi til en boolsk og kaller den i live, slik at vi kan se opp om spilleren er i live. Vi bruker det senere.

Kjør nå programmet! Du bør kunne se spilleren i midten. For øyeblikket er det ganske kjedelig, jeg innrømmer. Så la oss legge til litt handling!


Håndtering av inngang og flytting av spilleren

jMonkeyEngine-inngangen er ganske enkel når du har gjort det en gang. Vi starter med å implementere en Action Listener:

offentlig klasse MonkeyBlasterMain utvider SimpleApplication implementerer ActionListener 

Nå, for hver nøkkel, legger vi inn kartlegging og lytteren i simpleInitApp ():

 inputManager.addMapping ("left", ny KeyTrigger (KeyInput.KEY_LEFT)); inputManager.addMapping ("right", ny KeyTrigger (KeyInput.KEY_RIGHT)); inputManager.addMapping ("opp", ny KeyTrigger (KeyInput.KEY_UP)); inputManager.addMapping ("ned", ny KeyTrigger (KeyInput.KEY_DOWN)); inputManager.addMapping ("return", ny KeyTrigger (KeyInput.KEY_RETURN)); inputManager.addListener (dette, "venstre"); inputManager.addListener (dette, "høyre"); inputManager.addListener (dette, "opp"); inputManager.addListener (dette, "nede"); inputManager.addListener (dette, "return");

Når noen av disse nøklene trykkes eller slippes, må metoden VedHandling er kalt. Før vi kommer inn i hva som egentlig skal gjøre Når noen taster trykkes, må vi legge til en kontroll til vår spiller.

info: Kontroller representerer visse oppføringer av objekter i scenen. For eksempel kan du legge til en FightControl og en IdleControl til en fiende AI. Avhengig av situasjonen kan du aktivere og deaktivere eller legge til og ta av kontrollene.

Våre PlayerControl vil rett og slett passe på å flytte spilleren når en tast trykkes, roterer den i riktig retning og sørger for at spilleren ikke forlater skjermen.

Værsågod:

offentlig klasse PlayerControl utvider AbstractControl private int screenWidth, screenHeight; // går spilleren for tiden? offentlig boolean opp, ned, venstre, høyre; // hastighet på spilleren privat float speed = 800f; // sistRotering av spilleren privat float lastRotation; offentlig PlayerControl (int bredde, int høyde) this.screenWidth = width; this.screenHeight = height;  @Override protected void controlUpdate (float tpf) // flytt spilleren i en bestemt retning // hvis han ikke er ute av skjermen hvis (opp) hvis (spatial.getLocalTranslation (). Y < screenHeight - (Float)spatial.getUserData("radius"))  spatial.move(0,tpf*speed,0);  spatial.rotate(0,0,-lastRotation + FastMath.PI/2); lastRotation=FastMath.PI/2;  else if (down)  if (spatial.getLocalTranslation().y > (Float) spatial.getUserData ("radius")) spatial.move (0, tpf * -speed, 0);  romlig.rotat (0,0, -lastrotasjon + FastMath.PI * 1,5f); lastRotation = FastMath.PI * 1.5f;  annet hvis (venstre) hvis (spatial.getLocalTranslation () .x> (Float) spatial.getUserData ("radius")) spatial.move (tpf * -speed, 0,0);  spatial.rotate (0,0, -lastRotation + FastMath.PI); lastRotation = FastMath.PI;  annet hvis (høyre) hvis (spatial.getLocalTranslation (). x < screenWidth - (Float)spatial.getUserData("radius"))  spatial.move(tpf*speed,0,0);  spatial.rotate(0,0,-lastRotation + 0); lastRotation=0;   @Override protected void controlRender(RenderManager rm, ViewPort vp)  // reset the moving values (i.e. for spawning) public void reset()  up = false; down = false; left = false; right = false;  

Greit; nå, la oss ta en titt på kodestykket for hverandre.

 privat int skjermbredde, skjermhøyde; // går spilleren for tiden? offentlig boolean opp, ned, venstre, høyre; // hastighet på spilleren privat float speed = 800f; // sistRotering av spilleren privat float lastRotation; offentlig PlayerControl (int bredde, int høyde) this.screenWidth = width; this.screenHeight = height; 

Først starter vi noen variabler, definerer i hvilken retning og hvor raskt spilleren beveger seg, og hvor langt den roteres. Så satte vi screenWidth og screenHeight, som vi trenger i den neste store metoden.

controlUpdate (float tpf) blir automatisk kalt av jME hver oppdateringsperiode. Variabelen TPF Indikerer tiden siden den siste oppdateringen. Dette er nødvendig for å kontrollere hastigheten: Hvis noen datamaskiner tar dobbelt så lang tid for å beregne en oppdatering som andre, må spilleren flytte to ganger så langt i en enkelt oppdatering på disse datamaskinene.

Nå til den første hvis uttalelse:

 hvis (opp) hvis (spatial.getLocalTranslation (). y < screenHeight - (Float)spatial.getUserData("radius"))  spatial.move(0,tpf*speed,0); 

Vi kontrollerer om spilleren går opp, og i så fall kontrollerer vi om det kan gå lenger. Hvis det er langt nok unna grensen, beveger vi oss bare litt opp.

Nå på rotasjonen:

 spatial.rotate (0,0, -lastRotation + FastMath.PI / 2); lastRotation = FastMath.PI / 2;

Vi roterer spilleren igjen av lastRotation for å møte sin opprinnelige retning. Fra denne retningen kan vi rotere spilleren i den retningen vi vil se på. Endelig lagrer vi den faktiske rotasjonen.

Vi bruker den samme typen logikk for alle fire retninger. De tilbakestille() Metoden er bare her for å sette alle verdier til null igjen, for bruk når du respekterer spilleren.

Så, vi har endelig kontrollen for vår spiller. Det er på tide å legge det til selve romlige. Bare legg til følgende linje til simpleInitApp () metode:

player.addControl (ny PlayerControl (settings.getWidth (), settings.getHeight ()));

Objektet innstillinger er inkludert i klassen SimpleApplication. Den inneholder data om skjerminnstillingene for spillet.

Hvis vi starter spillet nå, er det fortsatt ingenting som skjer ennå. Vi må fortelle programmet hva som skal gjøres når en av de kortlagte tastene trykkes. For å gjøre dette overstyrer vi VedHandling metode:

 Offentlig tomgang onAction (Stringnavn, boolsk erPressed, float tpf) if ((Boolean) player.getUserData ("live")) if (name.equals ("up")) player.getControl (PlayerControl.class). up = isPressed;  annet hvis (name.equals ("down")) player.getControl (PlayerControl.class) .down = isPressed;  annet hvis (name.equals ("left")) player.getControl (PlayerControl.class) .left = isPressed;  annet hvis (name.equals ("right")) player.getControl (PlayerControl.class) .right = isPressed; 

For hver trykknapp, forteller vi PlayerControl Ny status for nøkkelen. Nå er det endelig tid til å starte spillet vårt og se noe som beveger seg på skjermen!

Når du er glad for at du forstår grunnleggende om inngangs- og adferdshåndtering, er det på tide å gjøre det samme igjen - denne gangen for kulene.


Legger til litt kulehandling

Hvis vi vil ha noen ekte handling skjer, må vi kunne skyte noen fiender. Vi skal følge den samme grunnleggende prosedyren som i forrige trinn: administrere innspill, opprette noen kuler og legge til en oppførsel for dem.

For å håndtere musinngang, implementerer vi en annen lytter:

offentlig klasse MonkeyBlasterMain utvider SimpleApplication implementerer ActionListener, AnalogListener 

Før det skjer noe, må vi legge til kartleggingen og lytteren som vi gjorde sist. Vi gjør det i simpleInitApp () metode, sammen med den andre inngangsinitialiseringen:

 inputManager.addMapping ("mousePick", ny MouseButtonTrigger (MouseInput.BUTTON_LEFT)); inputManager.addListener (dette, "mousePick");

Når vi klikker med musen, må metoden onAnalog blir kalt. Før vi kommer inn i selve skytingen, må vi implementere en liten hjelpemetode, Vector3f getAimDirection (), som vil gi oss retningen til å skyte på ved å trekke posisjonen til spilleren fra musens mus:

 privat Vector3f getAimDirection () Vector2f mouse = inputManager.getCursorPosition (); Vector3f playerPos = player.getLocalTranslation (); Vector3f dif = ny Vector3f (mouse.x-playerPos.x, mouse.y-playerPos.y, 0); return dif.normalizeLocal (); 
Tips: Når du legger objekter til guiNode, deres lokale oversettelsesenheter er lik en pixel. Dette gjør det enkelt for oss å beregne retningen, siden markørposisjonen også er angitt i pikselaggregatene.

Nå som vi har en retning å skyte på, la oss implementere den faktiske skytingen:

 Offentlig ugyldig påAnalog (Strenge navn, flyt verdi, float tpf) if ((Boolean) player.getUserData ("live")) if (name.equals ("mousePick")) // skyt Bullet if (System.currentTimeMillis () - bulletCooldown> 83f) bulletCooldown = System.currentTimeMillis (); Vector3f aim = getAimDirection (); Vector3f offset = ny Vector3f (aim.y / 3, -im.x / 3.0); // init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (ny BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (ny BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2); 

Ok, så, la oss gå gjennom dette:

 hvis (System.currentTimeMillis () - bulletCooldown> 83f) bulletCooldown = System.currentTimeMillis (); Vector3f aim = getAimDirection (); Vector3f offset = ny Vector3f (aim.y / 3, -im.x / 3.0);

Hvis spilleren er i live og museknappen klikkes, sjekker vår kode først om det siste bildet ble sparket minst 83 ms siden (bulletCooldown er en lang variabel vi initialiserer i begynnelsen av klassen). I så fall kan vi skyte, og vi beregner riktig retning for sikte og offset.

// init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (ny BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (ny BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);

Vi ønsker å gyte to kuler, den ene ved siden av den andre, så vi må legge til litt offset til hver av dem. Et passende forskyvning er ortogonalt i retningsretningen, som lett oppnås ved å bytte x og y verdier og negere en av det. Den andre vil rett og slett være en negasjon av den første.

// init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (ny BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (ny BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);

Resten skal virke ganske kjent: Vi initierer kulen ved å bruke vår egen getSpatial metode fra begynnelsen. Da oversetter vi det til rett sted og legger det til noden. Men vent, hvilken knutepunkt?

Vi organiserer våre enheter i bestemte noder, så det er fornuftig å lage en knutepunkt hvor vi kan knytte alle våre kuler til. For å vise barna til den noden, må vi legge den til guiNode.

Initialiseringen i simpleInitApp () er ganske grei:

// oppsett bulletNode bulletNode = ny knutepunkt ("kuler"); guiNode.attachChild (bulletNode);

Hvis du går videre og starter spillet, kan du se kulene dukker opp, men de beveger seg ikke! Hvis du vil teste deg selv, pause lesing og tenk selv hva vi må gjøre for å få dem til å flytte.

...

Fikk du det ut?

Vi må legge til en kontroll til hver kule som skal ta vare på bevegelsen. For å gjøre dette, oppretter vi en annen klasse som heter BulletControl:

offentlig klasse BulletControl utvider AbstractControl private int screenWidth, screenHeight; privat flytehastighet = 1100f; offentlig Vector3f retning; privat floatrotasjon; offentlig BulletControl (Vector3f retning, int screenWidth, int screenHeight) this.direction = retning; this.screenWidth = screenWidth; this.screenHeight = screenHeight;  @Override protected void controlUpdate (float tpf) // bevegelse spatial.move (direction.mult (speed * tpf)); // rotasjonsflyt actualRotation = MonkeyBlasterMain.getAngleFromVector (retning); hvis (actualRotation! = rotasjon) spatial.rotate (0,0, actualRotation - rotation); rotasjon = actualRotation;  // sjekke grenser Vector3f loc = spatial.getLocalTranslation (); hvis (loc.x> screenWidth || loc.y> screenHeight || loc.x < 0 || loc.y < 0)  spatial.removeFromParent();   @Override protected void controlRender(RenderManager rm, ViewPort vp)  

Et raskt blikk på strukturen i klassen viser at den er ganske lik den PlayerControl klasse. Hovedforskjellen er at vi ikke har noen nøkler som skal kontrolleres, og vi har a retning variabel. Vi beveger rett og slett kulen i retning og roterer den tilsvarende.

 Vector3f loc = spatial.getLocalTranslation (); hvis (loc.x> screenWidth || loc.y> screenHeight || loc.x < 0 || loc.y < 0)  spatial.removeFromParent(); 

I den siste blokken kontrollerer vi om kulen er utenfor skjermgrensene, og i så fall fjerner vi den fra sin overordnede node, som vil slette objektet.

Du kan ha fanget denne metoden samtale:

MonkeyBlasterMain.getAngleFromVector (retning);

Det refererer til en kort statisk matematisk hjelpemetode i hovedklassen. Jeg opprettet to av dem, en konverterer en vinkel i en vektor i 2D-rom og den andre konverterer slike vektorer tilbake i en vinkelverdi.

 offentlig statisk flyte getAngleFromVector (Vector3f vec) Vector2f vec2 = ny Vector2f (vec.x, vec.y); returnere vec2.getAngle ();  offentlig statisk Vector3f getVectorFromAngle (flytvinkel) returner ny Vector3f (FastMath.cos (vinkel), FastMath.sin (vinkel), 0); 
Tips: Hvis du føler deg ganske forvirret av alle disse vektoroperasjonene, gjør deg selv en tjeneste og graver deg til noen opplæringsprogrammer om vektormatematikk. Det er viktig i både 2D og 3D-rom. Mens du er i det, bør du også slå opp forskjellen mellom grader og radianer. Og hvis du vil ha mer inn i 3D-programmering, er quaternions også kjempebra ...

Nå tilbake til hovedoversikten: Vi opprettet en inngangslytter, initierte to kuler og opprettet en BulletControl klasse. Det eneste som igjen er å legge til en BulletControl til hver kule når du initialiserer den:

bullet.addControl (ny BulletControl (aim, settings.getWidth (), settings.getHeight ()));

Nå er spillet mye morsommere!



Konklusjon

Mens det ikke er akkurat utfordrende å fly rundt og skyte noen kuler, kan du i det minste gjøre noe. Men fortvil ikke - etter den neste opplæringen har du det vanskelig å prøve å unnslippe de voksende horder av fiender!