Introduzione a JavaFX per lo sviluppo di giochi

JavaFX è un toolkit GUI multipiattaforma per Java ed è il successore delle librerie Java Swing. In questo tutorial, esploreremo le funzionalità di JavaFX che lo rendono facile da usare per iniziare a programmare i giochi in Java.

Questo tutorial presume che tu sappia già come codificare in Java. In caso contrario, consulta Learn Java per Android, Introduzione alla programmazione con il computer con Java: 101 e 201, Head First Java, Greenfoot o Learn Java the Hard Way per iniziare.

Installazione

Se si sviluppano già applicazioni con Java, probabilmente non è necessario scaricare nulla: JavaFX è stato incluso nel pacchetto JDK (Java Development Kit) standard dalla versione 7J6 di JDK (agosto 2012). Se non hai aggiornato l'installazione Java per un po ', vai al sito Web di download Java per la versione più recente. 

Classi di base del framework

La creazione di un programma JavaFX inizia con la classe Application, dalla quale vengono estese tutte le applicazioni JavaFX. La tua classe principale dovrebbe chiamare il lanciare() metodo, che chiamerà il dentro() metodo e poi il inizio() metodo, attendere il completamento dell'applicazione e quindi chiamare il Stop() metodo. Di questi metodi, solo il inizio() il metodo è astratto e deve essere ignorato.

La classe Stage è il contenitore JavaFX di livello superiore. Quando viene lanciata un'applicazione, viene creato uno stage iniziale che viene passato al metodo di avvio dell'applicazione. Le fasi controllano le proprietà della finestra di base come titolo, icona, visibilità, resizabilità, modalità a schermo intero e decorazioni; quest'ultimo è configurato usando StageStyle. Le fasi aggiuntive possono essere costruite secondo necessità. Dopo aver configurato uno stage e aggiunto il contenuto, il mostrare() il metodo è chiamato.

Sapendo tutto questo, possiamo scrivere un esempio minimale che lancia una finestra in JavaFX:

importare javafx.application.Application; import javafx.stage.Stage; public class Example1 estende Application public static void main (String [] args) launch (args);  public void start (Stage theStage) theStage.setTitle ("Hello, World!"); theStage.show (); 

Strutturazione del contenuto

Il contenuto in JavaFX (come testo, immagini e controlli dell'interfaccia utente) è organizzato utilizzando una struttura di dati ad albero nota come un grafico di scena, che raggruppa e organizza gli elementi di una scena grafica. 

Rappresentazione di un grafico di scena JavaFX.

Un elemento generale di un grafico di scena in JavaFX è chiamato un nodo. Ogni nodo in un albero ha un singolo nodo "genitore", con l'eccezione di un nodo speciale designato come "root". Un gruppo è un nodo che può avere molti elementi del nodo "figlio". Le trasformazioni grafiche (traslazione, rotazione e scala) e gli effetti applicati a un gruppo si applicano anche ai suoi figli. I nodi possono essere disegnati usando i fogli di stile CSS (Cascading Style Sheets) di JavaFX, abbastanza simili ai CSS usati per formattare i documenti HTML.

La classe Scene contiene tutto il contenuto per un grafico di scena e richiede l'impostazione di un nodo radice (in pratica, questo è spesso un gruppo). È possibile impostare le dimensioni di una scena in modo specifico; in caso contrario, la dimensione di una scena verrà calcolata automaticamente in base al suo contenuto. Un oggetto Scene deve essere passato allo stage (dal setScene () metodo) per essere visualizzato.

Grafica di rendering

La grafica di rendering è particolarmente importante per i programmatori di giochi! In JavaFX, l'oggetto Canvas è un'immagine su cui possiamo disegnare testo, forme e immagini, usando l'oggetto GraphicsContext associato. (Per quegli sviluppatori che hanno familiarità con il toolkit Java Swing, questo è simile all'oggetto Graphics passato al dipingere() metodo nella classe JFrame.)

L'oggetto GraphicsContext contiene una vasta gamma di potenti capacità di personalizzazione. Per scegliere i colori per disegnare testo e forme, puoi impostare i colori di riempimento (interno) e di traccia (bordo), che sono oggetti di disegno: possono essere un singolo colore solido, un gradiente definito dall'utente (LinearGradient o RadialGradient) oppure anche un ImagePattern. Puoi anche applicare uno o più oggetti di stile Effect, come Illuminazione, Ombra o GaussianBlur, e cambiare i font dall'impostazione predefinita usando la classe Font. 

La classe Image semplifica il caricamento di immagini da una varietà di formati da file e disegni tramite la classe GraphicsContext. È facile costruire immagini generate proceduralmente utilizzando la classe WritableImage insieme alle classi PixelReader e PixelWriter.

Usando queste classi, possiamo scrivere un esempio in stile "Hello, World" molto più degno di quanto segue. Per brevità, includeremo semplicemente il inizio() metodo qui (salteremo le istruzioni di importazione e principale() metodo); tuttavia, il codice sorgente completo funzionante può essere trovato nel repository GitHub che accompagna questo tutorial.

public void start (Stage theStage) theStage.setTitle ("Esempio di canvas"); Radice di gruppo = nuovo gruppo (); Scene theScene = new Scene (root); theStage.setScene (theScene); Canvas canvas = new Canvas (400, 200); root.getChildren (). add (canvas); GraphicsContext gc = canvas.getGraphicsContext2D (); gc.setFill (Color.RED); gc.setStroke (Color.BLACK); gc.setLineWidth (2); Carattere theFont = Font.font ("Times New Roman", FontWeight.BOLD, 48); gc.setFont (theFont); gc.fillText ("Hello, World!", 60, 50); gc.strokeText ("Hello, World!", 60, 50); Immagine terra = nuova immagine ("earth.png"); gc.drawImage (earth, 180, 100); theStage.show (); 

Il ciclo di gioco

Quindi, dobbiamo fare i nostri programmi dinamico, il che significa che lo stato del gioco cambia nel tempo. Implementeremo un ciclo di gioco: un ciclo infinito che aggiorna gli oggetti del gioco e rende la scena sullo schermo, idealmente ad una velocità di 60 volte al secondo. 

Il modo più semplice per farlo in JavaFX è l'utilizzo della classe AnimationTimer, in cui un metodo (denominato maniglia()) può essere scritto che verrà chiamato ad una velocità di 60 volte al secondo, o il più vicino possibile a quella percentuale. (Questa classe non deve essere utilizzata solo per scopi di animazione, ma è in grado di fare molto di più.)

L'uso della classe AnimationTimer è un po 'complicato: poiché è una classe astratta, non può essere creata direttamente: la classe deve essere estesa prima che un'istanza possa essere creata. Tuttavia, per i nostri semplici esempi, estenderemo la classe scrivendo una classe interiore anonima. Questa classe interiore deve definire il metodo astratto maniglia(), che passerà un singolo argomento: il tempo corrente del sistema in nanosecondi. Dopo aver definito la classe interiore, invochiamo immediatamente il inizio() metodo, che inizia il ciclo. (Il ciclo può essere fermato chiamando il Stop() metodo.)

Con queste classi, possiamo modificare il nostro esempio "Hello, World", creando un'animazione composta dalla Terra che orbita intorno al Sole contro un'immagine di sfondo stellata.

public void start (Stage theStage) theStage.setTitle ("Esempio di timeline"); Radice di gruppo = nuovo gruppo (); Scene theScene = new Scene (root); theStage.setScene (theScene); Canvas canvas = new Canvas (512, 512); root.getChildren (). add (canvas); GraphicsContext gc = canvas.getGraphicsContext2D (); Immagine terra = nuova immagine ("earth.png"); Immagine sole = nuova immagine ("sun.png"); Spazio immagine = nuova immagine ("spazio.png"); final long startNanoTime = System.nanoTime (); nuovo AnimationTimer () handle void pubblico (long currentNanoTime) double t = (currentNanoTime - startNanoTime) / 1000000000.0; double x = 232 + 128 * Math.cos (t); doppio y = 232 + 128 * Math.sin (t); // l'immagine di sfondo cancella la tela gc.drawImage (spazio, 0, 0); gc.drawImage (earth, x, y); gc.drawImage (sun, 196, 196);  .inizio(); theStage.show (); 

Esistono modi alternativi per implementare un ciclo di gioco in JavaFX. Un approccio leggermente più lungo (ma più flessibile) coinvolge la classe Timeline, che è una sequenza di animazione costituita da un insieme di oggetti KeyFrame. Per creare un ciclo di gioco, la Timeline dovrebbe essere impostata per la ripetizione indefinita, e solo un singolo KeyFrame è richiesto, con la sua Durata impostata su 0.016 secondi (per raggiungere 60 cicli al secondo). Questa implementazione può essere trovata nel Example3T.java file nel repository GitHub.

Animazione basata su frame

Un altro componente di programmazione di gioco comunemente richiesto è l'animazione basata su frame: visualizzazione di una sequenza di immagini in rapida successione per creare l'illusione del movimento. 

Supponendo che tutto il ciclo di animazioni e tutti i frame vengano visualizzati per lo stesso numero di secondi, un'implementazione di base potrebbe essere la seguente:

public class AnimatedImage public Image [] frames; doppia durata pubblica; getFrame pubblica dell'immagine (doppia volta) int index = (int) ((time% (frames.length * duration)) / duration); return frames [indice]; 

Per integrare questa classe nell'esempio precedente, potremmo creare un UFO animato, inizializzando l'oggetto usando il codice:

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

... e, all'interno di AnimationTimer, aggiungendo la singola riga di codice:

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

... nel punto appropriato. Per un esempio di codice di lavoro completo, vedere il file Example3AI.java nel repository GitHub. 

Gestire l'input dell'utente

Rilevare e processare l'input dell'utente in JavaFX è semplice. Vengono chiamate le azioni dell'utente che possono essere rilevate dal sistema, come i tasti premuti e i clic del mouse eventi. In JavaFX, queste azioni provocano automaticamente la generazione di oggetti (come KeyEvent e MouseEvent) che memorizzano i dati associati (come il tasto premuto o la posizione del puntatore del mouse). Qualsiasi classe JavaFX che implementa la classe EventTarget, come una Scene, può "ascoltare" gli eventi e gestirli; negli esempi che seguono, mostreremo come impostare una scena per elaborare vari eventi.

Guardando la documentazione per la classe Scene, ci sono molti metodi che ascoltano per gestire diversi tipi di input da fonti diverse. Ad esempio, il metodo setOnKeyPressed () può assegnare un EventHandler che si attiverà quando viene premuto un tasto, il metodo setOnMouseClicked () può assegnare un EventHandler che si attiva quando viene premuto un pulsante del mouse e così via. La classe EventHandler ha uno scopo: incapsulare un metodo (chiamato maniglia()) che viene chiamato quando si verifica l'evento corrispondente. 

Quando si crea un EventHandler, è necessario specificare il genere dell'evento che gestisce: puoi dichiarare un Gestore di eventi o un Gestore di eventi, per esempio. Inoltre, EventHandlers vengono spesso creati come classi interne anonime, in quanto vengono in genere utilizzati una sola volta (quando vengono passati come argomento a uno dei metodi elencati sopra).

Gestione degli eventi della tastiera

L'input dell'utente viene spesso elaborato all'interno del ciclo di gioco principale, pertanto è necessario tenere un record di quali chiavi sono attualmente attive. Un modo per ottenere ciò è creare un oggetto ArrayList di oggetti String. Quando viene inizialmente premuto un tasto, aggiungiamo la rappresentazione String del KeyCode di KeyEvent all'elenco; quando la chiave viene rilasciata, la rimuoviamo dalla lista. 

Nell'esempio seguente, il canvas contiene due immagini di tasti freccia; ogni volta che viene premuto un tasto, l'immagine corrispondente diventa verde. 


Il codice sorgente è contenuto nel file Example4K.java nel repository GitHub.

public void start (Stage theStage) theStage.setTitle ("Esempio di tastiera"); Radice di gruppo = nuovo gruppo (); Scene theScene = new Scene (root); theStage.setScene (theScene); Canvas canvas = new Canvas (512 - 64, 256); root.getChildren (). add (canvas); Lista di array input = new ArrayList(); theScene.setOnKeyPressed (new EventHandler() handle void pubblico (KeyEvent e) String code = e.getCode (). toString (); // aggiungi solo una volta ... previ i duplicati se (! input.contains (code)) input.add (code); ); theScene.setOnKeyReleased (new EventHandler() handle void pubblico (KeyEvent e) String code = e.getCode (). toString (); input.remove (codice); ); GraphicsContext gc = canvas.getGraphicsContext2D (); Immagine a sinistra = nuova immagine ("left.png"); Immagine leftG = new Image ("leftG.png"); Immagine a destra = nuova immagine ("right.png"); Immagine rightG = new Image ("rightG.png"); new AnimationTimer () public void handle (long currentNanoTime) // Cancella il canvas gc.clearRect (0, 0, 512,512); if (input.contains ("LEFT")) gc.drawImage (leftG, 64, 64); else gc.drawImage (left, 64, 64); if (input.contains ("RIGHT")) gc.drawImage (rightG, 256, 64); else gc.drawImage (right, 256, 64);  .inizio(); theStage.show (); 

Gestire gli eventi del mouse

Ora diamo un'occhiata a un esempio che si concentra sulla classe MouseEvent piuttosto che sulla classe KeyEvent. In questo mini-gioco, il giocatore guadagna un punto ogni volta che si fa clic sul bersaglio.


Dato che gli EventHandler sono classi interne, qualsiasi variabile che usano deve essere finale o "efficacemente finale", il che significa che le variabili non possono essere reinizializzate. Nell'esempio precedente, i dati sono stati passati all'EventHandler per mezzo di un ArrayList, i cui valori possono essere modificati senza reinizializzazione (tramite il Inserisci() e rimuovere() metodi). 

Tuttavia, nel caso di tipi di dati di base, i valori non possono essere modificati una volta inizializzati. Se si desidera che EventHandler acceda ai tipi di dati di base modificati altrove nel programma, è possibile creare una classe wrapper che contenga variabili pubbliche o metodi getter / setter. (Nell'esempio sotto, intValue è una classe che contiene a pubblico int variabile chiamata valore.)

public void start (Stage theStage) theStage.setTitle ("Fai clic sul bersaglio!"); Radice di gruppo = nuovo gruppo (); Scene theScene = new Scene (root); theStage.setScene (theScene); Canvas canvas = new Canvas (500, 500); root.getChildren (). add (canvas); Circle targetData = new Circle (100,100,32); IntValue points = new IntValue (0); theScene.setOnMouseClicked (new EventHandler() public void handle (MouseEvent e) if (targetData.containsPoint (e.getX (), e.getY ())) double x = 50 + 400 * Math.random (); doppio y = 50 + 400 * Math.random (); targetData.setCenter (x, y); points.value ++;  else points.value = 0; ); GraphicsContext gc = canvas.getGraphicsContext2D (); Carattere theFont = Font.font ("Helvetica", FontWeight.BOLD, 24); gc.setFont (theFont); gc.setStroke (Color.BLACK); gc.setLineWidth (1); Immagine bullseye = new Image ("bullseye.png"); nuovo AnimationTimer () public void handle (long currentNanoTime) // Cancella il canvas gc.setFill (new Color (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 = "Points:" + points.value; gc.fillText (pointsText, 360, 36); gc.strokeText (pointsText, 360, 36);  .inizio(); theStage.show (); 

Il codice sorgente completo è contenuto nel repository GitHub; la classe principale è Example4M.java.

Creazione di una classe Sprite di base con JavaFX

Nei videogiochi, a folletto è il termine per una singola entità visiva. Di seguito è riportato un esempio di una classe Sprite che memorizza un'immagine e una posizione, oltre a informazioni sulla velocità (per le entità mobili) e la larghezza / altezza da utilizzare quando si calcolano le caselle di delimitazione ai fini del rilevamento delle collisioni. Abbiamo anche i metodi standard getter / setter per la maggior parte di questi dati (omessi per brevità) e alcuni metodi standard necessari nello sviluppo del gioco:

  • aggiornare(): calcola la nuova posizione in base alla velocità dello Sprite.
  • render (): disegna l'immagine associata alla tela (tramite la classe GraphicsContext) usando la posizione come coordinate.
  • getBoundary (): restituisce un oggetto Rectangle2D JavaFX, utile nel rilevamento delle collisioni grazie al metodo intersects.
  • interseca (): determina se il riquadro di delimitazione di questo Sprite si interseca con quello di un altro Sprite.
Sprite di classe pubblica immagine immagine privata; doppia posizione privata X; doppia posizione privataY; doppia velocityX privata; doppia velocità privataY; doppia larghezza privata; doppia altezza privata; // ... // metodi omessi per brevità // ... public void update (double time) positionX + = velocityX * time; posizioneY + = velocityY * tempo;  public void render (GraphicsContext gc) gc.drawImage (image, positionX, positionY);  public Rectangle2D getBoundary () return new Rectangle2D (positionX, positionY, width, height);  intersechi booleani pubblici (Sprite s) return s.getBoundary (). intersects (this.getBoundary ()); 

Il codice sorgente completo è incluso in Sprite.java nel repository GitHub.

Usando la classe Sprite

Con l'assistenza della classe Sprite, possiamo facilmente creare un semplice gioco di raccolta in JavaFX. In questo gioco, si assume il ruolo di una valigetta senziente il cui obiettivo è quello di raccogliere le molte borse di denaro che sono state lasciate in giro da un proprietario precedente incurante. I tasti freccia spostano il giocatore sullo schermo.

Questo codice prende molto in prestito dagli esempi precedenti: impostazione dei caratteri per visualizzare lo spartito, memorizzazione degli input da tastiera con ArrayList, implementazione del ciclo di gioco con un AnimationTimer e creazione di classi wrapper per valori semplici che devono essere modificati durante il ciclo di gioco.

Un segmento di codice di particolare interesse comporta la creazione di un oggetto Sprite per il giocatore (valigetta) e un oggetto ArrayList di oggetti Sprite per gli oggetti da collezione (sacchi di denaro):

Sprite briefcase = new Sprite (); briefcase.setImage ( "briefcase.png"); briefcase.setPosition (200, 0); Lista di array moneybagList = new ArrayList(); per (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 altro segmento di codice di interesse è la creazione del AnimationTimer, che ha il compito di:

  • calcolo del tempo trascorso dall'ultimo aggiornamento
  • impostazione della velocità del lettore in base ai tasti attualmente premuti
  • eseguire il rilevamento delle collisioni tra il giocatore e gli oggetti da collezione e aggiornare il punteggio e l'elenco degli oggetti da collezionare quando ciò si verifica (viene utilizzato un Iterator piuttosto che l'ArrayList direttamente per evitare un'eccezione di modifica simultanea durante la rimozione di oggetti dall'elenco)
  • rendere gli sprite e il testo sulla tela
nuovo AnimationTimer () handle void pubblico (long currentNanoTime) // calcola il tempo dall'ultimo aggiornamento. 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); // Iterator di rilevamento collisioni moneybagIter = moneybagList.iterator (); while (moneybagIter.hasNext ()) Sprite moneybag = moneybagIter.next (); if (briefcase.intersects (moneybag)) moneybagIter.remove (); score.value ++;  // render gc.clearRect (0, 0, 512,512); briefcase.render (gc); per (Sprite moneybag: moneybagList) moneybag.render (gc); String pointsText = "Cash: $" + (100 * score.value); gc.fillText (pointsText, 360, 36); gc.strokeText (pointsText, 360, 36);  .inizio();

Come al solito, il codice completo può essere trovato nel file di codice allegato (Example5.java) nel repository GitHub.

Prossimi passi

  • Esiste una raccolta di tutorial introduttivi sul sito Web di Oracle, che consente di apprendere le comuni attività JavaFX: Introduzione alle applicazioni di esempio JavaFX.
  • Potresti essere interessato a imparare come usare Scene Builder, un ambiente di layout visivo per la progettazione di interfacce utente. Questo programma genera FXML, che è un linguaggio basato su XML che può essere utilizzato per definire un'interfaccia utente per un programma JavaFX. Per questo, vedere JavaFX Scene Builder: Introduzione.
  • FX Experience è un blog eccellente, aggiornato regolarmente, che contiene informazioni e progetti di esempio di interesse per gli sviluppatori JavaFX. Molte delle demo elencate sono piuttosto ispiratrici!
  • José Pereda ha eccellenti esempi di giochi più avanzati costruiti con JavaFX nel suo repository GitHub.
  • Il progetto JFxtras è costituito da un gruppo di sviluppatori che hanno creato componenti JavaFX aggiuntivi che forniscono funzionalità comunemente necessarie attualmente mancanti da JavaFX.
  • Il progetto JavaFXPorts ti consente di impacchettare la tua applicazione JavaFX per la distribuzione su iOS e Android.
  • È necessario contrassegnare i riferimenti ufficiali per JavaFX, in particolare la guida JavaFX di Oracle e la documentazione API. 
  • Alcuni libri ben recensiti su JavaFX includono Pro JavaFX 8, JavaFX 8 - Introduzione di Esempio e, di particolare interesse per gli sviluppatori di giochi, Beginning Java 8 Games Development.

Conclusione

In questo tutorial, ho introdotto le classi JavaFX utili nella programmazione di giochi. Abbiamo lavorato attraverso una serie di esempi di crescente complessità, culminando in un gioco in stile collezione basato su sprite. Ora sei pronto per indagare su alcune delle risorse sopra elencate, o per tuffarti e iniziare a creare il tuo gioco. La migliore fortuna per te nei tuoi sforzi!