It’s been a long way for Java FX from the days of the F3 project the current release 2.2. Today there are many options how to create a Java FX application .. you may be using Java, Scala, Groovy or Visage, you may create your application in a programmatic way using the comfortable integrated builders or you may create your views using XML layouts and easy data-bindings with a few annotations.
If you need to bind your UI component properties to a specific application state, there’s a nice properties- and bindings API that makes your life easier.
In the following tutorial, I’m going to create a simple game application – one version using FXML templates, model- and controller classes and using external stylesheets – the other version as a programmatic version in one java class.
Finally I’m showing how easy it is to create a shippable application either as runnable jar or as Java Web Start/JNLP application by using Gradle and the Java FX Plugin for Gradle.
Game Rules
Our game should follow these simple rules:
-
The game is won when all boxes are hit
-
The game is lost when the ball reaches the lower boundary of the game raster
-
When the ball hits the left, top or right wall it bounces back and its speed is incremented
-
The player may move the paddle using his mouse and drag the paddle on the y-axis
-
When the ball hits the paddle, its speed is incremented
-
A progress bar with a label gives an information how many boxes are left
-
The game is started when the player presses the start button
-
When the game is lost, pressing the start button restarts the game
Creating a View using FXML Bindings
There are different ways to construct the graphical user interface and arrange its elements and ui components.
We’re using xml based declaration here – if you prefer to create your user interface in a programmatic way, please feel free to have a look at the chapter “Single class, no XML version”.
When using a modern IDE like Eclipse/IntelliJ/NetBeans the editor should help us by offering suggestions as we type and by marking missing references to a field in the controller class.
So first of all, we need to import the classes we’re using here – this is done by adding an <?import /> element to the layout.
If we wanted to add a controller class for the view, we should specify it using fx:controller .. e.g.: fx:controller=”com.hascode.jfx.game.BallGameController”
When we want to bind an element from our view template to a field in the controller class, we’re using the attribute fx:id to reference to a field.
An example: Our Group element contains fx:id=”area” – so BallGameController must contain a field of type Group named area.
We should avoid any definition here that can be achieved using style-sheets.
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import javafx.collections.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.effect.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Hyperlink?>
<Group xmlns:fx="http://javafx.com/fxml" fx:controller="com.hascode.jfx.game.BallGameController" fx:id="area">
<Circle fx:id="ball" radius="10.0" fill="BLACK" />
<Rectangle fx:id="borderTop" x="0" y="30" width="500" height="2" />
<Rectangle fx:id="borderBottom" x="0" y="500" width="500" height="2"/>
<Rectangle fx:id="borderLeft" x="0" y="0" width="2" height="500"/>
<Rectangle fx:id="borderRight" x="498" y="0" width="2" height="500"/>
<Rectangle fx:id="paddle" x="200" y="460" width="100" height="15" layoutX="20" fill="BLACK"/>
<Text fx:id="gameOverText" text="Game Over" fill="RED" layoutX="150" layoutY="330"/>
<Text fx:id="winnerText" text="You've won!" fill="GREEN" layoutX="150" layoutY="330"/>
<ToolBar minWidth="500">
<Button fx:id="startButton" text="Start"/>
<Button fx:id="quitButton" text="Quit"/>
<ProgressBar fx:id="progressBar" progress="100"/>
<Label fx:id="remainingBlocksLabel"/>
</ToolBar>
<ToolBar minWidth="500" layoutY="500">
<Hyperlink text="www.hascode.com" layoutX="360" layoutY="505" />
</ToolBar>
</Group>
Creating the Ball Game Application
This is the entry point for our application. Our class extends javafx.application.Application and initializes the stage and renders a scene using a defined FXML template.
In addition we’re setting the application title and an icon for the application by applying it to our stage.
package com.hascode.jfx.game;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.SceneBuilder;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class BallGame extends Application {
private static final String VIEW_GAME = "/view/GameView.fxml";
private static final String STYLESHEET_FILE = "/stylesheet/style.css";
public static final Image ICON = new Image(
BallGame.class.getResourceAsStream("/image/head.png"));
@Override
public void start(final Stage stage) throws Exception {
initGui(stage);
}
private void initGui(final Stage stage) throws IOException {
Parent root = FXMLLoader.load(getClass().getResource(VIEW_GAME));
Scene scene = SceneBuilder.create().root(root).width(500).height(530)
.fill(Color.GRAY).build();
scene.getStylesheets().add(STYLESHEET_FILE);
stage.setScene(scene);
stage.setTitle("hasCode.com - Java FX 2 Ball Game Tutorial");
stage.getIcons().add(ICON);
stage.show();
}
public static void main(final String... args) {
Application.launch(args);
}
}
Defining the Game Model
The model is a class that encapsulates our game’s state and makes use of Java FX Properties and Bindings framework that allows us to bind properties of our UI elements to a specific state.
There is an interesting tutorial to be found at the Oracle.com website about using properties and bindings.
My design here is not very elegant but sufficient for the purpose of a tutorial:
package com.hascode.jfx.game;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.image.ImageView;
public class GameModel {
// amount of horizontal blocks.
private final int INITIAL_BLOCKS_HORIZONTAL = 10;
// amount of vertical blocks.
private final int INITIAL_BLOCKS_VERTICAL = 5;
// amount of blocks total = vertical * horizontal
private final int INITIAL_AMOUNT_BLOCKS = getInitialBlocksHorizontal()
* getInitialBlocksVertical();
// coordinates of the ball
private final DoubleProperty ballX = new SimpleDoubleProperty();
private final DoubleProperty ballY = new SimpleDoubleProperty();
// x coordinate of the paddle
private final DoubleProperty paddleX = new SimpleDoubleProperty();
// game is stopped?
private final BooleanProperty gameStopped = new SimpleBooleanProperty();
// game is lost?
private final BooleanProperty gameLost = new SimpleBooleanProperty(false);
// game is won?
private final BooleanProperty gameWon = new SimpleBooleanProperty(false);
// amount of boxes left
private final DoubleProperty boxesLeft = new SimpleDoubleProperty(
getInitialAmountBlocks());
// ball is moving in direction: down?
private boolean movingDown = true;
// ball is moving in direction: right?
private boolean movingRight = true;
// ball moving speed
private double movingSpeed = 1.0;
// paddle drag/translate x
private double paddleDragX = 0.0;
private double paddleTranslateX = 0.0;
// a collection of image elements
private final ObservableList<ImageView> boxes = FXCollections
.observableArrayList();
public void reset() {
getBoxesLeft().set(getInitialAmountBlocks());
for (ImageView r : boxes) {
r.setVisible(true);
}
setMovingSpeed(1.0);
setMovingDown(true);
setMovingRight(true);
getBallX().setValue(250);
getBallY().setValue(350);
paddleX.setValue(175);
gameStopped.set(true);
getGameLost().set(false);
getGameWon().set(false);
setPaddleDragX(0.);
setPaddleTranslateX(0.);
}
// getter & setter ommitted
}
The Game Controller
In the first step, we’re using @FXML annotations to bind those UI elements from the XML view template to the controller that we need for further modification or data bindings to the model.
To make this work, the controller class needs to implement javafx.fxml.Initializable.
Besides the ui element bindings, we’re creating a new instance of our model class and we’re creating a new timeline running infinitely to trigger our heartbeat event handler every 10 milliseconds.
package com.hascode.jfx.game;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.animation.TimelineBuilder;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Group;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.image.ImageView;
import javafx.scene.image.ImageViewBuilder;
import javafx.scene.input.MouseEvent;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.util.Duration;
public class BallGameController implements Initializable {
// UI ELEMENTS
@FXML
private Group area;
@FXML
private Circle ball;
@FXML
private Rectangle borderTop;
@FXML
private Rectangle borderBottom;
@FXML
private Rectangle borderLeft;
@FXML
private Rectangle borderRight;
@FXML
private Rectangle paddle;
@FXML
private Text gameOverText;
@FXML
private Text winnerText;
@FXML
private Button startButton;
@FXML
private Button quitButton;
@FXML
private ProgressBar progressBar;
@FXML
private Label remainingBlocksLabel;
// GAME MODEL
private final GameModel model = new GameModel();
// GAME HEARTBEAT
private final EventHandler<ActionEvent> pulseEvent = new EventHandler<ActionEvent>() {
@Override
public void handle(final ActionEvent evt) {
checkWin();
checkCollisions();
updateBallPosition();
}
};
// THE TIMELINE, RUNS EVERY 10MS
private final Timeline heartbeat = TimelineBuilder.create()
.keyFrames(new KeyFrame(new Duration(10.0), pulseEvent))
.cycleCount(Timeline.INDEFINITE).build();
/*
* (non-Javadoc)
*
* @see javafx.fxml.Initializable#initialize(java.net.URL,
* java.util.ResourceBundle)
*/
@Override
public void initialize(final URL url, final ResourceBundle bundle) {
bindPaddleMouseEvents();
bindStartButtonEvents();
bindQuitButtonEvents();
bindElementsToModel();
initializeBoxes();
initializeGame();
area.requestFocus();
}
}
The methods called from initialize() are described in detail in the following section:
Paddle Drag and Drop Events
We want to be able to move the paddle on the y-axis using drag and drop – so we’re binding a MousePressedEvent and a MouseDraggedEvent to change the paddle’s coordinates.
/**
* binds events to drag the paddle using the mouse.
*/
private void bindPaddleMouseEvents() {
paddle.setOnMousePressed(new EventHandler<MouseEvent>() {
@Override
public void handle(final MouseEvent evt) {
model.setPaddleTranslateX(model.getPaddleTranslateX() + 150);
model.setPaddleDragX(evt.getSceneX());
}
});
paddle.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override
public void handle(final MouseEvent evt) {
if (!model.getGameStopped().get()) {
double x = model.getPaddleTranslateX() + evt.getSceneX()
- model.getPaddleDragX();
model.getPaddleX().setValue(x);
}
}
});
}
Start Button Event Handler
The start button should start or restart the game and this means:
-
The game area needs to be reset
-
The model needs to know that the game is running
-
The timeline should continue to run
/**
* binds events to the start button. by pressing the start button, the game
* is initialized and the timeline execution is started.
*/
private void bindStartButtonEvents() {
startButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(final ActionEvent evt) {
initializeGame();
model.getGameStopped().set(false);
heartbeat.playFromStart();
}
});
}
Quit Button Event Handler
Pressing the quit button should shut down the application.
We’re binding a new action event handler to the quit button and make use of javafx.application.Platform exit method instead of using System.exit().
/**
* creates event handler for the quit button. pressing it immediatly quits
* the application.
*/
private void bindQuitButtonEvents() {
quitButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(final ActionEvent evt) {
Platform.exit();
}
});
}
Model Data Binding
We’re binding several element properties to the model class here:
-
the start button is enabled/disabled state
-
the ball’s x and y coordinates
-
the paddle’s x coordinate
-
the visibility of the game-over-text and the winner-text
-
the progress bar’s progress state
-
the progress bar’s label – we’re using the javafx.beans.binding.Bindings class here to create a dynamic string, bound to the model that displays the amount of boxes left
/**
* binds ui elements to model state
*/
private void bindElementsToModel() {
startButton.disableProperty().bind(model.getGameStopped().not());
ball.centerXProperty().bind(model.getBallX());
ball.centerYProperty().bind(model.getBallY());
paddle.xProperty().bind(model.getPaddleX());
gameOverText.visibleProperty().bind(model.getGameLost());
winnerText.visibleProperty().bind(model.getGameWon());
progressBar.progressProperty().bind(
model.getBoxesLeft().subtract(model.getInitialAmountBlocks())
.multiply(-1).divide(model.getInitialAmountBlocks()));
remainingBlocksLabel.textProperty().bind(
Bindings.format("%.0f boxes left", model.getBoxesLeft()));
}
Initializing the target Boxes
We’re creating a bunch of boxes to shoot at with the ball.
For rendering each box we’re using an image (the same as the application’s icon) here.
The amount of rows and columns is defined in our model class.
/**
* initializes the boxes.
*/
private void initializeBoxes() {
int startX = 15;
int startY = 30;
for (int v = 1; v <= model.getInitialBlocksVertical(); v++) {
for (int h = 1; h <= model.getInitialBlocksHorizontal(); h++) {
int x = startX + (h * 40);
int y = startY + (v * 40);
ImageView imageView = ImageViewBuilder.create()
.image(BallGame.ICON).layoutX(x).layoutY(y).build();
model.getBoxes().add(imageView);
}
}
area.getChildren().addAll(model.getBoxes());
}
Game Initialization
We’re doing not much here .. just resetting our model..
/**
* initializes the game, is called for every new game
*/
private void initializeGame() {
model.reset();
}
Check if the game is won
The game is one when there’s no box left on the game stage. If the game is won, we need to tell it to the model and to stop the heartbeat event.
/**
* checks if the game is won.
*/
private void checkWin() {
if (0 == model.getBoxesLeft().get()) {
model.getGameWon().set(true);
model.getGameStopped().set(true);
heartbeat.stop();
}
}
Check for ball collisions
We’re using javafx.scene.control.Control‘s intersect method to check for a collision with the walls, the paddle or the dead zone aka borderBottom.
When the ball hits the paddle or the left, right or top wall, its speed is incremented, when the ball hits the left or right wall, we’re changing the ball’s direction on the x-axis, when the ball hits the top wall, we’re changing its direction on the y-axis.
When the ball intersects the borderBottom/ dead zone, the game is over and we’re stopping the Timeline and update the model to reflect the fact that the game is lost.
/**
* checks if the ball has collisions with the walls or the paddle.
*/
private void checkCollisions() {
checkBoxCollisions();
if (ball.intersects(paddle.getBoundsInLocal())) {
model.incrementSpeed();
model.setMovingDown(false);
}
if (ball.intersects(borderTop.getBoundsInLocal())) {
model.incrementSpeed();
model.setMovingDown(true);
}
if (ball.intersects(borderBottom.getBoundsInLocal())) {
model.getGameStopped().set(true);
model.getGameLost().set(true);
heartbeat.stop();
}
if (ball.intersects(borderLeft.getBoundsInLocal())) {
model.incrementSpeed();
model.setMovingRight(true);
}
if (ball.intersects(borderRight.getBoundsInLocal())) {
model.incrementSpeed();
model.setMovingRight(false);
}
if (paddle.intersects(borderRight.getBoundsInLocal())) {
model.getPaddleX().set(350);
}
if (paddle.intersects(borderLeft.getBoundsInLocal())) {
model.getPaddleX().set(0);
}
}
Eliminate a box when hit by the ball
When the ball hits a box, we’re hiding the box on the game raster and update the model to know how much boxes are left.
/**
* checks if the ball collides with one or more of the boxes. if there's a
* collision, the box is removed.
*/
private void checkBoxCollisions() {
for (ImageView r : model.getBoxes()) {
if (r.isVisible() && ball.intersects(r.getBoundsInParent())) {
model.getBoxesLeft().set(model.getBoxesLeft().get() - 1);
r.setVisible(false);
}
}
}
Calculate ball movement speed and direction
Depending on the current speed of the ball, we’re calculating the new ball position on the x- and y-axis and finally we’re updating the model to reflect these changes.
/**
* updates the ball position by calculating the ball's speed, position and
* direction.
*/
private void updateBallPosition() {
double x = model.isMovingRight() ? model.getMovingSpeed() : -model
.getMovingSpeed();
double y = model.isMovingDown() ? model.getMovingSpeed() : -model
.getMovingSpeed();
model.getBallX().set(model.getBallX().get() + x);
model.getBallY().set(model.getBallY().get() + y);
}
Adjust look and feel using a Stylesheet
This is our stylesheet/style.css – we’re just adding some basic styles that are applied to .root, and some drop-shadow effects to the text and some game elements.
The complete reference for Java FX CSS stylesheets can be found here.
.root {
-fx-font-size: 16pt;
-fx-font-family: "Arial";
-fx-width: 500px;
-fx-height: 530px;
}
#gameOverText,#winnerText {
-fx-font-size: 40pt;
-fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 100), 1, 1, 4, 2 );
}
#paddle,#ball {
-fx-effect: dropshadow(three-pass-box, #000000, 10.0,0.0,0.5,4.0);
}
Gradle Java FX Plugin – Building the App
I am using Danno Ferrin’s Java FX plugin for Gradle here to create a runnable jar and the Java Web Start / JNLP files.
The plugin really eases the build process here and there are only two steps needed to build the application with this plugin.
First of all, add the following lines to your build.gradle in the project directory:
apply from: 'http://dl.bintray.com/content/shemnon/javafx-gradle/0.3.0/javafx.plugin'
javafx {
mainClass = 'com.hascode.jfx.game.BallGame'
}
The first statement adds the Java FX plugin, in the second one is needed to specify the main class for the build process.
After running the following command, you can find the application build in the directory build/distributions
gradle assemble
Download the Game as runnable Jar
I have put the compiled game as runnable Jar on GitHub:
You may run the game with Oracle JRE >= Java 7 Update 6 installed like this:
java -cp jfx-ball-game.jar
Single Class, no XML Version
If you’d like to see an alternative not using FXML but a lot of chained element builders I have build the game in a single Java class (loong) ..
Please feel free to have a look at my SingleClassNoXmlBallGame.java at GitHub.
Tutorial Sources
Please feel free to download the tutorial sources from my GitHub repository, fork it there or clone it using Git:
git clone https://github.com/hascode/jfx-ball-game.git
Screencast
This is our final game in action (screencast on YouTube).
Resources
Article Updates
-
2018-06-01: Embedded YouTube video removed (GDPR/DSGVO).