© WHISKHEELS/Shutterstock.com
Von „sehr einfach“ über Pong bis Breakout

"FXGL-Tutorial"


Die Agenda dieses Tutorials zu FXGL ist nicht gerade schmal: Nachdem es zu Beginn um Grundlagen geht und ein einfaches Spiel erstellt wird, werden wir im weiteren Verlauf Bild- und Soundeffekte, Sprite-Animationen und mit Java FX-Kollisionen hinzufügen. Zu guter Letzt werden wir mit FXGL einen simplen Pong- sowie einen ebensolchen Breakout-Klon erstellen. Lassen Sie uns durchstarten!

Zunächst werden wir ein sehr einfaches Spiel erstellen. Ich gehe davon aus, dass FXGL bereits vorhanden ist und sich ein Java-Projekt mit Zugriff auf die neueste Version von FXGL in der IDE der Wahl befindet. Es ist zu beachten, dass dieses Tutorial für FXGL 11.0+ und Java 11+ gedacht ist.

Phase 1: die Definition des Ziels

Zuerst definieren wir ein paar Eckdaten für unser einfaches Spiel:

  • Es soll in einem Fenster mit der Auflösung von 600×600 Pixeln laufen.

  • Ein Spieler soll sich auf dem Bildschirm befinden, dargestellt durch ein blaues Rechteck.

  • Die Spielfigur soll mit den W-, A-, S- und D-Tasten des Keyboards bewegt werden.

  • Das UI wird durch eine einzelne Textzeile dargestellt.

  • Der angezeigte Text wird aktualisiert, sobald sich der Spieler bewegt. Angezeigt wird in Textform, um wie viele Pixel sich der Spieler während seiner Lebensdauer bewegt hat.

Am Ende sollten Sie so etwas herausbekommen, wie in Abbildung 1 gezeigt wird.

baimagambetov_javafx_1.tif_fmt1.jpgAbb. 1: Ein einfaches Spiel [1]

Obwohl das Ganze vielleicht nicht wie ein Spiel aussieht, wird es Ihnen helfen, die grundlegenden Funktionen von FXGL zu verstehen. Nachdem Sie dieses Tutorial beendet haben, können Sie basierend auf diesen Grundlagen eine Vielzahl von einfachen Spielen erstellen.

Phase 2: die Vorbereitung

Jetzt, da wir eine grobe Vorstellung davon haben, wie unser Spiel aussehen soll, können wir zurück in die IDE gehen und ein Paket für unser Spiel erstellen.

Achtung: Die Verzeichnisstruktur ist ähnlich wie die Maven-Verzeichnisstruktur, aber wenn Sie nicht wissen, was das ist, machen Sie sich keine Sorgen. Wir werden die Struktur zu einem späteren Zeitpunkt behandeln. An dieser Stelle ist es ausreichend, src als Hauptquellverzeichnis zu haben. Ich werde „tutorial“ als Namen für das Paket verwenden.

  • Erstellen Sie das Paket tutorial in Ihrer IDE.

  • Erstellen Sie eine Java-Klasse mit dem Namen BasicGameApp innerhalb des tutorial-Pakets.

Es ist üblich, dass man app an die Klasse anhängt, in der sich ihre main()-Methode befindet. Auf diese Weise können andere Entwickler leicht erkennen, wo sich der wichtigste Einstiegspunkt Ihres Spiels befindet. Ich schlage vor, dass Sie ihre BasicGameApp-Klasse öffnen und die Importe aus Listing 1 hinzufügen. Das erleichtert die nächsten Schritte maßgeblich. Jetzt sind wir zum Programmieren bereit.

Listing 1

import com.almasb.fxgl.app.GameApplication; import com.almasb.fxgl.app.GameSettings; import com.almasb.fxgl.dsl.FXGL; import com.almasb.fxgl.entity.Entity; import com.almasb.fxgl.input.Input; import com.almasb.fxgl.input.UserAction; import javafx.scene.input.KeyCode; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; import java.util.Map;

Phase 3: die Programmierung

Um FXGL verwenden zu können, muss Ihre App-Klasse die GameApplication erweitern und die initSettings()-Methode überschreiben:

public class BasicGameApp extends GameApplication { @Override protected void initSettings(GameSettings settings) {} }

Die meisten IDEs werden die überschriebene Methode automatisch generieren, sobald man GameApplication erweitert hat. Nun wollen wir in der Lage sein, das Spiel zu starten. Um das zu bewerkstelligen fügen wir einfach Folgendes hinzu:

public static void main(String[] args) { launch(args); }

