Introductie tot JavaFX voor game-ontwikkeling

JavaFX is een cross-platform GUI-toolkit voor Java en is de opvolger van de Java Swing-bibliotheken. In deze zelfstudie zullen we de functies van JavaFX verkennen die het gemakkelijk te gebruiken maken om aan de slag te gaan met het programmeren van spellen in Java.

Deze tutorial gaat ervan uit dat je al weet hoe je moet coderen in Java. Als dat niet het geval is, raadpleeg dan Learn Java for Android, Introduction to Computer Programming with Java: 101 and 201, Head First Java, Greenfoot of Learn Java the Hard Way aan de slag.

Installatie

Als u al toepassingen met Java ontwikkelt, hoeft u waarschijnlijk helemaal niets te downloaden: JavaFX is opgenomen in de standaard JDK (Java Development Kit) -bundel sinds JDK-versie 7u6 (augustus 2012). Als u uw Java-installatie een tijdje niet hebt bijgewerkt, ga dan naar de Java-downloadwebsite voor de nieuwste versie. 

Basis Framework-klassen

Het maken van een JavaFX-programma begint met de klasse Application, waaruit alle JavaFX-applicaties worden uitgebreid. Je hoofdklasse zou de. Moeten bellen lancering() methode, die dan de in het() methode en dan de begin() methode, wacht tot de toepassing is voltooid en bel de hou op() methode. Van deze methoden, alleen de begin() methode is abstract en moet worden overschreven.

De Stage-klasse is de JavaFX-container op het hoogste niveau. Wanneer een toepassing wordt gestart, wordt een eerste fase gemaakt en doorgegeven aan de startmethode van de toepassing. Fasen regelen de basale vensteigenschappen zoals titel, pictogram, zichtbaarheid, resizeerbaarheid, modus op volledig scherm en decoraties; de laatste is geconfigureerd met behulp van StageStyle. Bijkomende fasen kunnen zo nodig worden geconstrueerd. Nadat een fase is geconfigureerd en de inhoud is toegevoegd, de laten zien() methode wordt genoemd.

Als we dit allemaal weten, kunnen we een minimaal voorbeeld schrijven dat een venster opent in JavaFX:

importeer javafx.application.Application; importeer javafx.stage.Stage; public class Voorbeeld1 breidt Toepassing uit public static void main (String [] args) launch (args);  public void start (Stage theStage) theStage.setTitle ("Hello, World!"); theStage.show (); 

Inhoud structureren

Inhoud in JavaFX (zoals tekst, afbeeldingen en UI-besturingselementen) is georganiseerd met een boomachtige gegevensstructuur, een scènegrafiek, die de elementen van een grafische scène groepeert en rangschikt. 

Vertegenwoordiging van een JavaFX-scènegrafiek.

Een algemeen element van een scènegrafiek in JavaFX wordt een knooppunt genoemd. Elk knooppunt in een boom heeft een enkel "bovenliggend" knooppunt, met uitzondering van een speciaal knooppunt dat wordt aangeduid als de "wortel". Een groep is een knooppunt dat veel "onderliggende" knooppuntelementen kan hebben. Grafische transformaties (vertaling, rotatie en schaal) en effecten toegepast op een groep zijn ook van toepassing op zijn kinderen. Knooppunten kunnen worden gestileerd met behulp van JavaFX Cascading Style Sheets (CSS), vergelijkbaar met de CSS die wordt gebruikt voor het opmaken van HTML-documenten.

De Scene-klasse bevat alle inhoud voor een scènegrafiek en vereist dat een root-knooppunt wordt ingesteld (in de praktijk is dit vaak een groep). U kunt de grootte van een scène specifiek instellen; anders wordt de grootte van een scène automatisch berekend op basis van de inhoud. Een Scene-object moet worden doorgegeven aan het werkgebied (door de setScene () methode) om te worden weergegeven.

Afbeeldingen weergeven

Het weergeven van afbeeldingen is vooral belangrijk voor spelprogrammeurs! In JavaFX is het Canvas-object een afbeelding waarop we tekst, vormen en afbeeldingen kunnen tekenen met behulp van het bijbehorende GraphicsContext-object. (Voor ontwikkelaars die bekend zijn met de Java Swing-toolkit, is dit vergelijkbaar met het Graphics-object dat is doorgegeven aan de verf() methode in de JFrame-klasse.)

Het GraphicsContext-object bevat een schat aan krachtige aanpassingsmogelijkheden. Als u kleuren wilt kiezen voor het tekenen van tekst en vormen, kunt u de opvulkleur (binnenzijde) en lijn (rand) instellen, dit zijn Paint-objecten: deze kunnen een enkele effen kleur, een door de gebruiker gedefinieerde overgang (ofwel LinearGradient of RadialGradient) zijn, of zelfs een ImagePattern. U kunt ook een of meer effectstijlobjecten toepassen, zoals Lighting, Shadow of GaussianBlur, en lettertypen van de standaard wijzigen door de klasse Font te gebruiken. 

Met de klasse Image kunt u eenvoudig afbeeldingen uit verschillende indelingen uit bestanden laden en ze tekenen via de klasse GraphicsContext. Het is eenvoudig om procedureel gegenereerde afbeeldingen te maken met behulp van de klasse WritableImage, samen met de klassen PixelReader en PixelWriter.

Met behulp van deze klassen kunnen we een veel waardiger "Hallo, Wereld" -stijlvoorbeeld als volgt schrijven. Kortheidshalve zullen we alleen de begin() methode hier (we slaan de import statements en hoofd() methode); de volledige werkbroncode is echter te vinden in de GitHub-repo die bij deze zelfstudie hoort.

public void start (Stage theStage) theStage.setTitle ("Canvas Example"); Groepswortel = nieuwe Groep (); Scene theScene = nieuwe scène (root); theStage.setScene (theScene); Canvas canvas = nieuw canvas (400, 200); root.getChildren (). toevoegen (canvas); GraphicsContext gc = canvas.getGraphicsContext2D (); gc.setFill (Color.RED); gc.setStroke (Color.BLACK); gc.setLineWidth (2); Font theFont = Font.font ("Times New Roman", FontWeight.BOLD, 48); gc.setFont (theFont); gc.fillText ("Hallo, Wereld!", 60, 50); gc.strokeText ("Hallo, Wereld!", 60, 50); Afbeelding earth = new Image ("earth.png"); gc.drawImage (aarde, 180, 100); theStage.show (); 

The Game Loop

Vervolgens moeten we onze programma's maken dynamisch, wat betekent dat de spelstatus in de loop van de tijd verandert. We zullen een gamelus implementeren: een oneindige lus die de game-objecten bijwerkt en de scène op het scherm weergeeft, idealiter met een snelheid van 60 keer per seconde. 

De eenvoudigste manier om dit te bereiken in JavaFX is het gebruik van de klasse AnimationTimer, waarbij een methode (genaamd handvat()) kan worden geschreven met een snelheid van 60 keer per seconde of zo dicht mogelijk bij die snelheid. (Deze klasse hoeft niet alleen voor animatiedoeleinden te worden gebruikt, hij kan veel meer.)

Het gebruik van de klasse AnimationTimer is een beetje lastig: aangezien het een abstracte klasse is, kan deze niet rechtstreeks worden gemaakt - de klasse moet worden uitgebreid voordat een instantie kan worden gemaakt. Voor onze eenvoudige voorbeelden zullen we de klas echter uitbreiden door een anonieme innerlijke klasse te schrijven. Deze innerlijke klasse moet de abstracte methode definiëren handvat(), waarvoor een enkel argument zal worden doorgegeven: de huidige systeemtijd in nanoseconden. Na het definiëren van de innerlijke klasse, roepen we onmiddellijk het begin() methode, die de lus begint. (De lus kan worden gestopt door de hou op() methode.)

Met deze klassen kunnen we ons voorbeeld "Hallo, Wereld" aanpassen door een animatie te maken die bestaat uit de aarde die rond de zon draait tegen een sterrenachtergrond.

