Faceți un Shooter Vector Neon în jMonkeyEngine Elementele de bază

În această serie de tutorial, voi explica cum să creați un joc inspirat de Geometry Wars, folosind jMonkeyEngine. JMonkeyEngine ("jME" pentru scurt) este un motor de joc open source Java 3D - afla mai multe la site-ul lor sau în ghidul nostru How to Learn jMonkeyEngine.

În timp ce jMonkeyEngine este intrinsec un motor de joc 3D, este posibil, de asemenea, să creați jocuri 2D cu acesta.

postări asemănatoare
Această serie de tutoriale se bazează pe seria lui Michael Hoffman, explicând cum se face același joc în XNA:
  • Faceți un Shooter Vector Neon în XNA

Cele cinci capitole ale tutorialului vor fi dedicate anumitor componente ale jocului:

  1. Inițializați scena 2D, încărcați și afișați câteva imagini, introduceți mânerul.
  2. Adăugați dușmani, coliziuni și efecte sonore.
  3. Adăugați GUI și găuri negre.
  4. Adăugați câteva efecte spectaculoase ale particulelor.
  5. Adăugați grila de fond de bază.

Ca o anticipare vizuală, aici este rezultatul final al eforturilor noastre:


... Iată rezultatele noastre după acest prim capitol:


Muzica și efectele sonore pe care le puteți auzi în aceste videoclipuri au fost create de RetroModular și puteți citi despre cum a făcut-o aici.

Spritele sunt de Jacob Zinman-Jeanes, designerul nostru rezident Tuts +. Toate lucrările pot fi găsite în fișierul sursă de descărcare.


Fontul este Piața Nova, de Wojciech Kalinowski.

Tutorialul este conceput pentru a va ajuta sa invatati elementele de baza ale jMonkeyEngine si sa va creati primul joc cu el. În timp ce vom profita de caracteristicile motorului, nu vom folosi instrumente complicate pentru a îmbunătăți performanța. Ori de câte ori există un instrument mai avansat pentru a implementa o caracteristică, voi lega la tutorialele jME corespunzătoare, dar rămân în modul simplu în tutorialul propriu-zis. Când te uiți mai mult la jME, vei putea să construiești mai târziu și să îți îmbunătățești versiunea de MonkeyBlaster.

Începem!


Prezentare generală

Primul capitol va include încărcarea imaginilor necesare, manipularea intrărilor și transformarea navei jucătorului în mișcare și fotografiere.

Pentru a realiza acest lucru, vom avea nevoie de trei clase:

  • MonkeyBlasterMain: Clasa principală care conține bucla de joc și modul de bază.
  • PlayerControl: Această clasă va determina modul în care se comportă jucătorul.
  • BulletControl: Similar cu cele de mai sus, acest lucru definește comportamentul pentru gloanțele noastre.

În cursul tutorialului vom arunca codul general de joc în MonkeyBlasterMain și să gestionați obiectele de pe ecran, în principal prin comenzi și alte clase. Caracteristici speciale, cum ar fi sunetul, vor avea și propriile clase.


Încărcarea navei jucătorului

Dacă nu ați descărcat încă jME SDK-ul, este timpul! Puteți să o găsiți pe pagina de pornire jMonkeyEngine.

Creați un nou proiect în SDK-ul jME. Acesta va genera în mod automat clasa principală, care va arăta similar cu aceasta:

pachet de monkeyblaster; import com.jme3.app.SimpleApplication; import com.jme3.renderer.RenderManager; clasa publica MonkeyBlasterMain extinde SimpleApplication public static void main (String [] args) Principala aplicatie = noua Main (); app.start ();  @Override public void simpleInitApp ()  @Override public void simpluUpdate (float tpf)  @Override public void simpleRender (RenderManager rm) 

Vom începe prin a trece simpleInitApp (). Această metodă este apelată când începe aplicația. Acesta este locul unde puteți seta toate componentele:

 @Override public void simpleInitApp () // camera de instalare pentru jocuri 2D cam.setParallelProjection (true); cam.setLocation (nou Vector3f (0,0,0,5f)); getFlyByCamera () setEnabled (false). // dezactivați vizualizarea statisticilor (puteți să o lăsați activată, dacă doriți) setDisplayStatView (false); setDisplayFps (false); 

În primul rând, va trebui să reglat camera puțin, deoarece jME este în principiu un motor de joc 3D. Vizualizarea statisticilor din al doilea paragraf poate fi foarte interesantă, dar acesta este modul în care îl dezactivați.

Când începeți jocul acum, puteți vedea ... nimic.

Ei bine, trebuie să încărcăm jucătorul în joc! Vom crea o mică metodă pentru a gestiona încărcarea entităților noastre:

 private Spatial getSpatial (Nume șir) nod nod = nou Nod (nume); // încărcați imaginea Imagine pic = nouă Imagine (nume); Texture2D tex = (Textură2D) assetManager.loadTexture ("Texturi /" + nume + ".png"); pic.setTexture (assetManager, tex, true); // ajustați imaginea float width = tex.getImage () getWidth (); float înălțime = tex.getImage (). getHeight (); pic.setWidth (lățime); pic.setHeight (înălțime); pic.move (-width / 2f, inaltimea / 2f, 0); // adăugați un material la imagine Material picMat = material nou (assetManager, "Common / MatDefs / Gui / Gui.j3md"); . PicMat.getAdditionalRenderState () setBlendMode (BlendMode.AlphaAdditive); node.setMaterial (picMat); // setați raza node.setUserData ("rază", lățime / 2); // atașați imaginea la nod și returnați-o node.attachChild (pic); nod retur; 

La început vom crea un nod care va conține imaginea noastră.

Bacsis: Graficul grafului jME constă din spatials (noduri, imagini, geometrii etc.). Ori de câte ori adăugați ceva spațial la guiNode, devine vizibil în scenă. Vom folosi guiNode deoarece creăm un joc 2D. Puteți atașa spațiu-uri la alte spații și, prin urmare, vă organizați scena. Pentru a deveni un adevărat maestru al graficului scenei, recomand acest tutorial grafic pentru jME.

După crearea nodului, încărcăm imaginea și aplicăm textura corespunzătoare. Aplicarea dimensiunii potrivite imaginii este destul de ușor de înțeles, dar de ce trebuie să o mutăm?

Când încărcați o imagine în jME, centrul de rotație nu este în mijloc, ci mai degrabă într-un colț al imaginii. Dar putem muta imaginea la jumătate din lățimea sa spre stânga și jumătate din înălțimea sa în sus și să o adăugăm la alt nod. Apoi, când rotim nodul părinte, imaginea însăși este rotită în jurul centrului.

Următorul pas este adăugarea unui material în imagine. Un material determină modul în care imaginea va fi afișată. În acest exemplu, folosim materialul GUI implicit și setăm BlendMode la AlphaAdditive. Acest lucru înseamnă că pasajele transparente ale mai multor imagini vor deveni mai luminoase. Acest lucru va fi util mai târziu pentru a face explozii "mai strălucitoare".

În final, adăugăm imaginea noastră la nod și returnează-o.

Acum trebuie să adăugăm jucătorul la guiNode. Vom extinde simpleInitApp un pic mai mult:

// configurați player player = getSpatial ("Player"); player.setUserData ( "viu", adevărat); player.move (setări.getWidth () / 2, settings.getHeight () / 2, 0); guiNode.attachChild (jucator);

Pe scurt: încărcați playerul, configurați unele date, mutați-l în mijlocul ecranului și atașați-l la guiNode pentru a face afișarea.

Datele utilizatorului este pur și simplu unele date pe care le puteți atașa la orice spațiu. În acest caz, adăugăm un boolean și îl sunăm în viaţă, astfel încât să putem căuta dacă jucătorul este în viață. O vom folosi mai târziu.

Acum, conduceți programul! Ar trebui să puteți vedea jucătorul în mijloc. În momentul de față este destul de plictisitor, recunosc. Deci, să adăugăm o acțiune!


Manipularea intrării și mutarea playerului

Introducerea jMonkeyEngine este destul de simplă odată ce ați făcut-o odată. Începem prin implementarea unui Listener de acțiune:

clasa publica MonkeyBlasterMain extinde aplicatiile SimpleApplication ActionListener 

Acum, pentru fiecare cheie, vom adăuga maparea de intrare și ascultător în simpleInitApp ():

 inputManager.addMapping ("stânga", noul KeyTrigger (KeyInput.KEY_LEFT)); inputManager.addMapping ("dreapta", noul KeyTrigger (KeyInput.KEY_RIGHT)); inputManager.addMapping ("sus", noul KeyTrigger (KeyInput.KEY_UP)); inputManager.addMapping ("în jos", noul KeyTrigger (KeyInput.KEY_DOWN)); inputManager.addMapping ("return", noul KeyTrigger (KeyInput.KEY_RETURN)); inputManager.addListener (aceasta, "stânga"); inputManager.addListener (aceasta, "dreapta"); inputManager.addListener (aceasta, "sus"); inputManager.addListener (aceasta, "în jos"); inputManager.addListener (aceasta, "întoarcere");

Ori de câte ori oricare dintre aceste taste este apăsată sau eliberată, metoda onAction se numește. Înainte să ajungem la ce să facem do când este apăsată o tastă, trebuie să adăugăm un control jucătorului nostru.

Info: Controalele reprezintă anumite comportamente ale obiectelor din scenă. De exemplu, puteți adăuga o FightControl si un IdleControl la un AI inamic. În funcție de situație, puteți activa și dezactiva sau atașa și detașa controalele.

Al nostru PlayerControl va avea pur și simplu grijă de mutarea player-ului ori de câte ori este apăsată o tastă, rotindu-l în direcția corectă și asigurându-vă că playerul nu părăsește ecranul.

Poftim:

clasa publică PlayerControl extinde AbstractControl private int screenWidth, screenHeight; // este jucătorul care se mișcă în prezent? public boolean sus, jos, stânga, dreapta; // viteza vitezei flotorului privat al jucătorului = 800f; // lastRotarea playerului privat float lastRotation; public PlayerControl (lățime int, int înălțime) this.screenWidth = width; this.screenHeight = înălțime;  @Override protejat void controlUpdate (float tpf) // mutati jucatorul intr-o anumita directie // daca nu iese din ecran daca (up) if (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 ("raza")) spațial.move (0, tpf * -speed, 0);  spatial.rotate (0,0, -lastRotation + FastMath.PI * 1,5f); lastRotation = FastMath.PI * 1.5f;  altfel dacă (stânga) if (spatial.getLocalTranslation () .x> (float) spatial.getUserData ("raza")) spatial.move (tpf * -speed, 0,0);  spatial.rotate (0,0, -lastRotation + FastMath.PI); lastRotation = FastMath.PI;  altceva dacă (dreapta) if (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;  

Bine; acum, să aruncăm o privire la bucata de cod în parte.

 privat ecran intro, screenHeight; // este jucătorul care se mișcă în prezent? public boolean sus, jos, stânga, dreapta; // viteza vitezei flotorului privat al jucătorului = 800f; // lastRotarea playerului privat float lastRotation; public PlayerControl (lățime int, int înălțime) this.screenWidth = width; this.screenHeight = înălțime; 

În primul rând, inițializăm câteva variabile, definind în ce direcție și cât de repede se mișcă jucătorul și cât de departe este rotit. Apoi, am setat screenWidth și screenHeight, pe care o vom avea nevoie în următoarea metodă mare.

controlUpdate (float tpf) este numit automat de jME la fiecare ciclu de actualizare. Variabila TPF indică timpul de la ultima actualizare. Acest lucru este necesar pentru a controla viteza: dacă unele computere durează de două ori mai mult pentru a calcula o actualizare ca și altele, atunci playerul ar trebui să se miște de două ori până la o singură actualizare pe acele computere.

Acum la prima dacă afirmație:

 dacă (sus) if (spatial.getLocalTranslation (). y < screenHeight - (Float)spatial.getUserData("radius"))  spatial.move(0,tpf*speed,0); 

Verificăm dacă jucătorul merge în sus și, dacă da, verificăm dacă poate merge mai departe. Dacă este suficient de departe de graniță, o mutăm puțin.

Acum pe rotație:

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

Rotim playerul înapoi lastRotation să se confrunte cu direcția sa inițială. Din această direcție, putem roti jucătorul în direcția în care vrem să se uite. În cele din urmă, salvăm rotația reală.

Folosim același tip de logică pentru toate cele patru direcții. reset () metoda este aici doar pentru a seta din nou toate valorile la zero, pentru utilizare atunci când respawning player-ul.

Deci, avem în sfârșit controlul pentru jucătorul nostru. Este timpul să-l adăugăm la spațiul real. Pur și simplu adăugați următoarea linie la simpleInitApp () metodă:

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

Obiectul setări este inclusă în clasă SimpleApplication. Conține informații despre setările de afișare ale jocului.

Dacă începem jocul acum, încă nu se întâmplă nimic. Trebuie să spunem programului ce trebuie să facă atunci când este apăsată una dintre tastele mapate. Pentru a face acest lucru, vom suprascrie onAction metodă:

 public void onAction (numele șirului, boolean isPressed, float tpf) if ((Boolean) player.getUserData ("viu")) if (name.equals ("up")) player.getControl (PlayerControl.class). up = isPressed;  altfel dacă (name.equals ("jos")) player.getControl (PlayerControl.class) .down = isPressed;  altfel dacă (name.equals ("stânga")) player.getControl (PlayerControl.class) .left = isPressed;  altfel dacă (name.equals ("dreapta")) player.getControl (PlayerControl.class) .right = isPressed; 

Pentru fiecare tastă apăsată, îi spunem PlayerControl noul statut al cheii. Acum este timpul să începem jocul și să vedem ceva care se mișcă pe ecran!

Când sunteți mulțumit că înțelegeți elementele de bază ale managementului de comportament și comportament, este timpul să faceți același lucru din nou - de data aceasta, pentru gloanțe.


Adăugarea unor acțiuni bullet

Dacă vrem să avem niște real acțiunea continuă, trebuie să fim capabili să împușcăm niște dușmani. Vom urmări aceeași procedură de bază ca în pasul anterior: gestionarea intrărilor, crearea unor gloanțe și adăugarea unui comportament la acestea.

Pentru a gestiona intrarea mouse-ului, vom implementa un alt ascultător:

clasa publica MonkeyBlasterMain extinde aplicatiile SimpleApplication ActionListener, AnalogListener 

Înainte de a se întâmpla ceva, trebuie să adăugăm cartografiere și ascultător ca în ultima vreme. Vom face asta în simpleInitApp () , alături de altă inițializare a intrărilor:

 inputManager.addMapping ("mousePick", noul MouseButtonTrigger (MouseInput.BUTTON_LEFT)); inputManager.addListener (acest lucru, "mousePick");

Ori de câte ori faceți clic cu mouse-ul, metoda onAnalog este sunat. Înainte de a intra în filmările actuale, trebuie să implementăm o mică metodă de ajutor, Vector3f getAimDirection (), care ne va da direcția de tragere la scăderea poziției jucătorului față de cea a mouse-ului:

 privat Vector3f getAimDirection () Vector2f mouse = inputManager.getCursorPosition (); Vector3f playerPos = player.getLocalTranslation (); Vector3f dif = nou Vector3f (mouse.x-playerPos.x, mouse.y-playerPos.y, 0); retur dif.normalizeLocal (); 
Bacsis: Când atașați obiecte la guiNode, unitățile lor locale de traducere sunt egale cu un pixel. Acest lucru ne face ușor să calculam direcția, deoarece poziția cursorului este specificată și în unitățile de pixeli.

Acum că avem o direcție pentru a trage la, să punem în aplicare fotografia reală:

 public void onAnalog (numele șirului, float value, float tpf) if ((Boolean) player.getUserData ("viu")) if (name.equals ("mousePick")) // shoot Bullet if (System.currentTimeMillis () - bulletCooldown> 83f) bulletCooldown = System.currentTimeMillis (); Vector3f țintă = getAimDirection (); Vector3f offset = Vector3f nou (target.y / 3, -aim.x / 3,0); // init bullet 1 Spațial bullet = getSpatial ("Bullet"); Vector3f finalOffset = scopul.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation () adăugați (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (noul BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (glonț); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()) mult (30); trans = player.getLocalTranslation () adăugați (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (noul BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2); 

Bine, deci, să trecem prin asta:

 dacă (System.currentTimeMillis () - bulletCooldown> 83f) bulletCooldown = System.currentTimeMillis (); Vector3f țintă = getAimDirection (); Vector3f offset = Vector3f nou (target.y / 3, -aim.x / 3,0);

Dacă playerul este în viață și este apăsat butonul mouse-ului, codul nostru verifică mai întâi dacă ultimul împușcat a fost concediat cu cel puțin 83 de minute în urmă (bulletCooldown este o variabilă lungă pe care o inițializăm la începutul clasei). Dacă da, atunci ni se permite să tragem, iar noi calculam direcția corectă pentru direcționare și pentru compensare.

// init bullet 1 Spațial bullet = getSpatial ("Bullet"); Vector3f finalOffset = scopul.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation () adăugați (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (noul BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (glonț); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()) mult (30); trans = player.getLocalTranslation () adăugați (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (noul BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);

Vrem să facem gloanțe gemene, unul lângă celălalt, așa că va trebui să adăugăm o mică compensare fiecăruia dintre ele. O decalare adecvată este ortogonală față de direcția țintă, care este ușor de realizat prin comutarea X și y valorile și negarea uneia dintre ele. Al doilea va fi pur și simplu o negare a primului.

// init bullet 1 Spațial bullet = getSpatial ("Bullet"); Vector3f finalOffset = scopul.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation () adăugați (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (noul BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (glonț); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()) mult (30); trans = player.getLocalTranslation () adăugați (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (noul BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);

Restul ar trebui să pară destul de familiar: inițializăm glonțul folosind propriile noastre getSpatial de la început. Apoi îl traducem în locul potrivit și îl atașăm la nod. Dar așteptați, ce nod?

Ne vom organiza entitățile în noduri specifice, deci este logic să creezi un nod în care să ne atașăm toate gloanțele. Pentru a afișa copiii din acel nod, va trebui să îl atașăm la guiNode.

Inițializarea în simpleInitApp () este destul de simplă:

// configurați bulletNode bulletNode = Nod nou ("gloanțe"); guiNode.attachChild (bulletNode);

Dacă mergeți înainte și începeți jocul, veți putea vedea gloanțele care apar, dar nu se mișcă! Dacă doriți să vă testați, întrerupeți lectura și gândiți-vă pentru ceea ce trebuie să facem pentru a le face să se miște.

...

Ți-ai dat seama?

Trebuie să adăugăm un control la fiecare glonț care va avea grijă de mișcarea sa. Pentru a face acest lucru, vom crea o altă clasă numită BulletControl:

clasa publică BulletControl extinde AbstractControl private int screenWidth, screenHeight; viteza privată a flotorului = 1100f; public Vector3f direcție; rotația privată a flotorului; public BulletControl (direcția Vector3f, int screenWidth, int screenHeight) this.direction = direcție; this.screenWidth = ecranWidth; this.screenHeight = screenHeight;  @Override protejate void controlUpdate (float tpf) // mișcare spatial.move (direction.mult (speed * tpf)); // rotație float actualRotation = MonkeyBlasterMain.getAngleFromVector (direcție); dacă (actualRotation! = rotație) spatial.rotate (0,0, actualRotation - rotation); rotation = actualRotation;  // verificați limitele Vector3f loc = spatial.getLocalTranslation (); dacă (loc.x> ecranWidth || loc.y> screenHeight || loc.x < 0 || loc.y < 0)  spatial.removeFromParent();   @Override protected void controlRender(RenderManager rm, ViewPort vp)  

O privire rapidă asupra structurii clasei arată că este destul de similar cu PlayerControl clasă. Principala diferență este că nu avem chei care să fie verificate și avem un direcţie variabil. Pur și simplu mutați glonțul în direcția sa și rotiți-l corespunzător.

 Vector3f loc = spatial.getLocalTranslation (); dacă (loc.x> ecranWidth || loc.y> screenHeight || loc.x < 0 || loc.y < 0)  spatial.removeFromParent(); 

În ultimul bloc, verificăm dacă glonțul se află în afara limitelor ecranului și, dacă da, îl eliminăm din nodul părinte, care va șterge obiectul.

Este posibil să fi prins această metodă de apel:

MonkeyBlasterMain.getAngleFromVector (direcția);

Se referă la o metodă matematică statică scurtă în clasa principală. Am creat două dintre ele, unul transformând un unghi într-un vector în spațiul 2D, iar celălalt convertește astfel de vectori înapoi într-un unghi.

 static float public getAngleFromVector (Vector3f vec) Vector2f vec2 = Vector2f nou (vec.x, vec.y); retur vec2.getAngle ();  static public Vector3f getVectorFromAngle (unghi de flotare) returnează Vector3f nou (FastMath.cos (unghi), FastMath.sin (unghi), 0); 
Bacsis: Dacă vă simțiți destul de confuz de toate acele operații vectoriale, atunci faceți-vă o favoare și săturați câteva tutoriale despre matematica vectorială. Este esențială atât în ​​spațiul 2D cât și în spațiul 3D. În timp ce sunteți la el, ar trebui să căutați și diferența dintre grade și radiani. Și dacă doriți să obțineți mai mult în programare joc 3D, quaternions sunt minunat, de asemenea ...

Înapoi la prezentarea principală: Am creat un ascultător de intrare, am inițializat două gloanțe și am creat un BulletControl clasă. Singurul lucru rămas este să adăugați o BulletControl la fiecare glonte la inițializare:

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

Acum jocul este mult mai distractiv!



Concluzie

Deși nu este exact provocarea de a zbura și de a împușca câteva gloanțe, puteți cel puțin do ceva. Dar nu dispera - după următorul tutorial, veți avea dificultăți în încercarea de a scăpa de hoardele în creștere ale inamicilor!