Wer bereits in JavaFX programmiert hat, dem wird auffallen, dass es sich um die exakt gleiche Signatur handelt, die man auch benutzt, um eine JavaFX-Applikation zu starten. Kurz und knapp gesagt handelt es sich bei FXGL um nichts anderes als um eine JavaFX-Applikation, die über Funktionen zur Spieleentwicklung verfügt.

Herausforderung 1: das Fenster

Zu diesem Zeitpunkt sollte es schon möglich sein, das Spiel auszuführen, aber vorher optimieren wir ein paar Einstellungen (Listing 2).

Listing 2

@Override protected void initSettings(GameSettings settings) { settings.setWidth(600); settings.setHeight(600); settings.setTitle("Basic Game App"); settings.setVersion("0.1"); }

Wie man sehen kann, werden alle Einstellungen innerhalb von initSettings() geändert. Sobald die Einstellungen festgelegt worden sind, können diese während der Laufzeit nicht geändert werden. Jetzt kann man in seiner IDE auf Ausführen klicken, was das Spiel in einem 600×600 Pixel großen Fenster mit dem Titel „Basic Game App“ starten sollte.

Wir haben damit die erste Herausforderung gemeistert. Ganz leicht, nicht wahr?

Herausforderung 2: der Spieler

Der nächste Schritt besteht darin, den Spieler hinzuzufügen und ihn auf dem Bildschirm darzustellen. Das werden wir in initGame() machen. Kurz gesagt richtet man hier alles ein, was vor Spielbeginn fertig sein muss (Listing 3).

Listing 3

private Entity player; @Override protected void initGame() { player = FXGL.entityBuilder() .at(300, 300) .view(new Rectangle(25, 25, Color.BLUE)) .buildAndAttach(); }

Achtung: Für das Speichern bzw. Laden von Systemen ist es wichtig, dass man Instanzebenenfelder nicht bei der Deklaration initialisiert, sondern in initGame().

Den Spieler hinzuzufügen ist eine ganze Menge Arbeit auf einmal, wenn man nicht mit Fluent API vertraut ist. Deswegen wollen wir es langsam angehen. Es gibt ein Instanzebenenfeld, das player heißt und vom Typ Entity ist. Eine Entity ist im Grunde genommen ein Spielobjekt. Das ist alles, was man im Moment darüber wissen muss. FXGL.entityBuilder() ist die bevorzugte Methode, um Entitys zu erstellen. Durch den Aufruf von .at() positionieren wir die Entity dort, wo wir sie haben wollen. In diesem Fall ist das bei x = 300 und y = 300. Achtung: Die Position einer Entity in FXGL ist ihr oberer linker Punkt, wie in JavaFX.

Dann sagen wir dem Builder, dass er eine Ansicht der Entity erstellen soll, wofür er den UI-Knoten verwendet, den wir als Parameter übergeben. In diesem Fall ist das ein Standard-JavaFX-Rectangle mit width = 25, height = 25 und der Farbe Blau. Übrigens: Man kann jedes Objekt, das auf JavaFX-Knoten basiert, verwenden, was ziemlich cool ist.

Schließlich rufen wir .buildAndAndAttach() auf. Durch den Aufruf von build können wir die Referenz auf die Entity erhalten, die wir erstellt haben. Der attach-Teil ermöglicht es uns praktischerweise, die erstellte Entity direkt in die Spielwelt zu integrieren. Wenn man das Spiel ausführt, sollte man jetzt ein blaues Rechteck in der Mitte des Bildschirms sehen.

Großartig, wir haben gerade die zweite Herausforderung gemeistert!

Herausforderung 3: der Input

Wir werden nun mit der Herausforderung „Benutzereingabe“ fortfahren. Den Code für das Handling des User-Inputs fügen wir in initInput() ein (Listing 4).

Listing 4

