Introduksjon til JavaFX for spillutvikling

JavaFX er en kryssplattform GUI toolkit for Java, og er etterfølgeren til Java Swing-bibliotekene. I denne opplæringen vil vi utforske funksjonene til JavaFX som gjør det enkelt å bruke for å komme i gang med programmering av spill i Java.

Denne opplæringen forutsetter at du allerede vet hvordan du skal kode i Java. Hvis ikke, sjekk ut Lær Java for Android, Introduksjon til dataprogrammering med Java: 101 og 201, Hov første Java, Greenfoot eller Lær Java den harde måten å komme i gang.

Installasjon

Hvis du allerede utvikler programmer med Java, trenger du sannsynligvis ikke å laste ned noe i det hele tatt: JavaFX har blitt inkludert i standard JDK (Java Development Kit) bunt siden JDK versjon 7u6 (august 2012). Hvis du ikke har oppdatert Java-installasjonen på en stund, går du til Java-nedlastingswebsiden for den nyeste versjonen. 

Grunnleggende rammeklasser

Opprettelse av et JavaFX-program begynner med applikasjonsklassen, hvorfra alle JavaFX-applikasjoner blir utvidet. Din hovedklasse bør ringe lansering () metode, som da vil ringe til i det() metode og deretter start() metode, vent på at programmet er ferdig, og ring deretter Stoppe() metode. Av disse metodene, bare start() Metoden er abstrakt og må overskrides.

Stage-klassen er JavaFX-beholderen på toppnivå. Når et program er startet, opprettes et første trinn og overføres til programmets startmetode. Stages kontrollerer grunnleggende vinduegenskaper som tittel, ikon, synlighet, resizability, fullskjermmodus og dekorasjoner; sistnevnte er konfigurert med StageStyle. Ytterligere trinn kan bygges etter behov. Etter at et trinn er konfigurert og innholdet er lagt til, vise fram() Metoden kalles.

Å vite alt dette, kan vi skrive et minimalt eksempel som lanserer et vindu i JavaFX:

importer javafx.application.Application; importer javafx.stage.Stage; offentlig klasse Eksempel 1 utvider applikasjon offentlig statisk tomt hoved (String [] args) launch (args);  offentlig tomgangstart (Stage theStage) theStage.setTitle ("Hei, Verden!"); theStage.show (); 

Strukturering av innhold

Innhold i JavaFX (som tekst, bilder og UI-kontroller) er organisert ved hjelp av en treaktig datastruktur kjent som en scenediagram, som grupperer og arrangerer elementene i en grafisk scene. 

Representasjon av en JavaFX Scene Graph.

Et generelt element i en scenediagram i JavaFX kalles en Node. Hver Node i et tre har en enkelt "parent" node, med unntak av en spesiell node betegnet som "root". En gruppe er en knutepunkt som kan ha mange "barn" knutepunkter. Grafiske transformasjoner (oversettelse, rotasjon og skala) og effekter på en gruppe gjelder også for barna. Noder kan styles ved hjelp av JavaFX Cascading Style Sheets (CSS), ganske lik CSS som brukes til å formatere HTML-dokumenter.

Scene-klassen inneholder alt innhold for en scenediagram, og krever at en rotnode skal settes (i praksis er dette ofte en gruppe). Du kan spesifisere størrelsen på en scene spesielt; ellers blir størrelsen på en scene automatisk beregnet ut fra innholdet. En sceneobjekt må sendes til scenen (ved setScene () metode) for å bli vist.

Rendering Graphics

Gjenvinning av grafikk er spesielt viktig for spillprogrammerere! I JavaFX er Canvas-objektet et bilde som vi kan tegne tekst, former og bilder ved hjelp av det tilhørende GraphicsContext-objektet. (For de utviklere som er kjent med Java Swing-verktøyet, ligner dette grafikkobjektet som sendes til maling() metode i JFrame-klassen.)

Objektet GraphicsContext inneholder et vell av kraftige tilpasningsevner. For å velge farger for tegning av tekst og former, kan du angi fylle (innvendige) og strekkfarger (grense), som er malingsobjekter: disse kan være en enkelt solid farge, en brukerdefinert gradient (enten LinearGradient eller RadialGradient), eller selv en ImagePattern. Du kan også bruke en eller flere effektstileobjekter, for eksempel Lys, Skygge eller GaussiskBlur, og endre skrifttyper fra standard ved å bruke Font-klassen. 

Bildeklassen gjør det enkelt å laste bilder fra en rekke formater fra filer og tegne dem via GraphicsContext-klassen. Det er enkelt å konstruere prosedyrelt genererte bilder ved å bruke WritableImage-klassen sammen med PixelReader og PixelWriter-klassene.

Ved å bruke disse klassene kan vi skrive et mye mer verdig "Hello, World" -stil eksempel som følger. For kortfattet vil vi bare inkludere start() metode her (vi hopper over importklæringene og hoved() metode); Men fullstendig arbeidskildekode finnes i GitHub repo som følger med denne opplæringen.

Offentlig tomgangstart (Stage theStage) theStage.setTitle ("Canvas Eksempel"); Grupperot = Ny gruppe (); Scene theScene = ny scene (root); TheStage.setScene (theScene); Lerret lerret = nytt lærred (400, 200); root.getChildren (). legg til (lerret); 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 ("Hei, Verden!", 60, 50); gc.strokeText ("Hei, Verden!", 60, 50); Bildejord = nytt bilde ("earth.png"); gc.drawImage (jord, 180, 100); theStage.show (); 

The Game Loop

Deretter må vi lage våre programmer dynamisk, noe som betyr at spilltilstanden endres over tid. Vi implementerer en spillsløyfe: en uendelig sløyfe som oppdaterer spillobjektene og gjør scenen til skjermen, ideelt med en hastighet på 60 ganger per sekund. 

Den enkleste måten å oppnå dette i JavaFX bruker klassen AnimationTimer, der en metode (navngitt håndtak()) kan skrives som vil bli kalt med en hastighet på 60 ganger per sekund, eller så nær den som mulig. (Denne klassen må ikke brukes bare til animasjonsformål, det er i stand til langt mer.)

Bruk av AnimationTimer-klassen er litt vanskelig: siden det er en abstrakt klasse, kan den ikke opprettes direkte. Klassen må utvides før en forekomst kan opprettes. For våre enkle eksempler vil vi imidlertid forlenge klassen ved å skrive en anonym indre klasse. Denne indre klassen må definere den abstrakte metoden håndtak(), som vil bli bestått et enkelt argument: den nåværende systemtiden i nanosekunder. Etter å ha definert den indre klassen, påberoper vi oss umiddelbart start() metode, som begynner sløyfen. (Sløyfen kan stoppes ved å ringe Stoppe() metode.)

Med disse klassene kan vi endre vår "Hello, World" -eksempel, og skape en animasjon som består av jorden som kretser rundt Sola mot et stjerneklar bakgrunnsbilde.

offentlig tomgangstart (Stage theStage) theStage.setTitle ("Tidslinjeprøve"); Grupperot = Ny gruppe (); Scene theScene = ny scene (root); TheStage.setScene (theScene); Lerret lerret = nytt lærred (512, 512); root.getChildren (). legg til (lerret); GraphicsContext gc = canvas.getGraphicsContext2D (); Bildejord = nytt bilde ("earth.png"); Bilde søndag = nytt bilde ("sun.png"); Image space = nytt bilde ("space.png"); siste lange startNanoTime = System.nanoTime (); Ny AnimationTimer () Offentlig tomgangshåndtak (Long CurrentNanoTime) double t = (currentNanoTime - startNanoTime) / 1000000000.0; dobbelt x = 232 + 128 * Math.cos (t); dobbelt y = 232 + 128 * Math.sin (t); // bakgrunnsbilde rydder lerret gc.drawImage (plass, 0, 0); gc.drawImage (jord, x, y); gc.drawImage (sol, 196, 196);  .start(); theStage.show (); 

Det finnes alternative måter å implementere en spillsløyfe på i JavaFX. En litt lengre (men mer fleksibel) tilnærming innebærer Tidslinjeklassen, som er en animasjonssekvens bestående av et sett med KeyFrame-objekter. For å opprette en spillsløyfe må tidslinjen settes til å gjenta på ubestemt tid, og bare en enkelt nøkkelramme er nødvendig, med varigheten satt til 0,016 sekunder (for å oppnå 60 sykluser per sekund). Denne gjennomføringen finner du i Example3T.java fil i GitHub repo.

Rammebasert animasjon

En annen vanlig nødvendig programmeringskomponent er rammebasert animasjon: viser en rekke bilder i rask rekkefølge for å skape en illusjon av bevegelse. 

Forutsatt at alle animasjoner sløyfes og alle rammer vises i samme antall sekunder, kan en grunnleggende implementering være så enkel som følger:

offentlig klasse AnimatedImage offentlig bilde [] rammer; offentlig dobbeltvarighet offentlig bilde getFrame (dobbelt tid) int index = (int) ((tid% (frames.length * varighet)) / varighet); returrammer [indeks]; 

For å integrere denne klassen i forrige eksempel kan vi lage en animert UFO, som initialiserer objektet ved hjelp av koden:

AnimatedImage ufo = ny AnimatedImage (); Bilde [] imageArray = nytt bilde [6]; for (int i = 0; i < 6; i++) imageArray[i] = new Image( "ufo_" + i + ".png" ); ufo.frames = imageArray; ufo.duration = 0.100;

... og i AnimationTimer legger du til enkeltlinjen med kode:

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

... på riktig sted. For et komplett arbeidskodeeksempel, se filen Example3AI.java i GitHub repo. 

Håndtering av brukerinngang

Detektere og behandle brukerinngang i JavaFX er grei. Brukerhandlinger som kan oppdages av systemet, for eksempel tastetrykk og museklikk, kalles arrangementer. I JavaFX forårsaker disse handlingene automatisk generering av objekter (som KeyEvent og MouseEvent) som lagrer de tilknyttede dataene (for eksempel den faktiske tastetrykk eller plasseringen av musepekeren). Enhver JavaFX-klasse som implementerer EventTarget-klassen, for eksempel en scene, kan "lytte" til hendelser og håndtere dem; I eksemplene som følger, viser vi hvordan du konfigurerer en scene for å behandle ulike hendelser.

Glancing gjennom dokumentasjonen for sceneklassen, er det mange metoder som lytter etter håndtering av ulike typer innspill fra forskjellige kilder. For eksempel metoden setOnKeyPressed () kan tildele en EventHandler som vil aktiveres når en tast trykkes, metoden setOnMouseClicked () kan tildele en EventHandler som aktiveres når en museknapp trykkes, og så videre. EventHandler-klassen tjener ett formål: å inkapslere en metode (kalles håndtak()) som kalles når den tilsvarende hendelsen oppstår. 

Når du oppretter en EventHandler, må du spesifisere type av hendelsen som den håndterer: du kan erklære en Event eller en Event, for eksempel. EventHandlers er også ofte opprettet som anonyme indre klasser, da de vanligvis bare brukes en gang (når de blir sendt som et argument til en av metodene som er oppført ovenfor).

Håndtering av tastaturhendelser

Brukerinngang blir ofte behandlet i hovedspillsløyfen, og dermed må man ha en oversikt over hvilke taster som for øyeblikket er aktive. En måte å oppnå dette på er å lage en ArrayList of String-objekter. Når en nøkkel er trykket inn, legger vi String-representasjonen til KeyEvents KeyCode til listen; Når nøkkelen slippes, fjerner vi den fra listen. 

I eksemplet nedenfor inneholder lerretet to bilder av piltastene; Når en tast trykkes, blir det tilsvarende bildet grønt. 


Kildekoden er inneholdt i filen Example4K.java i GitHub repo.

Offentlig tomgangsstart (Stage theStage) theStage.setTitle ("Tastatureksempel"); Grupperot = Ny gruppe (); Scene theScene = ny scene (root); TheStage.setScene (theScene); Lerret lerret = nytt lærred (512 - 64, 256); root.getChildren (). legg til (lerret); Arraylist input = ny ArrayList(); theScene.setOnKeyPressed (ny EventHandler() Offentlig tomgangshåndtak (KeyEvent e) Stringkode = e.getCode (). toString (); // bare legg til en gang ... forhindre duplikater hvis (! input.contains (code)) input.add (kode); ); theScene.setOnKeyReleased (ny EventHandler() Offentlig tomgangshåndtak (KeyEvent e) Stringkode = e.getCode (). toString (); input.remove (kode); ); GraphicsContext gc = canvas.getGraphicsContext2D (); Bilde igjen = nytt bilde ("left.png"); Bilde leftG = nytt bilde ("leftG.png"); Bilde høyre = nytt bilde ("right.png"); Bilde rightG = nytt bilde ("rightG.png"); Ny AnimationTimer () Offentlig tomgangshåndtak (Long CurrentNanoTime) // Fjern lerretet gc.clearRect (0, 0, 512.512); hvis (input.contains ("LEFT")) gc.drawImage (leftG, 64, 64); ellers gc.drawImage (venstre, 64, 64); hvis (input.contains ("RIGHT")) gc.drawImage (rightG, 256, 64); ellers gc.drawImage (høyre, 256, 64);  .start(); theStage.show (); 

Håndtere mushendelser

La oss nå se på et eksempel som fokuserer på MouseEvent-klassen i stedet for KeyEvent-klassen. I dette minispillet tjener spilleren et poeng hver gang målet klikkes.


Siden EventHandlers er indre klasser, må alle variabler de bruker være endelige eller "effektive endelige", noe som betyr at variablene ikke kan reinitialiseres. I det forrige eksempelet ble dataene sendt til EventHandler ved hjelp av en ArrayList, hvis verdier kan endres uten reinitialisering (via Legg til() og fjerne() metoder). 

Men når det gjelder grunnleggende datatyper, kan verdiene ikke endres når de er initialisert. Hvis du vil at EventHandler skal få tilgang til grunnleggende datatyper som endres andre steder i programmet, kan du opprette en wrapper-klasse som inneholder enten offentlige variabler eller getter / setter-metoder. (I eksemplet nedenfor, IntValue er en klasse som inneholder en offentlig int variabel kalt verdi.)

Offentlig tomgangstart (Stage theStage) theStage.setTitle ("Klikk mål!"); Grupperot = Ny gruppe (); Scene theScene = ny scene (root); TheStage.setScene (theScene); Lerret lerret = nytt lærred (500, 500); root.getChildren (). legg til (lerret); Sirkel targetData = Ny sirkel (100,100,32); IntValue poeng = ny IntValue (0); theScene.setOnMouseClicked (ny EventHandler() public void handle (MouseEvent e) hvis (targetData.containsPoint (e.getX (), e.getY ())) double x = 50 + 400 * Math.random (); dobbelt 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); Bilde bullseye = nytt bilde ("bullseye.png"); ny AnimationTimer () offentlig tomgangshåndtak (long currentNanoTime) // Fjern lerretet gc.setFill (ny farge (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 = "Poeng:" + points.value; gc.fillText (pointsText, 360, 36); gc.strokeText (pointsText, 360, 36);  .start(); theStage.show (); 

Full kildekoden finnes i GitHub repo; hovedklassen er Example4M.java.

Opprette en grunnleggende sprite klasse med JavaFX

I videospill, a sprite er begrepet for en enkelt visuell enhet. Nedenfor er et eksempel på en Sprite-klasse som lagrer et bilde og en posisjon, samt hastighetsinformasjon (for mobile enheter) og bredde / høydeinformasjon som skal brukes ved beregning av avgrensningsbokser i forbindelse med kollisjonsdeteksjon. Vi har også standard getter / setter metoder for de fleste av disse dataene (utelatt for korthet), og noen standard metoder som trengs i spillutvikling:

  • Oppdater(): Beregner den nye posisjonen basert på Sprite-hastigheten.
  • render (): tegner det tilknyttede bildet til lerretet (via GraphicsContext-klassen) ved hjelp av posisjonen som koordinater.
  • getBoundary (): Returnerer et JavaFX Rectangle2D objekt, nyttig ved kollisjon gjenkjenning på grunn av den skjærer metoden.
  • skjærer (): bestemmer om grenseboksen til denne Sprite krysser med en annen Sprite.
offentlig klasse Sprite privat bilde bilde; privat dobbel posisjonX; privat dobbeltstilling; privat dobbelt hastighetX; privat dobbelt hastighet; privat dobbel bredde; privat dobbel høyde; // ... // metoder utelatt for korthet // ... offentlig ugyldig oppdatering (dobbelt tid) positionX + = hastighetX * tid; posisjonY + = hastighetY * tid;  Offentlig ugyldig gjengivelse (GraphicsContext gc) gc.drawImage (bilde, posisjonX, posisjonY);  offentlig Rectangle2D getBoundary () returner ny Rectangle2D (posisjonX, posisjonY, bredde, høyde);  offentlige boolske kryss (Sprite s) return s.getBoundary (). krysser (this.getBoundary ()); 

Full kildekoden er inkludert i Sprite.java i GitHub repo.

Bruke Sprite-klassen

Med hjelp av Sprite-klassen kan vi enkelt lage et enkelt samlingsspill i JavaFX. I dette spillet antar du rollen som en bevisst koffert som har som mål å samle de mange pengeposene som har blitt liggende liggende av en uforsiktig tidligere eier. Piltastene flytter spilleren rundt skjermen.

Denne koden låne tungt fra tidligere eksempler: Konfigurere skrifttyper for å vise poengsummen, lagre tastaturinngang med en ArrayList, implementere spillsløyfen med en AnimationTimer og lage wrapper klasser for enkle verdier som må endres under spillsløyfen.

Ett kodesegment av spesiell interesse innebærer å skape et Sprite-objekt for spilleren (dokumentmappe) og en ArrayList of Sprite-objekter for samlerobjektene (pengeposer):

Sprite briefcase = ny Sprite (); briefcase.setImage ( "briefcase.png"); briefcase.setPosition (200, 0); Arraylist moneybagList = ny ArrayList(); for (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 ); 

Et annet kodesegment av interesse er opprettelsen av AnimationTimer, som har oppgave:

  • beregner tiden som er gått siden siste oppdatering
  • setter spillerens hastighet avhengig av tastene som for øyeblikket er trykket på
  • utfører kollisjonsdeteksjon mellom spilleren og samleobjektene, og oppdaterer poengsummen og listen over samleobjekter når dette skjer (en Iterator brukes i stedet for ArrayList direkte for å unngå en samtidig endringseksjon når man fjerner objekter fra listen)
  • gjør sprites og tekst til lerretet
Ny AnimationTimer () Offentlig tomgangshåndtak (Long CurrentNanoTime) // Beregn tid siden sist oppdatering. double elapsedTime = (currentNanoTime - lastNanoTime.value) / 1000000000.0; lastNanoTime.value = currentNanoTime; // spilllogikk briefcase.setVelocity (0,0); hvis (input.contains ("LEFT")) koffert.addVelocity (-50,0); hvis (input.contains ("RIGHT")) briefcase.addVelocity (50,0); hvis (input.contains ("UP")) briefcase.addVelocity (0, -50); hvis (input.contains ("DOWN")) briefcase.addVelocity (0,50); briefcase.update (elapsedTime); // kollisjon gjenkjenning Iterator moneybagIter = moneybagList.iterator (); mens (moneybagIter.hasNext ()) Sprite moneybag = moneybagIter.next (); hvis (dokumentmappe.intersects (moneybag)) moneybagIter.remove (); score.value ++;  // gjengi gc.clearRect (0, 0, 512.512); briefcase.render (gc); for (Sprite moneybag: moneybagList) moneybag.render (gc); String pointsText = "Cash: $" + (100 * score.value); gc.fillText (pointsText, 360, 36); gc.strokeText (pointsText, 360, 36);  .start();

Som vanlig kan du finne fullstendig kode i vedlagte kodefil (Example5.java) i GitHub repo.

Neste skritt

  • Det er en samling innledende veiledninger på Oracle-nettstedet, som hjelper deg med å lære vanlige JavaFX-oppgaver: Komme i gang med JavaFX-prøveprogrammer.
  • Du kan være interessert i å lære hvordan du bruker Scene Builder, et visuelt layoutmiljø for å designe brukergrensesnitt. Dette programmet genererer FXML, som er et XML-basert språk som kan brukes til å definere et brukergrensesnitt for et JavaFX-program. For dette, se JavaFX Scene Builder: Komme i gang.
  • FX Experience er en utmerket blogg, oppdatert jevnlig, som inneholder informasjon og eksempler av prosjekter av interesse for JavaFX-utviklere. Mange av de oppgitte demoene er ganske inspirerende!
  • José Pereda har gode eksempler på mer avanserte spill bygget med JavaFX i sitt GitHub repository.
  • JFxtras-prosjektet består av en gruppe utviklere som har opprettet ekstra JavaFX-komponenter som gir nødvendig funksjonalitet som for tiden mangler fra JavaFX.
  • JavaFXPorts-prosjektet lar deg pakke JavaFX-programmet for distribusjon på iOS og Android.
  • Du bør bokmerke de offisielle referansene til JavaFX, spesielt Oracle's JavaFX-guide og API-dokumentasjonen. 
  • Noen godt gjennomgåtte bøker på JavaFX inkluderer Pro JavaFX 8, JavaFX 8 - Introduksjon av eksempel, og av spesiell interesse for spillutviklere, Beginning Java 8 Games Development.

Konklusjon

I denne opplæringen har jeg introdusert deg til JavaFX-klasser som er nyttige i spillprogrammering. Vi jobbet gjennom en serie eksempler på økende kompleksitet, som kulminerte i et sprite-basert samlingstil spill. Nå er du klar til å undersøke noen av ressursene ovenfor, eller å dykke inn og begynne å lage ditt eget spill. Lykke til deg i dine bestrebelser!