Introducere în JavaFX pentru dezvoltarea jocurilor

JavaFX este un set de instrumente GUI pentru platforme Java, care este succesorul bibliotecilor Java Swing. În acest tutorial, vom explora caracteristicile JavaFX care îl fac ușor de utilizat pentru a începe jocurile de programare în Java.

Acest tutorial presupune că deja știți cum să codificați în Java. Dacă nu, consultați Aflați Java pentru Android, Introducere în programarea pe calculator cu Java: 101 și 201, Head First Java, Greenfoot sau Aflați Java calea grea pentru a începe.

Instalare

Dacă deja dezvoltați aplicații cu Java, probabil că nu trebuie să descărcați nimic: JavaFX a fost inclus împreună cu pachetul standard JDK (Java Development Kit) de la JDK versiunea 7u6 (august 2012). Dacă nu ați actualizat instalarea Java într-un timp, vizitați site-ul Web de descărcare Java pentru cea mai recentă versiune. 

Clasele-cadru de bază

Crearea unui program JavaFX începe cu clasa Application, din care toate aplicațiile JavaFX sunt extinse. Clasa principală ar trebui să apeleze lansa() metoda, care va apela apoi init () și apoi start() , așteptați terminarea aplicației și apoi apelați Stop() metodă. Din aceste metode, numai start() metoda este abstractă și trebuie suprimată.

Clasa Stage este containerul JavaFX de nivel superior. Când se lansează o aplicație, se creează o etapă inițială și se trece la metoda de pornire a aplicației. Scenele controlează proprietățile ferestrelor de bază, cum ar fi titlul, pictograma, vizibilitatea, capacitatea de redimensionare, modul ecran complet și decorațiile; acesta din urmă este configurat utilizând StageStyle. Etapele suplimentare pot fi construite după cum este necesar. După ce o etapă este configurată și conținutul este adăugat, spectacol() se numește metoda.

Cunoscând toate acestea, putem scrie un exemplu minim care lansează o fereastră în JavaFX:

import javafx.application.Application; import javafx.stage.Stage; clasa publica Exemplu1 extinde aplicatia public static void main (String [] args) lansare (args);  void public start (Stage theStage) theStage.setTitle ("Bună ziua, lumea!"); theStage.show (); 

Structurarea conținutului

Conținutul în JavaFX (cum ar fi textul, imaginile și comenzile UI) este organizat folosind o structură de date asemănătoare unui arbore, cunoscută sub numele de grafic scenă, care grupează și aranjează elementele unei scene grafice. 

Reprezentarea unui grafic JavaFX Scene.

Un element general al unui grafic de scenă în JavaFX se numește Nod. Fiecare nod dintr-un arbore are un singur nod "părinte", cu excepția unui nod special desemnat drept "rădăcină". Un grup este un nod care poate avea mai multe elemente Node "copil". Transformările grafice (traducerea, rotația și scara) și efectele aplicate unui grup se aplică și copiilor săi. Nodurile pot fi personalizate folosind foi de stil cascadă Java (CSS), destul de asemănătoare cu CSS-ul folosit pentru formarea documentelor HTML.

Clasa Scene conține tot conținutul pentru un grafic de scenă și necesită setarea unui nod rădăcină (în practică, acesta este adesea un grup). Puteți seta dimensiunea unei scene în mod specific; în caz contrar, dimensiunea unei scene va fi calculată automat în funcție de conținutul acesteia. Un obiect Scene trebuie să fie transferat în Etapă (cu setScene () metoda) pentru a fi afișată.

Rendering Graphics

Realizarea graficelor este deosebit de importantă pentru programatorii de jocuri! În JavaFX, obiectul Canvas este o imagine pe care putem desena text, forme și imagini folosind obiectul GraphicsContext asociat. (Pentru dezvoltatorii familiarizați cu setul de instrumente Java Swing, acest lucru este similar cu obiectul Graphics trecut la a picta() în clasa JFrame.)

Obiectul GraphicsContext conține o multitudine de abilități puternice de personalizare. Pentru a alege culorile pentru desenarea textului și a formelor, puteți seta culorile de umplere (interior) și curse (margine), care sunt obiecte Paint: acestea pot fi un singur culor solid, un gradient definit de utilizator (fie LinearGradient sau RadialGradient) chiar și un ImagePattern. De asemenea, puteți aplica unul sau mai multe obiecte de stil Effect, cum ar fi Lighting, Shadow sau GaussianBlur, și puteți schimba fonturile din setările implicite utilizând clasa Font. 

Clasa imagine facilitează încărcarea imaginilor dintr-o varietate de formate din fișiere și desenarea acestora prin intermediul clasei GraphicsContext. Este ușor să construiți imagini generate procedural utilizând clasa WritableImage împreună cu clasele PixelReader și PixelWriter.

Folosind aceste clase, putem scrie un exemplu mult mai demn de "Hello, World" după cum urmează. Pentru scurtcircuit, vom include doar start() aici (vom trece peste instrucțiunile de import și principal() metodă); cu toate acestea, codul sursă complet de lucru poate fi găsit în replica GitHub care însoțește acest tutorial.

public void start (Stage theStage) theStage.setTitle ("Exemplul canvasului"); Grup rădăcină = grup nou (); Scena scena = Scena nouă (rădăcină); Starea.setScene (Scena); Canvas panza = pânză nouă (400, 200); root.getChildren () adăugați (canvas); GraphicsContext gc = canvas.getGraphicsContext2D (); gc.setFill (Color.RED); gc.setStroke (Color.BLACK); gc.setLineWidth (2); Fonturile theFont = Font.font ("Times New Roman", FontWeight.BOLD, 48); gc.setFont (theFont); gc.fillText ("Bună ziua, lumea!", 60, 50); gc.strokeText ("Bună ziua, lumea!", 60, 50); Imagine pământ = imagine nouă ("earth.png"); gc.drawImage (pământ, 180, 100); theStage.show (); 

Jocul Loop

Apoi, trebuie să facem programele noastre dinamic, ceea ce înseamnă că starea jocului se schimbă în timp. Vom implementa o buclă de joc: o buclă infinită care actualizează obiectele de joc și redă scena pe ecran, în mod ideal la o rată de 60 de ori pe secundă. 

Cea mai ușoară modalitate de a realiza acest lucru în JavaFX este utilizarea clasei AnimationTimer, unde o metodă (numită mâner()) poate fi scrisă care va fi apelată la o rată de 60 de ori pe secundă sau cât mai aproape posibil de această rată. (Această clasă nu trebuie folosită exclusiv în scopuri de animație, este capabilă de mult mai mult.)

Folosind clasa AnimationTimer este un pic dificil: deoarece este o clasă abstractă, nu poate fi creată direct - clasa trebuie extinsă înainte ca o instanță să poată fi creată. Cu toate acestea, pentru exemplele noastre simple, vom extinde clasa scriind o clasă anonimă interioară. Această clasă interioară trebuie să definească metoda abstractă mâner(), care va fi trecut printr-un singur argument: timpul curent al sistemului în nanosecunde. După definirea clasei interioare, invocăm imediat start() , care începe buclele. (Buclele pot fi oprite apelând Stop() metodă.)

Cu aceste clase, putem modifica exemplul nostru "Hello, World", creând o animație compusă din Pământul care orbitează în jurul Soarelui cu o imagine de fundal înstelată.

public void start (Stage theStage) theStage.setTitle ("Exemplu de timp"); Grup rădăcină = grup nou (); Scena scena = Scena nouă (rădăcină); Starea.setScene (Scena); Canvas panza = pânză nouă (512, 512); root.getChildren () adăugați (canvas); GraphicsContext gc = canvas.getGraphicsContext2D (); Imagine pământ = imagine nouă ("earth.png"); Imagine soare = imagine nouă ("sun.png"); Spațiu de imagine = imagine nouă ("space.png"); ultimul început lungNanoTime = System.nanoTime (); noul AnimationTimer () mânerul public void (lungulNanoTime) dublu t = (currentNanoTime - startNanoTime) / 1000000000.0; dublu x = 232 + 128 * Math.cos (t); dublu y = 232 + 128 * Math.sin (t); // background image șterge pânza gc.drawImage (spațiu, 0, 0); gc.drawImage (pământ, x, y); gc.drawImage (soare, 196, 196);  .start(); theStage.show (); 

Există moduri alternative de a implementa o buclă de joc în JavaFX. O abordare puțin mai lungă (dar mai flexibilă) implică clasa Cronologie, care este o secvență de animație constând dintr-un set de obiecte KeyFrame. Pentru a crea o buclă de joc, cronologia trebuie setată să se repete pe o perioadă nedefinită și este necesară o singură cheie KeyFrame, durata acesteia fiind setată la 0,016 secunde (pentru a atinge 60 de cicluri pe secundă). Această implementare poate fi găsită în Example3T.java fișier în repo GitHub.

Grafică pe bază de animație

O altă componentă de programare a jocurilor este în mod obișnuit animația bazată pe cadre: afișarea unei secvențe de imagini în succesiune rapidă pentru a crea iluzia mișcării. 

Presupunând că toate buletinele de animație și toate cadrele sunt afișate pentru același număr de secunde, o implementare de bază ar putea fi la fel de simplă, după cum urmează:

clasa publica AnimatedImage public Image [] cadre; dublă durată publică; public imagine getFrame (timp dublu) int index = (int) ((timp% (frames.length * duration)) / durată); cadre de retur [index]; 

Pentru a integra această clasă în exemplul anterior, am putea crea un OZN animat, inițializând obiectul folosind codul:

AnimatedImage ufo = animație animată nouă (); Imagine [] imageArray = imagine nouă [6]; pentru (int i = 0; i < 6; i++) imageArray[i] = new Image( "ufo_" + i + ".png" ); ufo.frames = imageArray; ufo.duration = 0.100;

... și, în cadrul AnimationTimer, adăugând linia unică de cod:

gc.drawImage (ufo.getFrame (t), 450, 25); 

... la locul potrivit. Pentru un exemplu complet de cod de lucru, consultați fișierul Example3AI.java în repo GitHub. 

Manipularea intrărilor de utilizatori

Detectarea și procesarea intrărilor de utilizatori în JavaFX este simplă. Sunt apelate acțiunile utilizatorului care pot fi detectate de sistem, cum ar fi apăsările de taste și clicurile de mouse evenimente. În JavaFX, aceste acțiuni generează automat generarea de obiecte (cum ar fi KeyEvent și MouseEvent) care stochează datele asociate (cum ar fi tasta reală apăsată sau locația cursorului mouse-ului). Orice clasă JavaFX care implementează clasa EventTarget, cum ar fi o scenă, poate "asculta" evenimentele și să le gestioneze; în exemplele care urmează, vom arăta cum să configurați o scenă pentru a procesa diverse evenimente.

Privind prin documentația pentru clasa Scene, există multe metode care asculta pentru manipularea diferitelor tipuri de intrări din diferite surse. De exemplu, metoda setOnKeyPressed () poate atribui un EventHandler care se va activa la apăsarea unei taste, metoda setOnMouseClicked () poate atribui un EventHandler care se activează atunci când este apăsat un buton al mouse-ului și așa mai departe. Clasa EventHandler servește unui scop: de a încapsula o metodă (numită mâner()) care se numește atunci când apare evenimentul corespunzător. 

Când creați un EventHandler, trebuie să specificați tip de eveniment pe care îl ocupă: poți declara un Organizatorul evenimentului sau an Organizatorul evenimentului, de exemplu. De asemenea, EventHandlers sunt adesea create ca clase interioare anonime, deoarece acestea sunt de obicei folosite doar o dată (când sunt transmise ca argument la una din metodele enumerate mai sus).

Manipularea evenimentelor tastaturii

Intrarea utilizatorilor este adesea procesată în cadrul principalei buclă de jocuri și, prin urmare, trebuie păstrată o înregistrare a cărora cheile sunt active în prezent. O modalitate de a realiza acest lucru este crearea unui obiect ArrayList of String. Atunci când o tastă este apăsată inițial, adăugăm reprezentarea String a keyCode-ului KeyEvent în listă; când cheia este eliberată, o eliminăm din listă. 

În exemplul de mai jos, panza conține două imagini ale tastelor săgeți; ori de câte ori este apăsată o tastă, imaginea corespunzătoare devine verde. 


Codul sursă este conținut în fișier Example4K.java în repo GitHub.

public void start (Stage theStage) theStage.setTitle ("Exemplu de tastatură"); Grup rădăcină = grup nou (); Scena scena = Scena nouă (rădăcină); Starea.setScene (Scena); Panza canvas = pânză nouă (512 - 64, 256); root.getChildren () adăugați (canvas); ArrayList input = noul ArrayList(); theScene.setOnKeyPressed (noul EventHandler() mâner public void (KeyEvent e) Codul șirului = e.getCode (). toString (); // adăugați o singură dată ... prevenirea duplicatelor dacă (! input.contains (code)) input.add (code); ); theScene.setOnKeyReleased (noul EventHandler() mâner public void (KeyEvent e) Codul șirului = e.getCode (). toString (); input.remove (cod); ); GraphicsContext gc = canvas.getGraphicsContext2D (); Imagine din stânga = imagine nouă ("left.png"); Imagine leftG = imagine nouă ("leftG.png"); Imagine dreapta = imagine nouă ("right.png"); Imaginea rightG = imagine nouă ("rightG.png"); new AnimationTimer () mâner public void (lungNanoNetNew) // Șterge pânza gc.clearRect (0, 0, 512,512); dacă (input.contains ("LEFT")) gc.drawImage (leftG, 64, 64); altceva gc.drawImage (stânga, 64, 64); dacă (input.contains ("DREAPTA")) gc.drawImage (rightG, 256, 64); altceva gc.drawImage (dreapta, 256, 64);  .start(); theStage.show (); 

Manipularea evenimentelor mouse-ului

Acum, să examinăm un exemplu care se concentrează mai degrabă pe clasa MouseEvent decât pe clasa KeyEvent. În acest mini-joc, jucătorul câștigă un punct de fiecare dată când este țintit clicul.


Deoarece Event Handlers sunt clase interioare, orice variabile pe care le folosesc trebuie să fie definitive sau "efectiv finale", ceea ce înseamnă că variabilele nu pot fi reinitializate. În exemplul anterior, datele au fost transmise EventHandler-ului printr-un ArrayList, ale cărui valori pot fi modificate fără reinitializare (prin intermediul adăuga() și elimina() metode). 

Cu toate acestea, în cazul tipurilor de date de bază, valorile nu pot fi modificate odată inițializate. Dacă doriți ca EventHandler să acceseze tipurile de date de bază care sunt modificate în altă parte a programului, puteți crea o clasă de wrapper care conține fie variabile publice, fie metode getter / setter. (În exemplul de mai jos, intValue este o clasă care conține a public int variabilă numită valoare.)

public void start (Stage theStage) theStage.setTitle ("Faceți clic pe țintă!"); Grup rădăcină = grup nou (); Scena scena = Scena nouă (rădăcină); Starea.setScene (Scena); Panza canvas = panza nouă (500, 500); root.getChildren () adăugați (canvas); Cercul țintăData = nou Cerc (100,100,32); Puncte IntValue = IntValue nou (0); theScene.setOnMouseClicked (noul EventHandler() public void handle (MouseEvent e) if (targetData.containsPoint (e.getX (), e.getY ())) dublu x = 50 + 400 * Math.random (); dublu y = 50 + 400 * Math.random (); targetData.setCenter (x, y); points.value ++;  altfel points.value = 0; ); GraphicsContext gc = canvas.getGraphicsContext2D (); Fonturile theFont = Font.font ("Helvetica", FontWeight.BOLD, 24); gc.setFont (theFont); gc.setStroke (Color.BLACK); gc.setLineWidth (1); Imagine bullseye = imagine nouă ("bullseye.png"); new AnimationTimer () mâner public void (long timeNanoTime) // Șterge pânza gc.setFill (culoare nouă (0.85, 0.85, 1.0, 1.0)); gc.fillRect (0,0, 512,512); gc.drawImage (bullseye, targetData.getX () - targetData.getRadius (), targetData.getY () - targetData.getRadius ()); gc.setFill (Color.BLUE); String pointsText = "Puncte:" + punct.value; gc.fillText (puncteText, 360, 36); gc.strokeText (puncteText, 360, 36);  .start(); theStage.show (); 

Codul sursă complet este conținut în repo GitHub; clasa principală este Example4M.java.

Crearea unei clase de bază Sprite cu JavaFX

În jocurile video, a spiriduș este termenul pentru o singură entitate vizuală. Mai jos este un exemplu de clasă Sprite care stochează o imagine și o poziție, precum și informații despre viteza (pentru entitățile mobile) și informații privind lățimea / înălțimea pe care să le utilizeze la calcularea cutiilor de margine în scopul detectării coliziunilor. De asemenea, avem cele mai multe metode getter / setter standard pentru cele mai multe dintre aceste date (omise pentru scurtcircuit) și câteva metode standard necesare dezvoltării jocurilor:

  • Actualizați(): calculează noua poziție pe baza vitezei Sprite.
  • face(): desenează imaginea asociată la pânză (prin clasa GraphicsContext) folosind poziția ca coordonate.
  • getBoundary (): returnează un obiect JavaFX Rectangle2D, util în detectarea coliziunilor datorită metodei intersecte.
  • intersecteaza (): determină dacă caseta de legare a acestui Sprite se intersectează cu cea a unui alt Sprite.
clasa publică Sprite privată Imagine imagine; poziția dublă privatăX; poziția dublă privatăY; dublă velocityX privată; viteza privată dublă; lățime dublă privată; dublă înălțime privată; // // // metode omitate pentru scurtătate // ... public void update (timp dublu) positionX + = velocityX * time; pozițiaY + = vitezaY * timp;  public void rendere (GraphicsContext gc) gc.drawImage (imagine, pozițieX, pozițieY);  public Rectangle2D getBoundary () returnează noul Rectangle2D (pozițiaX, pozițiaY, lățimea, înălțimea);  intersecte publice booleene (Sprite s) returnați s.getBoundary () intersectează (this.getBoundary ()); 

Este inclus codul sursă complet Sprite.java în repo GitHub.

Utilizând clasa Sprite

Cu ajutorul clasei Sprite, putem crea cu ușurință un simplu joc de colectare în JavaFX. În acest joc, îți asumi rolul de servietă sentientă, al cărei scop este să colectezi multe saci de bani care au fost lăsați în jurul valorii de un proprietar anterioară neglijent. Tastele cu săgeți deplasează playerul în jurul ecranului.

Acest cod împrumută foarte mult din exemplele anterioare: configurarea fonturilor pentru a afișa scorul, stocarea intrărilor de la tastatură cu un ArrayList, implementarea bucla de joc cu un AnimationTimer și crearea de clase de împachetare pentru valori simple care trebuie modificate în timpul jocului.

Un segment de cod de interes special implică crearea unui obiect Sprite pentru player (servietă) și un ArrayList de obiecte Sprite pentru colecții (pungi de bani):

Sprite servietă = Sprite nou (); briefcase.setImage ( "briefcase.png"); briefcase.setPosition (200, 0); ArrayList moneybagList = noul ArrayList(); pentru (int i = 0; i < 15; i++)  Sprite moneybag = new Sprite(); moneybag.setImage("moneybag.png"); double px = 350 * Math.random() + 50; double py = 350 * Math.random() + 50; moneybag.setPosition(px,py); moneybagList.add( moneybag ); 

Un alt segment de cod care prezintă interes este crearea unui AnimationTimer, care are sarcina:

  • calculând timpul scurs de la ultima actualizare
  • stabilind viteza jucătorului în funcție de tastele apăsate în prezent
  • efectuarea detecției de coliziune între player și colecții și actualizarea scorului și a listei de colecții atunci când se întâmplă acest lucru (un Iterator este folosit mai degrabă decât ArrayList în mod direct pentru a evita o excepție de modificare concomitentă la eliminarea obiectelor din listă)
  • redând spritele și textul pe pânză
new AnimationTimer () mâner public void (long timeNanoTime) // calcula timpul de la ultima actualizare. dublu elapsedTime = (actualNanoTime - lastNanoTime.value) / 1000000000.0; lastNanoTime.value = currentNanoTime; // game logic servietă.setVelocity (0,0); dacă (input.contains ("LEFT")) briefcase.addVelocity (-50,0); dacă (input.contains ("DREAPTA")) briefcase.addVelocity (50,0); dacă (input.contains ("UP")) briefcase.addVelocity (0, -50); dacă (input.contains ("DOWN")) briefcase.addVelocity (0,50); briefcase.update (elapsedTime); // detectarea coliziunii Iterator moneybagIter = moneybagList.iterator (); în timp ce (moneybagIter.hasNext ()) Sprite moneybag = moneybagIter.next (); dacă (briefcase.intersects (moneybag)) moneybagIter.remove (); score.value ++;  // render gc.clearRect (0, 0, 512, 512); briefcase.render (gc); pentru (Sprite moneybag: moneybagList) moneybag.render (gc); String pointsText = "Numerar: $" + (100 * score.value); gc.fillText (puncteText, 360, 36); gc.strokeText (puncteText, 360, 36);  .start();

Ca de obicei, codul complet poate fi găsit în fișierul de cod atașat (Example5.java) în repo GitHub.

Pasii urmatori

  • Există o colecție de tutoriale introductive pe site-ul Oracle, care vă va ajuta să învățați sarcini JavaFX comune: Noțiuni de bază cu JavaFX Exemple de aplicații.
  • Este posibil să fiți interesat să învățați cum să utilizați Scenic Builder, un mediu vizual de aspect pentru proiectarea interfețelor utilizatorilor. Acest program generează FXML, care este un limbaj bazat pe XML care poate fi utilizat pentru a defini o interfață de utilizator pentru un program JavaFX. Pentru aceasta, consultați JavaFX Builder de scene: Începeți.
  • Experiența FX este un blog excelent, actualizat în mod regulat, care conține informații și exemple de proiecte de interes pentru dezvoltatorii JavaFX. Multe dintre demo-urile listate sunt destul de inspirationale!
  • José Pereda are exemple excelente de jocuri mai avansate construite cu JavaFX în depozitul său GitHub.
  • Proiectul JFxtras constă într-un grup de dezvoltatori care au creat componente suplimentare JavaFX care oferă funcționalități de obicei necesare, care lipsesc în prezent din JavaFX.
  • Proiectul JavaFXPorts vă permite să vă împachetați aplicația JavaFX pentru implementare pe iOS și Android.
  • Ar trebui să marcați referințele oficiale pentru JavaFX, în special ghidul JavaFX al Oracle și documentația API. 
  • Unele cărți bine cotate pe JavaFX includ Pro JavaFX 8, JavaFX 8 - Introducere prin exemplu și, de interes deosebit pentru dezvoltatorii de jocuri, Începutul jocului Java 8.

Concluzie

În acest tutorial, v-am prezentat clase JavaFX care sunt utile în programarea jocurilor. Am lucrat printr-o serie de exemple de complexitate crescândă, culminând cu un joc în stil de colectare bazat pe sprite. Acum sunteți gata să investigați unele dintre resursele enumerate mai sus sau să vă aruncați cu capul în apă și să începeți să vă creați propriul joc. Cel mai bun noroc pentru tine în eforturile tale!