public void start (Stage theStage) theStage.setTitle ("Timeline Example"); Groepswortel = nieuwe Groep (); Scene theScene = nieuwe scène (root); theStage.setScene (theScene); Canvas canvas = nieuw canvas (512, 512); root.getChildren (). toevoegen (canvas); GraphicsContext gc = canvas.getGraphicsContext2D (); Afbeelding earth = new Image ("earth.png"); Afbeelding zon = nieuwe afbeelding ("sun.png"); Beeldruimte = nieuwe afbeelding ("space.png"); finale lange startNanoTime = System.nanoTime (); nieuwe AnimationTimer () openbare ongeldige handle (lange currentNanoTime) double t = (currentNanoTime - startNanoTime) / 1000000000.0; dubbel x = 232 + 128 * Math.cos (t); dubbele y = 232 + 128 * Math.sin (t); // achtergrondafbeelding wist canvas gc.drawImage (spatie, 0, 0); gc.drawImage (aarde, x, y); gc.drawImage (sun, 196, 196);  .start (); theStage.show (); 

Er zijn alternatieve manieren om een ​​gamelus in JavaFX te implementeren. Een iets langere (maar meer flexibele) benadering heeft betrekking op de tijdlijnklasse, die een animatiereeks is die bestaat uit een set KeyFrame-objecten. Om een ​​gamelus te maken, moet de tijdlijn worden ingesteld om oneindig te herhalen, en is slechts één KeyFrame vereist, met een duur ingesteld op 0,016 seconden (om 60 cycli per seconde te bereiken). Deze implementatie is te vinden in de Example3T.java bestand in de GitHub-repo.

Op frames gebaseerde animatie

Een andere vaak benodigde component voor het programmeren van games is frame-gebaseerde animatie: snel achter elkaar een reeks beelden weergeven om de illusie van beweging te creëren. 

Ervan uitgaande dat alle animatielus en alle frames hetzelfde aantal seconden weergeven, kan een basisimplementatie zo simpel zijn als volgt:

public classImage public image [] frames; openbare dubbele duur; public Image getFrame (dubbele tijd) int index = (int) ((tijd% (frames.length * duur)) / duur); return-frames [index]; 

Om deze klasse in het vorige voorbeeld te integreren, kunnen we een geanimeerde UFO maken, waarbij het object met behulp van de code wordt geïnitialiseerd:

AnimatedImage ufo = new AnimatedImage (); Afbeelding [] imageArray = nieuwe afbeelding [6]; voor (int i = 0; i < 6; i++) imageArray[i] = new Image( "ufo_" + i + ".png" ); ufo.frames = imageArray; ufo.duration = 0.100;

... en, binnen de AnimationTimer, het toevoegen van de enkele regel code:

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

... op de juiste plek. Zie het bestand voor een voorbeeld van een volledige werkende code Example3AI.java in de repository van GitHub. 

Omgaan met gebruikersinvoer

Het detecteren en verwerken van gebruikersinvoer in JavaFX is eenvoudig. Gebruikersacties die door het systeem kunnen worden gedetecteerd, zoals toetsaanslagen en muisklikken, worden aangeroepen events. In JavaFX veroorzaken deze acties automatisch het genereren van objecten (zoals KeyEvent en MouseEvent) die de bijbehorende gegevens opslaan (zoals de eigenlijke ingedrukte toets of de locatie van de muisaanwijzer). Elke JavaFX-klasse die de klasse EventTarget implementeert, zoals een scène, kan naar gebeurtenissen 'luisteren' en deze afhandelen; in de voorbeelden die volgen, laten we zien hoe je een scène kunt instellen om verschillende gebeurtenissen te verwerken.

Als je de documentatie voor de klasse Scene doorkijkt, zijn er veel methoden die luisteren naar het omgaan met verschillende soorten invoer uit verschillende bronnen. Bijvoorbeeld de methode setOnKeyPressed () kan een EventHandler toewijzen die wordt geactiveerd wanneer een toets wordt ingedrukt, de methode setOnMouseClicked () kan een EventHandler toewijzen die wordt geactiveerd wanneer een muisknop wordt ingedrukt, enzovoort. De klasse EventHandler heeft één doel: een methode inkapselen (genaamd handvat()) die wordt aangeroepen wanneer de overeenkomstige gebeurtenis plaatsvindt. 

Wanneer u een EventHandler maakt, moet u de type of Event dat het verwerkt: u kunt een declareren EventHandler of een EventHandler, bijvoorbeeld. EventHandlers worden ook vaak gemaakt als anonieme innerlijke klassen, omdat ze meestal maar één keer worden gebruikt (wanneer ze worden doorgegeven als argument voor een van de hierboven genoemde methoden).

Toetsenbordevents verwerken

Gebruikersinvoer wordt vaak verwerkt binnen de hoofdgame-lus en er moet dus een record worden bijgehouden van welke toetsen momenteel actief zijn. Een manier om dit te bereiken is door een ArrayList of String-objecten te maken. Wanneer een toets in eerste instantie wordt ingedrukt, voegen we de tekenreeksrepresentatie van KeyEvent's KeyCode aan de lijst toe; wanneer de sleutel wordt vrijgegeven, verwijderen we deze uit de lijst. 

In het onderstaande voorbeeld bevat het canvas twee afbeeldingen van pijltoetsen; telkens wanneer een toets wordt ingedrukt, wordt de bijbehorende afbeelding groen. 


De broncode bevindt zich in het bestand Example4K.java in de repository van GitHub.

public void start (Stage theStage) theStage.setTitle ("Keyboard Example"); Groepswortel = nieuwe Groep (); Scene theScene = nieuwe scène (root); theStage.setScene (theScene); Canvas canvas = nieuw canvas (512 - 64, 256); root.getChildren (). toevoegen (canvas); ArrayList input = nieuwe ArrayList(); theScene.setOnKeyPressed (nieuwe EventHandler() openbare ongeldige handle (KeyEvent e) String code = e.getCode (). toString (); // voeg slechts één keer toe ... voorkom duplicaten als (! input.contains (code)) input.add (code); ); theScene.setOnKeyReleased (nieuwe EventHandler() openbare ongeldige handle (KeyEvent e) String code = e.getCode (). toString (); input.remove (code); ); GraphicsContext gc = canvas.getGraphicsContext2D (); Afbeelding links = nieuwe afbeelding ("left.png"); Afbeelding linksG = nieuwe afbeelding ("leftG.png"); Afbeelding rechts = nieuwe afbeelding ("right.png"); Afbeelding rightG = nieuwe afbeelding ("rightG.png"); nieuwe AnimationTimer () openbare ongeldige handle (lange currentNanoTime) // Wis het canvas gc.clearRect (0, 0, 512.512); if (input.contains ("LEFT")) gc.drawImage (leftG, 64, 64); else gc.drawImage (links, 64, 64); if (input.contains ("RIGHT")) gc.drawImage (rightG, 256, 64); else gc.drawImage (right, 256, 64);  .start (); theStage.show (); 

Omgaan met muisevents

Laten we nu eens kijken naar een voorbeeld dat zich richt op de klasse MouseEvent in plaats van de klasse KeyEvent. In deze minigame verdient de speler een punt telkens wanneer op het doelwit wordt geklikt.


Omdat de EventHandlers innerlijke klassen zijn, moeten alle variabelen die ze gebruiken definitief zijn of "effectief definitief" zijn, wat betekent dat de variabelen niet opnieuw kunnen worden geïnitialiseerd. In het vorige voorbeeld werden de gegevens doorgegeven aan de EventHandler door middel van een ArrayList, waarvan de waarden kunnen worden gewijzigd zonder opnieuw te initialiseren (via de toevoegen() en verwijderen() methoden). 

In het geval van basistypen kunnen de waarden echter niet worden gewijzigd nadat ze zijn geïnitialiseerd. Als u wilt dat de EventHandler toegang krijgt tot de basistyptypen die elders in het programma zijn gewijzigd, kunt u een wrapper-klasse maken die openbare variabelen of methoden voor getter / setter bevat. (In het onderstaande voorbeeld, intValue is een klasse die een bevat openbaar int variabele genoemd waarde.)

public void start (Stage theStage) theStage.setTitle ("Click the Target!"); Groepswortel = nieuwe Groep (); Scene theScene = nieuwe scène (root); theStage.setScene (theScene); Canvas canvas = nieuw canvas (500, 500); root.getChildren (). toevoegen (canvas); Circle targetData = nieuwe Circle (100,100,32); IntValue-punten = nieuwe IntValue (0); theScene.setOnMouseClicked (nieuwe EventHandler() openbare ongeldige handle (MouseEvent e) if (targetData.containsPoint (e.getX (), e.getY ())) double x = 50 + 400 * Math.random (); dubbel y = 50 + 400 * Math.random (); targetData.setCenter (x, y); points.value ++;  else points.value = 0; ); GraphicsContext gc = canvas.getGraphicsContext2D (); Font theFont = Font.font ("Helvetica", FontWeight.BOLD, 24); gc.setFont (theFont); gc.setStroke (Color.BLACK); gc.setLineWidth (1); Afbeelding bullseye = nieuwe afbeelding ("bullseye.png"); new AnimationTimer () openbare ongeldige handle (lange currentNanoTime) // Maak het canvas leeg gc.setFill (nieuwe kleur (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 = "Punten:" + punten.waarde; gc.fillText (pointsText, 360, 36); gc.strokeText (pointsText, 360, 36);  .start (); theStage.show (); 

De volledige broncode is opgenomen in de GitHub-repo; de hoofdklasse is Example4M.java.

Een standaard Sprite-klasse maken met JavaFX

In videogames, a sprite is de term voor één visuele entiteit. Hieronder ziet u een voorbeeld van een Sprite-klasse die een afbeelding en positie opslaat, evenals informatie over de snelheid (voor mobiele entiteiten) en informatie over de breedte / hoogte die moet worden gebruikt bij het berekenen van begrenzende vakken voor botsingsdetectie. We hebben ook de standaard getter / setter-methoden voor de meeste van deze gegevens (weggelaten voor beknoptheid) en enkele standaardmethoden die nodig zijn bij het ontwikkelen van games:

  • bijwerken(): berekent de nieuwe positie op basis van de snelheid van de Sprite.
  • render (): tekent de gekoppelde afbeelding naar het canvas (via de klasse GraphicsContext) met de positie als coördinaten.
  • getBoundary (): retourneert een JavaFX Rectangle2D-object, handig bij botsingsdetectie vanwege de kruisingsmethode.
  • intersects (): bepaalt of het selectiekader van deze Sprite een kruising met een andere Sprite heeft.
public class Sprite private Image image; privé dubbele positieX; dubbele privépositie; privé dubbele velocityX; privé dubbele snelheidY; privé dubbele breedte; privé dubbele hoogte; // ... // methoden weggelaten voor beknoptheid // ... openbare nietige update (dubbele tijd) positionX + = velocityX * time; positionY + = velocityY * time;  public void render (GraphicsContext gc) gc.drawImage (afbeelding, positionX, positionY);  public Rectangle2D getBoundary () retourneer nieuwe Rectangle2D (positionX, positionY, width, height);  openbare boolean kruist (Sprite s) retourneert s.getBoundary (). snijdt (this.getBoundary ()); 

De volledige broncode is opgenomen in Sprite.java in de repository van GitHub.

De Sprite-klasse gebruiken

Met behulp van de Sprite-klasse kunnen we eenvoudig een eenvoudig verzamelspel maken in JavaFX. In dit spel neem je de rol aan van een gevoelige werkmap die tot doel heeft de vele geldzakken te verzamelen die rond zijn gelaten door een onvoorzichtige vorige eigenaar. De pijltjestoetsen bewegen de speler over het scherm.

Deze code leent zwaar van de vorige voorbeelden: het instellen van lettertypen om de score weer te geven, toetsenbordinvoer op te slaan met een ArrayList, de gamelus met een AnimationTimer te implementeren en wrapper classes te maken voor eenvoudige waarden die tijdens de gamelus moeten worden gewijzigd.

Een codesegment van bijzonder belang betreft het maken van een Sprite-object voor de speler (koffer) en een ArrayList van Sprite-objecten voor de verzamelobjecten (geldzakken):

Sprite-koffertje = nieuwe Sprite (); briefcase.setImage ( "briefcase.png"); aktetas.setPosition (200, 0); ArrayList moneybagList = new ArrayList(); voor (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 ); 

Een ander interessant codesegment is het creëren van de AnimationTimer, die belast is met:

  • het berekenen van de tijd die is verstreken sinds de laatste update
  • de afspeelsnelheid van de speler instellen, afhankelijk van de toetsen die op dat moment worden ingedrukt
  • botsingdetectie uitvoeren tussen de speler en verzamelobjecten en de score en lijst met verzamelobjecten bijwerken wanneer dit gebeurt (er wordt een Iterator gebruikt in plaats van de ArrayList om een ​​gelijktijdige wijzigingsexemplaar te vermijden wanneer objecten uit de lijst worden verwijderd)
  • de sprites en tekst weergeven op het canvas
new AnimationTimer () public void handle (long currentNanoTime) // bereken de tijd sinds de laatste update. double elapsedTime = (currentNanoTime - lastNanoTime.value) / 1000000000.0; lastNanoTime.value = currentNanoTime; // game logic briefcase.setVelocity (0,0); if (input.contains ("LEFT")) briefcase.addVelocity (-50,0); if (input.contains ("RIGHT")) briefcase.addVelocity (50,0); if (input.contains ("UP")) briefcase.addVelocity (0, -50); if (input.contains ("DOWN")) briefcase.addVelocity (0,50); briefcase.update (ElapsedTime); // botsing detectie Iterator moneybagIter = moneybagList.iterator (); while (moneybagIter.hasNext ()) Sprite moneybag = moneybagIter.next (); if (aktetas.intersecten (geldzak)) moneybagIter.remove (); score.value ++;  // render gc.clearRect (0, 0, 512.512); aktetas.render (gc); voor (Sprite moneybag: moneybagList) moneybag.render (gc); String pointsText = "Cash: $" + (100 * score.value); gc.fillText (pointsText, 360, 36); gc.strokeText (pointsText, 360, 36);  .start ();

Zoals gebruikelijk is de volledige code te vinden in het bijgevoegde codebestand (Example5.java) in de GitHub-repo.

Volgende stappen

  • Er is een verzameling inleidende zelfstudies op de Oracle-website, waarmee u algemene JavaFX-taken leert: Aan de slag met JavaFX-voorbeeldtoepassingen.
  • Mogelijk bent u geïnteresseerd in het gebruik van Scene Builder, een visuele lay-outomgeving voor het ontwerpen van gebruikersinterfaces. Dit programma genereert FXML, een op XML gebaseerde taal die kan worden gebruikt om een ​​gebruikersinterface voor een JavaFX-programma te definiëren. Zie hiervoor JavaFX Scene Builder: Aan de slag.
  • FX Experience is een uitstekende blog die regelmatig wordt bijgewerkt en die informatie en voorbeeldprojecten bevat die van belang zijn voor JavaFX-ontwikkelaars. Veel van de genoemde demo's zijn behoorlijk inspirerend!
  • José Pereda heeft uitstekende voorbeelden van geavanceerdere games gebouwd met JavaFX in zijn GitHub-repository.
  • Het JFxtras-project bestaat uit een groep ontwikkelaars die extra JavaFX-componenten hebben gemaakt die de vaak benodigde functionaliteit bieden die momenteel ontbreekt in JavaFX.
  • Met het JavaFXPorts-project kunt u uw JavaFX-applicatie verpakken voor implementatie op iOS en Android.
  • Maak een bladwijzer van de officiële referenties voor JavaFX, in het bijzonder de JavaFX-handleiding van Oracle en de API-documentatie. 
  • Sommige goed beoordeelde boeken op JavaFX zijn onder andere Pro JavaFX 8, JavaFX 8 - Introduction by Example en, met name van belang voor game-ontwikkelaars, Beginnen met Java 8 Games Development.

Conclusie

In deze tutorial heb ik je kennis laten maken met JavaFX-klassen die handig zijn bij het programmeren van spellen. We hebben een aantal voorbeelden van toenemende complexiteit doorgenomen, met als hoogtepunt een op sprite gebaseerd spel in collectiestijl. Nu ben je klaar om een ​​paar van de hierboven genoemde bronnen te onderzoeken, of om in te duiken en je eigen spel te maken. Veel succes met je in je inspanningen!