@Override protected void initInput() { Input input = FXGL.getInput(); input.addAction(new UserAction("Move Right") { @Override protected void onAction() { player.translateX(5); // move right 5 pixels } }, KeyCode.D); }

Gehen wir diesen Ausschnitt Zeile für Zeile durch. Zuerst holen wir uns das Inputobjekt. Um die meisten FXGL-Funktionen zu verwenden, muss man nur FXGL.*** aufrufen. Die IDE zeigt einem dann alle Funktionen an, die man aufrufen kann.

Als Nächstes fügen wir eine Aktion hinzu, gefolgt von einem Schlüsselcode. Auch hier gilt: Wenn man JavaFX schon einmal verwendet hat, weiß man, dass es sich hierbei um genau die gleichen Schlüsselcodes handelt, die auch in Event Handlern verwendet werden. Wir sagen im Grunde genommen: „Wenn ‚D‘ gedrückt wird, führe die Aktion durch, die wir erstellt haben.“

Schauen wir uns nun die Aktion selbst an. Wenn wir eine Aktion erstellen, geben wir ihr auch einen Namen: Move Right. Das ist wichtig, da diese direkt an die Steuerungen und Menüsysteme weitergeleitet wird, wo der Benutzer sie jederzeit ändern kann. Der Name muss also für den Benutzer aussagekräftig und einzigartig sein.

Sobald wir die Aktion erstellt haben, überschreiben wir eine ihrer Methoden (diesmal onAction()) und stellen etwas Code bereit. Dieser Code wird aufgerufen, wenn die Aktion ausgeführt wird, d. h. wenn D gedrückt wird. Erinnern Sie sich an die Voraussetzung: Wir wollen den Spieler bewegen. Wenn also D gedrückt wird, wollen wir den Spieler nach rechts bewegen. Wir rufen also player.translateX(5) auf, was seine X-Koordinate um 5 Pixel verschiebt.

Das führt dazu, dass sich die Spielerentität um 5 Pixel nach rechts bewegt. Sie können sich wahrscheinlich denken, wie der Rest des Eingabecodes aussehen wird, aber für den Fall der Fälle sehen Sie in Listing 5 die Angaben für W, S, D und A.

Listing 5

@Override protected void initInput() { Input input = FXGL.getInput(); input.addAction(new UserAction("Move Right") { @Override protected void onAction() { player.translateX(5); // move right 5 pixels } }, KeyCode.D); input.addAction(new UserAction("Move Left") { @Override protected void onAction() { player.translateX(-5); // move left 5 pixels } }, KeyCode.A); input.addAction(new UserAction("Move Up") { @Override protected void onAction() { player.translateY(-5); // move up 5 pixels } }, KeyCode.W); input.addAction(new UserAction("Move Down") { @Override protected void onAction() { player.translateY(5); // move down 5 pixels } }, KeyCode.S); }

Die dritte Herausforderung ist damit erledigt und abgehakt. Wir haben schon mehr als die Hälfte erledigt, gut gemacht!

Herausforderung 4: das UI

Jetzt gehen wir zum nächsten Teil über – dem UI, dessen Handhabung wir in initUI() definieren (Listing 6).

Listing 6

@Override protected void initUI() { Text textPixels = new Text(); textPixels.setTranslateX(50); // x = 50 textPixels.setTranslateY(100); // y = 100 FXGL.getGameScene().addUINode(textPixels); // add to the scene graph }

Für die meisten UI-Objekte verwenden wir einfach JavaFX-Objekte, da es nicht notwendig ist, das Rad neu zu erfinden. Als wir eine Entity zur Welt hinzugefügt haben, hat das Spiel die Tatsache aufgegriffen, dass mit der Entity eine bestimmte Ansicht assoziiert ist. Daher hat das Spiel die Entity auf „magische Weise“ der Spielanzeige hinzugefügt. Bei UI-Objekten sind wir für ihre Einbettung in die Spielanzeige verantwortlich und können das durch den Aufruf von getGameScene().addUINode() tun.

Das ist alles, was für das Meistern der vierten Herausforderung gemacht werden muss. Weiter so!

Herausforderung 5: das Gameplay

Um die letzte Herausforderung zu meistern, werden wir eine Spielvariable verwenden. In FXGL kann von jedem Teil des Spiels aus auf eine Spielvariable zugegriffen und diese geändert werden. In gewisser Weise ist es eine globale Variable, deren Umfang an die FXGL-Spielinstanz gebunden ist. Darüber hinaus können solche Variablen auch an etwas gebunden werden (ähnlich wie bei JavaFX-Eigenschaften). Wir beginnen damit, eine solche Variable zu erstellen:

@Override protected void initGameVars(Map<String, Object> vars) { vars.put("pixelsMoved", 0); }

Dann müssen wir die Variable aktualisieren, sobald sich der Spieler bewegt. Das können wir im Abschnitt Inputhandling machen (Listing 7).

Listing 7

input.addAction(new UserAction("Move Right") { @Override protected void onAction() { player.translateX(5); FXGL.getGameState().increment("pixelsMoved", +5); } }, KeyCode.D);

Das Gleiche machen wir mit dem Rest der Bewegungsabläufe (links, oben und unten). Die letzte Herausforderung dieses Abschnitts besteht darin, unser UI-Textobjekt an die Variable pixelsMoved zu binden. In initUI() können wir folgendes tun, sobald wir das textPixels-Objekt erstellt haben:

textPixels.textProperty().bind(FXGL.getGameState().intProperty("pixelsMoved").asString());

Anschließend wird im Text im User Interface anzeigen, um wie viele Pixel sich der Spieler automatisch bewegt hat.

Jetzt haben wir ein einfaches FXGL-Spiel. Hoffentlich hat es Ihnen Spaß gemacht. In Listing 8 sehen Sie den vollständigen Quellcode zu diesem Abschnitt unseres Tutorials.

Listing 8

package tutorial; import com.almasb.fxgl.app.GameApplication; import com.almasb.fxgl.app.GameSettings; import com.almasb.fxgl.dsl.FXGL; import com.almasb.fxgl.entity.Entity; import com.almasb.fxgl.input.Input; import com.almasb.fxgl.input.UserAction; import javafx.scene.input.KeyCode; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; import java.util.Map; public class BasicGameApp extends GameApplication { @Override protected void initSettings(GameSettings settings) { settings.setWidth(600); settings.setHeight(600); settings.setTitle("Basic Game App"); settings.setVersion("0.1"); } @Override protected void initInput() { Input input = FXGL.getInput(); input.addAction(new UserAction("Move Right") { @Override protected void onAction() { player.translateX(5); // move right 5 pixels FXGL.getGameState().increment("pixelsMoved", +5); } }, KeyCode.D); input.addAction(new UserAction("Move Left") { @Override protected void onAction() { player.translateX(-5); // move left 5 pixels FXGL.getGameState().increment("pixelsMoved", +5); } }, KeyCode.A); input.addAction(new UserAction("Move Up") { @Override protected void onAction() { player.translateY(-5); // move up 5 pixels FXGL.getGameState().increment("pixelsMoved", +5); } }, KeyCode.W); input.addAction(new UserAction("Move Down") { @Override protected void onAction() { player.translateY(5); // move down 5 pixels FXGL.getGameState().increment("pixelsMoved", +5); } }, KeyCode.S); } @Override protected void initGameVars(Map<String, Object> vars) { vars.put("pixelsMoved", 0); } private Entity player; @Override protected void initGame() { player = FXGL.entityBuilder() .at(300, 300) .view(new Rectangle(25, 25, Color.BLUE)) .buildAndAttach(); } @Override protected void initUI() { Text textPixels = new Text(); textPixels.setTranslateX(50); // x = 50 textPixels.setTranslateY(100); // y = 100 textPixels.textProperty().bind(FXGL.getGameState().intProperty("pixelsMoved").asString()); FXGL.getGameScene().addUINode(textPixels); // add to the scene graph } public static void main(String[] args) { launch(args); } }

Nun widmen wir uns den folgenden drei Aufgaben:

  • Die Spielfigur soll durch ein Bild dargestellt werden.

  • Dem UI des Spiels soll ein Bild hinzugefügt werden.

  • Ein Sound soll abgespielt werden, wenn die Zahl der bewegten Pixel durch 100 teilbar ist.

Am Schluss sollte der Zustand in Abbildung 2 erreicht werden.

baimagambetov_javafx_2.tif_fmt1.jpgAbb. 2: Das ist der Zustand, den wir anstreben [2]

Vorbereitung

Bevor wir durchstarten können, erstellen wir mit Hilfe des Projekts aus dem vorherigen Abschnitt das Verzeichnis für die assets in:

  • src/main/resources/, wenn wir Maven oder Gradle verwenden.

  • src, wenn wir Uber-JARs (als externe Bibliothek) einsetzen.

Achtung: Auf GitHub [3] habe ich weitere Informationen über die Standardverzeichnisstruktur zusammengetragen.

Nun erstellen wir die Verzeichnisse textures und sounds in assets. Ich werde exemplarisch das Bild unter [4] und den Sound unter [5] in unserem Tutorial verwenden. Zum Herunterladen klicken Sie einfach auf den Link und dann auf Download. Es spielt aber keine Rolle, ob sie meine Beispieldateien verwenden: Sie können gerne auch Ihre eigenen nutzen.

Ist das getan, platzieren wir das Bild und den Sound, den wir in unserem Spiel verwenden möchten, in textures bzw. sounds.

Feature 1: Spielfigur

Zuerst ersetzen wir das blaue Rechteck, das aktuell noch den Spieler darstellt, durch ein Bild unserer Wahl. Das lässt sich leicht erledigen, wenn man mit einem Fluent API arbeitet. Tatsächlich gibt es nur eine Zeile Code aus dem vorherigen Teil, die wir ersetzen müssen (Listing 9).

Listing 9

@Override protected void initGame() { player = FXGL.entityBuilder() .at(300, 300) //....

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang