In chapters 6, 7, and 10 you’ve learned how to create simple GUI with JavaFX using such components as buttons, labels and text fields. In this chapter we’ll use JavaFX again, but this time you’ll be drawing and animating shapes.
JavaFX includes the package javafx.scene.shape, which includes such classes as Circle
, Rectangle
, Line
and more. All of these classes are inherited from the class Shape
.
Ping-Pong is the perfect game to illustrate the drawing and animating capabilities of JavaFX. In this game you’ll have another chance to work with event handlers, and will learn how to move (animate) shapes around the stage.
Let’s come up with some rules for our Ping-Pong game:
-
This game will have two players named Kid and Computer. The table should be painted green, and the left paddle should be painted blue - it’ll be controlled by the Computer. The right paddle should be yellow - it’ll be controlled by the Kid. Initially the red ball should be located in the center of the table.
-
A new game starts when a player presses the N-key on the keyboard, pressing Q will end the game, and the S-key will serve the ball. Only the kid can serve the ball.
-
The Kid’s paddle movements should be controlled by the Up and Down arrow keys. Pressing the key once should move the paddle vertically by the preset number of pixels up or down. The paddle shouldn’t cross the table borders.
-
The Kid can move the right paddle and press the S-key. This should serve the ball from the position where the right paddle is located. The ball can move at an angle, which means that its x and y coordinates should change while moving. If the ball crosses the top or bottom borders of the table, the Kid loses a point.
-
While the ball is moving to the left, the Computer should move the left paddle in the direction of the ball movement.
-
If the ball touches the left paddle, it should bounce and start moving to the right. When the Computer bounces the ball, it can move only horizontally to the right.
-
If the ball contacts the Kid’s paddle in the upper half of the table, the ball should be moving in the up-and-left direction. If the ball was located in the bottom part of the table, it should move in the down-and-left direction.The game lasts until one of the players reaches the score of 21. Since the computer can’t serve the ball, the Kid should score a point if the ball crosses the left border of the table. If the ball crosses the top or bottom table borders the point is given to the computer.The computer can also earn the point if it bounces the ball and it crosses the right border of the table. These rules are not the same as in a real Ping-Pong game, but they are good enough for our game.
-
The game score should be displayed in the bottom left corner of the table.
At first, it seems to be a very challenging task. In programming we often have to break the complicated problem into a set of smaller and simpler tasks. The ability to do so is called analytical thinking, and it helps not only in programming, but everywhere in your life. Do not get frustrated if you can’t achieve a big goal, split it in a set of the smaller ones and reach them one at time!
Let’s start breaking our complex task. Try to visualize a ping-pong table. Can you write a program that will draw a green rectangle? That’s our first goal to achieve. Then we’ll add the paddle. Then we’ll see how to draw the ball. Learning how to move the ball will be the next task. Writing the keyboard event handlers shouldn’t be too difficult.
Now let’s implement each step from the game strategy, one step at a time.
In this section our goal is to draw a 2D ping-pong table, which should look as a green rectangle.
Let’s start with creating a new JavaFX project in IntelliJ IDEA. Select JavaFX as a project type. Press Next, and enter PingPong as a project name on the next popup window. Press the button Finish and IDEA will generate a new project for you. Rename (right-click | Refactor | Rename) the file sample.fxml into pingpong.fxml, the Main
class into PingPong
, and Controller
into PingPongController
. Change the name of the package from sample to pong in the same fashion.
Now let’s draw the ping-pong table in Scene Builder. Right-click on the file pingpong.fxml and open it in Scene Builder. As you’ve seen in Chapter 7, the generated FXML uses the <GridBag>
as a default container. You’ll see an empty GridBag
in the Hierarchy pane. The left side of the Scene Builder panels will look as follows:
I’m not going to use any layouts in the PingPong game. The game will consist of a number of shapes that I’ll keep in the Group
container. We haven’t use it yet - the Group
is used just to keep several child components together. Grouping components in a container can come in handy if you need to assign one event handler to all child components or auto-resize all of them as a group.
In Scene Builder delete the GridBag
container and drag/drop the Group
from the Miscellaneous section down to the Hierarchy panel. Then drag/drop a Rectangle
from the Shapes panel onto the Group
. Resize it and pick a nice green color from the Fill property on the right side. You’ll be picking a color from a dropdown palette, and the corresponding color code will be automatically inserted as a fill
property of the Rectangle
. Your Scene Builder should look like this:
Open the file pingpong.fxml and you’ll see the tag <Rectangle>
inside the <Group>
generated by Scene Builder. Set the width of the Rectangle
to 400, and the height to 250. It should look like this:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.shape.*?>
<?import javafx.scene.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<Group xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<children>
<Rectangle arcHeight="5.0" arcWidth="5.0" fill="#31b287" height="250" stroke="BLACK" strokeType="INSIDE" width="400" />
</children>
</Group>
The archHeight
and arcWidth
were auto-generated in case you need to create rounded corners in the rectangle. The properties stroke
and strokeType
define the look and the placement of the rectangle’s border. Every rectangle has the x
and y
properties that define the coordinates of its upper left corner inside the container. By default, the values of x
and y
are zeros, so this rectangle will be drawn starting from the upper left corner of the Group
container.
If you’re interested in learning how different properties change the look of the rectangle, visit the online documentation for this class. Keep in mind that some of the properties are defined in the class Shape
, the ancestor of the Rectangle
.
Running the auto-generated PingPong
class will show the following window:
There are a couple of things that I don’t like here. First, the corners of the rectangle are rounded just a little bit. This is caused by the properties archHeight
and arcWidth
, which are vertical and horizontal diameters of corner arcs. I’ll delete them from the table rectangle in pingpong.fxml
.
Second, the window has a title Hello World set by the auto-generated code in IntelliJ IDEA. It’s easy to change in the class PingPong
, but I don’t even want to see a title bar!
This is also an easy fix in JavaFX. Setting the stage style to UNDECORATED
will remove the standard window title and borders. But if I’ll remove the title bar, I’ll lose the ability to close the window by clicking on the circle (or a little cross in Windows). A bit later we’ll write the code to close the window by pressing the Q-key on the keyboard. At this point the code of the PingPong
class looks like this:
package pong;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class PingPong extends Application {
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("pingpong.fxml"));
primaryStage.setScene(new Scene(root, 400, 250));
primaryStage.initStyle(StageStyle.UNDECORATED);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
This class was generated by IDEA, but I’ve removed the Hello World title, set the size of the scene to 400 by 250 pixels, and added the line
primaryStage.initStyle(StageStyle.UNDECORATED);
. Running the PingPong
class will display the following green rectangle:
Now let’s open pingpong.fxml in Scene Builder again and add the paddles and the ball on top of the table. For paddles, I’ll drag/drop two Rectangle
objects from the Shapes section onto the Group container. Our paddles will have a size of 10 by 50 pixels. The left paddle will be blue, and the right one will be yellow. Then I drag/drop, resize and color the Circle
to set its radius to 9 pixels, and then will paint it red. My Scene Builder will look like this:
Now back to IDEA. Since our shapes will need to communicate with the controller class, we need to assign an fx:id
to each of them. Let’s assign the fx:id="theGroup"
to the Group
container. Our green Rectangle
will get fx:id="table"
.
The computer will play with the left paddle, and I’ll give it fx:id="compPaddle"
. The Kid will play with the right paddle that will go by fx:id="kidPaddle"
. The ball will get fx:id="ball"
. Now The Group
container in my file pingpong.fxml will look like this:
<Group fx:id="theGroup" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<children>
<Rectangle fx:id="table" fill="#31b287" height="250" stroke="BLACK" strokeType="INSIDE" width="400" />
<Rectangle fx:id="compPaddle" arcHeight="5.0" arcWidth="5.0" fill="DODGERBLUE" height="50.0" layoutX="24.0" layoutY="98.0" stroke="BLACK" strokeType="INSIDE" width="10.0" />
<Rectangle fx:id="kidPaddle" arcHeight="5.0" arcWidth="5.0" fill="#f0ff1f" height="50.0" layoutX="365.0" layoutY="98.0" stroke="BLACK" strokeType="INSIDE" width="10.0" />
<Circle fx:id="ball" fill="#ff1f35" layoutX="191.0" layoutY="123.0" radius="9.0" stroke="BLACK" strokeType="INSIDE" />
</children>
</Group>
Running the PingPong
program will display the following ping-pong table:
The GUI drawing is complete, now we need to take care of the user interactions, which will be done in the class PingPongController
. In pingpong.fxml we need to assign this class as the fx:controller
to the Group
(see Chapter 8 for a refresher):
<Group fx:id="theGroup" fx:controller="pong.PingPongController" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
Dear Scene Builder and FXML, you’ve been very helpful. Now I’m happily going back to Java programming.
We need to add the event handler methods to the class PingPongController
to process keyboard events. Every key on the keyboard has a special code assigned, and our first goal is to figure out which key the player pressed.
For processing keyboard events JavaFX components have special event handler properties onKeyPressed
, onKeyReleased
, and onKeyTyped
. The first two properties allow you to program different actions for the downward and upward key motions, if needed.
The onKeyTyped
is used to assign a handler to the event when the key code is sent to the system output. This event is not generated for the keys that don’t produce character output. We’ll use onKeyReleased
- this is when the the user lifts his finger up.
Note
|
IDEA can help you find out which event handlers can be used with a particular component. Just click CTRL-Space inside this component’s FXML tag and start typing with the letters on and you’ll see all applicable event handlers.
|
When the user releases a key, the onKeyReleased
method handler receives the KeyEvent
object as an argument. The method getCode
from the class KeyEvent
returns the KeyCode
object that represents the key pressed. For example, if you press the button Q, the getCode
will return Q
. If you press the arrow up, the getCode
will return UP
.
But the same key can result in displaying more than one character (e.g. Q or q) The method getText
of KeyEvent
returns a String
that represents the character typed by the user.
To enable our GUI to react on keyboard events right after the program starts, we need to set the focus on the GUI. This was not required when we clicked on the GUI components with the mouse, but now we won’t even touch the screen.
To set the focus to the Group
container we’ll need to do two things:
-
Enable the
Group
to receive the focus by useing the attributefocusTraversable="true"
in pingpong.fxml. -
Right after the stage is displayed in the
PingPong
class, we’ll call the methodrequestFocus
on theGroup
container. The methodstart
inPingPong
will look like this (I’ve added just the last line to the code generated by IDEA):public void start(Stage primaryStage) throws Exception{ Parent root = FXMLLoader.load(getClass().getResource("pingpong.fxml")); primaryStage.setScene(new Scene(root, 400, 250)); primaryStage.initStyle(StageStyle.UNDECORATED); primaryStage.show(); root.requestFocus(); }
In the code that comes with this chapter the final version of the controller is called PingPongController
. I’ve also included multiple versions of the controller that gradually implement the steps listed in the game strategy. Each "intermediate" controller class name starts with PingPongController
followed by a different suffix with a version number (e.g. PingPongController_v1
, PingPongController_v2
etc.) The starting comment in each class briefly describes what was added in this version of the controller. To see any of these controllers in action, just specify its name as fx:controller
in the file pingpong.fxml
and run the PingPong
program.
Note
|
In InelliJ IDEA you can easily compare two files to see the difference. Press CTRL or CMD button and click on the names of two files you’d like to compare (e.g. PingPongController_v1 and PingPongController_v2 ). Then select the menu View | Compare Two Files, and you’ll see the source code of these files next to each other with highlighted differences.
|
Let’s add to the PingPongControler
a method handler for the key-released events. The first very simple version of the PingPongControler
is shown next. The goal is to see that the controller receives the keyboard events and can recognize the keys pressed by the player.
package pong;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class PingPongController {
public void keyReleasedHandler(KeyEvent event){
KeyCode keyCode = event.getCode();
System.out.println("You pressed " + keyCode);
}
}
The event handler method just extracts the key code from the KeyEvent
object provided by the Java runtime and prints it.
For example, after running the PingPong
class and pressing the up and down arrows, n, q, and s keys, the console output should look like this:
You pressed UP
You pressed DOWN
You pressed N
You pressed Q
You pressed S
The KeyCode
in PingPongController
is not a class or an interface, but a special Java construct called enum
described next.
Our controller class declares a variable of type KeyCode
, which is neither a class nor an interface. It’s a special Java data type enum
used for declaring a bunch of pre-defined constants that never change. For example, you can declare a new enum
type day-of-the-week:
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY
}
The following class GreetTheDay
illustrates the use of the enum Day
:
public class GreetTheDay {
public static void main(String[] args) {
greet(Day.SATURDAY);
}
static void greet(Day day){
switch (day) {
case MONDAY:
System.out.println("The week begins");
break;
case SATURDAY:
case SUNDAY:
System.out.println("Hello Weekend!");
break;
default:
System.out.println("Hello Midweek");
break;
}
}
}
The method greet
expects to receive one of the Day
values as an argument. Our main
method wants to greet Saturday, and if you run the program GreetTheDay
it’ll print Hello Weekend!.
If you’ll open the online documentation for KeyCode
you’ll find there the declarations of all possible keyboard keys.
Now we’ll add a switch
statement to the controller to invoke the method that corresponds to the pressed key. Let’s not worry about implementing the application logic just yet. We want to make sure that the program invokes the correct method for each key.
package pong;
import javafx.application.Platform;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class PingPongController {
public void keyReleasedHandler(KeyEvent event){
KeyCode keyCode = event.getCode();
switch (keyCode){
case UP:
process_key_Up();
break;
case DOWN:
process_key_Down();
break;
case N:
process_key_N();
break;
case Q:
Platform.exit(); // Terminate the app
break;
case S:
process_key_S();
break;
}
}
private void process_key_Up() {
System.out.println("Processing the Up key");
}
private void process_key_Down() {
System.out.println("Processing the Down key");
}
private void process_key_N() {
System.out.println("Processing the N key");
}
private void process_key_S() {
System.out.println("Processing the S key");
}
}
The switch
statement checks the value of enum KeyCode
and calls the corresponding method which just prints a hard-coded message. We’ll implement them shortly, but the Q-key in the above PingPongController
is fully functional. When the user presses the Q-key, the program invokes the method exit
on the class Platform
, which terminates the program.
Now let’s teach the keys Up and Down to move the Kid’s paddle vertically. Pressing the Up-arrow should move the Kid’s paddle several pixels up according to the predefined moving increment. Pressing the Down-arrow should move the paddle down. We’ll declare an movement increment as a final
variable in PingPongController
:
final int PADDLE_MOVEMENT_INCREMENT = 6;
Pressing the key once will change the vertical position of the paddle by 7 pixels. Seven is not a magical number, and you can use any other integer here.
The new version of the controller will use the @FXML
annotations to inject the references to the GUI components. To update the position of the kid’s paddle on the GUI we’ll use data binding explained in Chapter 8. We’ll also add the method initialize
that is invoked by the Java runtime once when the controller object is created. Finally, we’ll write the code in the methods process_key_Down
and process_key_Up
to move the kid’s paddle vertically.
In JavaFX the x and y coordinates of the top left corner of the stage have zero values. x-coordinate increases from left to right, and the y-coordinate increases from top to bottom. The following image shows how x and y coordinates change if a ping-pong table has the width of 400 pixels and the height of 250:
In our game the paddles can move only up or down, so depending on the key pressed we’ll be changing the value of the property layoutY
of the right paddle, which will move it on stage accordingly. Here’s how the PingPongController
will look now:
package pong;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.fxml.FXML;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
public class PingPongController {
final int PADDLE_MOVEMENT_INCREMENT = 6;
DoubleProperty currentKidPaddleY =
new SimpleDoubleProperty(); // (1)
double allowedPaddleTopY; // (2)
double allowedPaddleBottomY;
@FXML Rectangle table; // (3)
@FXML Rectangle compPaddle;
@FXML Rectangle kidPaddle;
@FXML Circle ball;
public void initialize() {
currentKidPaddleY.set(kidPaddle.getLayoutY()); // (4)
kidPaddle.layoutYProperty().bind(currentKidPaddleY);
allowedPaddleTopY = PADDLE_MOVEMENT_INCREMENT; // (5)
allowedPaddleBottomY = table.getHeight() - kidPaddle.getHeight() - PADDLE_MOVEMENT_INCREMENT;
}
public void keyReleasedHandler(KeyEvent event){
KeyCode keyCode = event.getCode();
switch (keyCode){
case UP:
process_key_Up();
break;
case DOWN:
process_key_Down();
break;
case N:
process_key_N();
break;
case Q:
Platform.exit(); // Terminate the application
break;
case S:
process_key_S();
break;
}
}
private void process_key_Up() { // (6)
if (currentKidPaddleY.get() > allowedPaddleTopY) {
currentKidPaddleY.set(currentKidPaddleY.get() - PADDLE_MOVEMENT_INCREMENT);
}
}
private void process_key_Down() { // (7)
if (currentKidPaddleY.get()< allowedPaddleBottomY) {
currentKidPaddleY.set(currentKidPaddleY.get() + PADDLE_MOVEMENT_INCREMENT);
}
}
private void process_key_N() {
System.out.println("Processing the N key");
}
private void process_key_S() {
System.out.println("Processing the S key");
}
}
-
Declaring the property
currentKidPaddleY
that will be bound to the propertylayoutY
of the kid’s paddle. -
The Kid will be moving the paddle up and down, but we don’t want to allow the paddle to leave the table boundaries. The variable
allowedPaddleTopY
will store the maximum allowed y-coordinate for the top of the paddle, and theallowedPaddleBottomY
will have the maximum allowed y-coordinate for the bottom of the paddle. -
Using the
@FXML
tag we inject the references to the GUI components defined in pingpong.fxml into the controller’s variables. -
The method
initialize
in the controller is called only once and is the right place to initialize important variables. First, we initialize the propertycurrentKidPaddleY
, with the value of thelayoutY
property of the right paddle (the kidPaddle component has an attributelayoutY="98.0"
in the file pingpong.fxml). Then we bindcurrentKidPaddleY
to thelayoutY
property of the GUI component kidPaddle. -
Here we set the limits for the paddle movements. We set the variable
allowedPaddleTopY=PADDLE_MOVEMENT_INCREMENT
to make sure that if the Kid keeps pressing the Up arrow, the paddle will never cross the top border of the table. The bottom restrictionallowedPaddleBottomY
is calculated by subtracting the height of the paddle andPADDLE_MOVEMENT_INCREMENT
from the table height. -
The method
process_key_Up
gets the current y-coordinate of the top border of the paddle, and if it’s far enough from the table top, the code lowers the value of the propertycurrentKidPaddleY
byPADDLE_MOVEMENT_INCREMENT
. BecausecurrentKidPaddleY
is bound to thelayoutY
property of the GUI componentkidPaddle
, the latter moves up on stage. The movement stops if thecurrentKidPaddleY
value is higher thanallowedPaddleTopY
. Remember, the y-coordinate increases from top down, so the higher y-coordinates means that it’s physically lower on stage. -
The method
process_key_Down
works similarly toprocess_key_Up
but ensures that the paddle won’t cross the bottom border of the table.
Now our controller knows how to move the Kid’s paddle. The next challenge is to learn how to move the ball.
Let’s start implementing step 4 of the game strategy by calculating the starting position and painting the ball depending on the location of the right paddle. When the user preses the S-key, we need to serve the ball from the position where the right paddle is currently located. Initially it’s located in the middle of the table, but the user may move it up or down before serving the ball.
The ball is represented by the shape Circle
. From school math you should remember that a circle is represented by the coordinates of the center and the radius. In JavaFX the corresponding properties of the class Circle
are called centerX
, centerY
, and radius
. When the Circle
is placed in a layout, its center gets the corresponding properties layoutX
and layoutY
. By changing the coordinates of the center we can move the ball around the stage. Our ball is defined in the file pingpong.fxml like this:
<Circle fx:id="ball" fill="#ff1f35" layoutX="191.0" layoutY="123.0" radius="9.0" stroke="BLACK" strokeType="INSIDE" />
But why doesn’t the above tag <Circle>
include centerX
and centerY
? Actually we can and will replace the attributes layoutX
and layoutY
with centerX
and centerY
because we use the Group
container that’s not a part of any other layout (e.g. BorderPane
or GridPane
). JavaFX allows you to build complex scenes that can dynamically change sizes and reposition its child components.Hence the x and y coordinates of a component relative to a layout may not be the same as coordinates in the scene. For example, the actual x-coordinate of a component may be calculated by adding the x-coordinate of a container within a scene and the x-coordinate of the component within a container.
Let’s modify the attributes of the tag <Circle>
so it’ll look like this:
<Circle fx:id="ball" fill="#ff1f35" centerX="191.0" centerY="123.0" radius="9.0" stroke="BLACK" strokeType="INSIDE" />
Since the ball will be moving, we’ll keep track of its center in the new properties ballCenterX
and ballCenterY
:
DoubleProperty ballCenterX = new SimpleDoubleProperty();
DoubleProperty ballCenterY = new SimpleDoubleProperty();
In the method initialize
we’ll set the initial values of these properties to the center coordinates of the ball. We’ll also bind the above properties to the center of the Circle
, so changing ballCenterX
and ballCenterY
will automatically change the location of the ball on the scene:
ballCenterX.set(ball.getCenterX());
ballCenterY.set(ball.getCenterY());
ball.centerXProperty().bind(ballCenterX);
ball.centerYProperty().bind(ballCenterY);
Let’s place the ball by the current position of the kid’s paddle. In the method process_key_S
we’ll adjust the centerY
coordinate of the ball. Our controller has the variable currentKidPaddleY
that remembers the current y-coordinate of the top of the kid’s paddle. So if we’ll add to currentKidPaddleY
the half of the the paddle’s height, we’ll get the the y-coordinate of the paddle’s center. The centerX
coordinate will be the same as the layoutX
of the Kid’s paddle.The new version of the method process_key_S
will look like this:
private void process_key_S() {
ballCenterY.set(currentKidPaddleY.doubleValue() + kidPaddle.getHeight()/2);
ballCenterX.set(kidPaddle.getLayoutX());
}
I ran the PingPong
application, moved the paddle up by clicking the arrow key several times, and then pressed the S-key. The ball obediently moved to the current position of the right paddle:
The ball is ready to start moving now. To make the movement smooth, we’ll use the class javafx.animation.Timeline
that allows us to change the values of the GUI component’s properties over a time interval. Similarly to a movie, the animation is a set of frames that are displayed over a specific period of time. Each frame is a snapshot of a GUI component at a certain state. For the ball movement we’ll declare the variable timeline
of the type TimeLine
, which we’ll use to display a set of snapshots of a ball at different positions along its trajectory.
Each frame is represented by a class KeyFrame
. Each snapshot is represented by the class KeyValue
. Let’s write a method moveTheBall
that will move the ball horizontally all the way to the left until the centerX
will become equal to zero. If we change only the centerX
property of the Circle
, it’ll be moving horizontally.
Timeline timeline;
private void moveTheBall(){
timeline = new Timeline(); // (1)
timeline.setCycleCount(1);
KeyValue keyValue = new KeyValue(ballCenterX, 0); // (2)
KeyFrame keyFrame = new KeyFrame(new Duration(1000), keyValue); // (3)
timeline.getKeyFrames().add(keyFrame); // (4)
timeline.play(); // (5)
}
-
First we create an instance of the
Timeline
object and invoke thesetCycleCount
requesting that the animation will be done only once. In this example we could have declared the variabletimeline
inside the method, but keeping this variable on the class level will allow me to programatically stop the animation that I’ll demonstrate in the next version of the methodmoveTheBall
. -
Then we’ll create the
KeyValue
object to specify which changing value to display in frames. In this case we want the animation to change the x-coordinate of the ball center from its current valueballCenterX
to zero. -
We want the
KeyFrame
to reach the target (change thecenterX
specified inKeyValue
from the current value to zero) over a period of 1000 milliseconds. The smaller the number, the faster the ball will move. The number of frames will be automatically calculated based on the duration and the target position of the ball. -
Adding the our
KeyFrame
object to thetimeline
completes the preparations. -
The method
play
will play thetimeline
.
Now if you’ll invoke the method moveTheBall
from process_key_S
the ball will move to the left and stop there. Here’s what I’ve got after starting the game and pressing the S-key:
Our ball is not smart enough to notice that there was a left paddle on its way and went right through it. We’ll take care of the GUI component collisions a bit later.
Depending on the provided duration, the Timeline
object will calculate how many snapshots (key frames) to create while the KeyValue
is changing to reach the target. The Timeline
class has several overloaded constructors, and one of them allows you to specify the frames per second for the animation.
The KeyFrame
class also has several overloaded constructors, and one of them allows you to specify the duration, the event handler for the ActionEvent
, and optional key value(s). The handler for the ActionEvent
can be implemented as a lambda expression.
In the following version of the method moveTheBall
we’ll write the code to advance the ball at the specified increments. We’ll also use different constructors of TimeLine
and KeyFrame
:
final int BALL_MOVEMENT_INCREMENT = 5;
private void moveTheBall(){
KeyFrame keyFrame = new KeyFrame(new Duration(10), // (1)
event -> {
if (ballCenterX.get() > BALL_MOVEMENT_INCREMENT) { // (2)
ballCenterX.set(ballCenterX.get() - BALL_MOVEMENT_INCREMENT); // (3)
} else {
timeline.stop(); // (4)
}
}
);
timeline = new Timeline(keyFrame); // (5)
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
}
-
The first argument of the constructor is the
Duration
object, but this time it has different meaning than in the previous version of themoveTheBall
. The value of 10 means to run the code from the event handler (the lambda expression) every 10 milliseconds. -
The lambda expression starts with checking if the current value of the x-coordinate of the ball center is larger than the value in
BALL_MOVEMENT_INCREMENT
to ensure that the ball will not cross the left border and will stay at the 0 coordinates. To make our game more realistic, we’ll let the ball go off the table in the next version of themoveTheBall
. -
Decrement the value of the
ballCenterX
byBALL_MOVEMENT_INCREMENT
. Because of binding, this will move the ball on the GUI. -
Stop the animation if the ball were to fall off the table on the next move.
-
Create the
Timeline
using ourKeyFrame
and play it. In this case I requested to play the animation indefinitely because I don’t want to calculate how many moves would it take to reach the target position. I’ll stop the animation manually anyway as explained in step 4.
The game that serves the ball horizontally is pretty boring, so let’s change not only the x-, but y-coordinate as well while the ball is moving. To add some fun, let’s change the y-coordinate in a random manner, so that each ball serving sends the ball in a different direction. We’ll create a ball serving machine.
If the user moved the paddle to the upper half of the table, the ball should be moving either horizontally or down. If the ball is served from the lower half - the ball can move either horizontally or upward. To know the y-coordinate of the table center we’ll declare the double
variable centerTableY
and set its value in the method initialize
like this:
centerTableY = table.getHeight()/2;
As the ball moves, the modified value of the y-coordinate of the ball center will be assigned to the property ballCenterY
. The following version of the method moveTheBall
implements the random ball servings.
private void moveTheBall(){
Random randomYGenerator = new Random();
double randomYincrement = randomYGenerator.nextInt(BALL_MOVEMENT_INCREMENT); // (1)
final boolean isServingFromTop = (ballCenterY.get() <= centerTableY)?true:false; // (2)
KeyFrame keyFrame = new KeyFrame(new Duration(10), event -> {
if (ballCenterX.get() >= -20) { // (3)
ballCenterX.set(ballCenterX.get() - BALL_MOVEMENT_INCREMENT);
if (isServingFromTop) { // (4)
ballCenterY.set(ballCenterY.get() + randomYincrement);
} else {
ballCenterY.set(ballCenterY.get() + randomYincrement);
}
} else {
timeline.stop();
}
});
timeline = new Timeline(keyFrame);
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
}
-
We create an instance of the class
java.util.Random
. This class has methods to generate random numbers of different types. Invoking the methodnextInt
generates a random integer number in the range between zero and the value of the method argument. I decided to use the value ofBALL_MOVEMENT_INCREMENT
as a top limit. This random number will be used as an increment for the y-coordinate of the moving ball. -
Using the conditional operator (as explained in Chapter 4) we set the variable
isServingFromTop
to true if the right paddle is located on the upper half of the table, or to false if the paddle is in the lower half. -
In the previous version of the method
moveTheBall
, we were keeping the ball on the table when it was reaching the left edge of the table. Now we will let the ball fall off the table. The-20
is just an arbitrary number - we stopped the ball movement after the ball moved 20 pixels past the left table edge. In the final version of the game we’ll enable the movement for the left paddle, and sometimes it’ll bounce the ball back. -
If
isServingFromTop
is true, we’re increasing the y-coordinate of the ball’s center byrandomYincrement
, otherwise we’re decreasing it by the same amount. Now the ball will be served in an unpredictable manner and will pretty often fall off the table crossing the top or bottom edge of the table. I took the following screen shot when the moving ball was about to cross the bottom edge of the table.
When the ball is served, the Computer needs to move its paddle in the right direction to bounce the ball. This is a pretty easy task since the computer knows that if the Kid’s paddle served from the top, the ball would move down, and if the ball was served from the bottom it’ll move up. So the moment the ball is served, the computer’s paddle should also start moving.
First of all, we’ll declare the property currentComputerPaddleY
to keep track of the y-coordinate of the Computer’s paddle. We’ll also need to store the initial y-coordinate of the Computer’s paddle, because on each ball serving this paddle should be in the middle of the left side of the table:
DoubleProperty currentComputerPaddleY = new SimpleDoubleProperty();
double initialComputerPaddleY;
In the method initialize
we’ll bind currentComputerPaddleY
to the layoutY
property of the Rectangle
that represents the Computer’s paddle:
initialComputerPaddleY = compPaddle.getLayoutY();
currentComputerPaddleY.set(initialComputerPaddleY);
compPaddle.layoutYProperty().bind(currentComputerPaddleY);
The new version of the method moveTheBall
will start the movement of the Computer’s paddle in the right direction as soon as the ball is served.
private void moveTheBall(){
Random randomYGenerator = new Random();
double randomYincrement = randomYGenerator.nextInt(BALL_MOVEMENT_INCREMENT);
final boolean isServingFromTop = (ballCenterY.get() <= centerTableY)?true:false;
KeyFrame keyFrame = new KeyFrame(new Duration(10), event -> {
if (ballCenterX.get() >= -20) {
ballCenterX.set(ballCenterX.get() - BALL_MOVEMENT_INCREMENT);
if (isServingFromTop) {
ballCenterY.set(ballCenterY.get() + randomYincrement);
currentComputerPaddleY.set( currentComputerPaddleY.get() + 1); // (1)
} else {
ballCenterY.set(ballCenterY.get() - randomYincrement);
currentComputerPaddleY.set(currentComputerPaddleY.get() - 1); // (2)
}
} else {
timeline.stop();
currentComputerPaddleY.set(initialComputerPaddleY); // (3)
}
});
timeline = new Timeline(keyFrame);
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
}
-
If the ball was served from the top, move the Computer’s paddle one pixel down on each frame.
-
If the ball was served from the lower half of the table, move the Computer’s paddle one pixel up on each frame.
-
When the ball stops moving, return the Computer’s paddle to the initial position in the middle of the left side of the table.
There is no guarantee that the Computer’s paddle will advance to a position to bounce the ball. Our Ping-Pong game doesn’t implement the algorithm that adjusts the movement of the Computer’s paddle based on the trajectory of the ball’s movement. But if the ball accidentally contacts the paddle, we need to bounce the ball and send it from left to right.
Every JavaFX GUI component is a subclass of a Node
, which has a special property boundsInParent
. It’s an invisible rectangle that encapsulates the component when it’s placed inside a layout. So our ball and paddles are sitting inside of these invisible rectangles too. When the ball or paddles are being moved, the coordinates of their boundsInParent
properties are being recalculated. If these invisible rectangles of the ball and the paddle intersect, we can say that there was a contact. The method checkForBallPaddleContact
returns true if there was a contact, and false if not.
private boolean checkForBallPaddleContact(){
if (ball.intersects(compPaddle.getBoundsInParent())){
return true;
} else {
return false;
}
}
We should call this method from each frame in the timeline that we started in the method moveTheBall
. After this check for contact to the method moveTheBall
it’ll look like this:
private void moveTheBall(){
Random randomYGenerator = new Random();
double randomYincrement = randomYGenerator.nextInt(BALL_MOVEMENT_INCREMENT);
final boolean isServingFromTop = (ballCenterY.get() <= centerTableY)?true:false;
KeyFrame keyFrame = new KeyFrame(new Duration(10), event -> {
if (ballCenterX.get() >= -20) {
ballCenterX.set(ballCenterX.get() - BALL_MOVEMENT_INCREMENT);
if (isServingFromTop) {
ballCenterY.set(ballCenterY.get() + randomYincrement);
currentComputerPaddleY.set( currentComputerPaddleY.get() + 1);
} else {
ballCenterY.set(ballCenterY.get() - randomYincrement);
currentComputerPaddleY.set(currentComputerPaddleY.get() - 1);
}
if (checkForBallPaddleContact()){
timeline.stop(); // (1)
currentComputerPaddleY.set(initialComputerPaddleY);
bounceTheBall(); // (2)
};
} else {
timeline.stop();
currentComputerPaddleY.set(initialComputerPaddleY);
}
});
timeline = new Timeline(keyFrame);
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
}
-
If there was a ball/paddle contact, we need to stop playing the current
timeline
, set the Computer’s paddle to its initial position. -
Then we’ll need to call the method
bounceTheBall
, which we’ll write next.
According to step 6 in our game strategy, the computer can only serve the ball horizontally, so writing the bounceTheBall
method seems like a pretty simple thing to do. We already wrote a version of the moveTheBall
method that sends the ball horizontally from right to left, so reversing the ball moving direction should be easy.
But what if the Kid will move his or her paddle to hit the ball back? OK, then we need to be checking if the ball contacts the Kid’s paddle as well. Can we reuse the method checkForBallPaddleContact
for this? Not in its current form, because it’s written specifically for the computer paddle. We can certainly write a similar method for the Kid’s paddle and have two almost identical methods, but it’s better to re-write checkForBallPaddleContact
to work for both paddles.
In programmer’s jargon re-writing an existing and working code is called refactoring. So let’s refactor the method checkForBallPaddleContact
by providing the paddle as an argument. Here’s the refactored version that can be used for both paddles:
private boolean checkForBallPaddleContact(Rectangle paddle){
if (ball.intersects(paddle.getBoundsInParent())){
return true;
} else {
return false;
}
}
Accordingly, the method moveTheBall
would check for the contact with Computer’s paddle as follows:
checkForBallPaddleContact(compPaddle);
To check for the ball contact with the Kid’s paddle you’d write this line:
checkForBallPaddleContact(kidPaddle);
Now let’s write the method bounceTheBall
, which should be very similar to moveTheBall
. The ball should move from left to write, if you’ve been decreasing the x-coordinate in moveTheBall
, you’ll need to decrease it now. If you’ve been stopping the game where the coordinate of the ball was less than -20, now it has to be more than the table width plus 20. I could refactor the method moveTheBall
to introduce these values as methods arguments, but let’s keep it as a small project for you. As long as you understand how the code works, you should be able to do it on your own. Here’s the code of the method bounceTheBall
:
private void bounceTheBall() {
double theBallOffTheTableX = table.getWidth() + 20; // (1)
KeyFrame keyFrame = new KeyFrame(new Duration(10), event -> {
System.out.println(ballCenterX);
if (ballCenterX.get() < theBallOffTheTableX) {
ballCenterX.set(ballCenterX.get() + BALL_MOVEMENT_INCREMENT); // (2)
if (checkForBallPaddleContact(kidPaddle)){ // (3)
timeline.stop();
moveTheBall();
};
} else {
timeline.stop();
}
});
timeline = new Timeline(keyFrame);
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
}
-
The ball should stop its movement when its x-coordinate is 20 pixels to the right of the table.
-
Advance the ball to the right until its x-coordinate reaches the value of
theBallOffTheTableX
. -
While the Computer bounces the ball, the Kid can press the up and down arrow keys to stop the ball. If the ball contacts the Kid’s paddle, we call the
moveTheBall
method to start the random movement to the left again.
The basic functionality of the game is implemented aside from displaying the game score and starting the new game when one of the players gets 21 points. I’ll leave this part for you to implement on your own.
I’ve been showing and explaining various code fragments of the PingPongController
as we’ve been implementing the game strategy one step at a time. Now I’ll just show how the code of the PingPongConroller
class looks like without any additional explanations. You should be able to read and understand the code.
package pong;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.fxml.FXML;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import java.util.Random;
// This code implements 6 steps of the Game strategy
public class PingPongController {
final int PADDLE_MOVEMENT_INCREMENT = 7;
final int BALL_MOVEMENT_INCREMENT = 3;
double centerTableY;
DoubleProperty currentKidPaddleY = new SimpleDoubleProperty();
DoubleProperty currentComputerPaddleY = new SimpleDoubleProperty();
double initialComputerPaddleY;
DoubleProperty ballCenterX = new SimpleDoubleProperty();
DoubleProperty ballCenterY = new SimpleDoubleProperty();
double allowedPaddleTopY;
double allowedPaddleBottomY;
Timeline timeline;
@FXML
Rectangle table;
@FXML Rectangle compPaddle;
@FXML Rectangle kidPaddle;
@FXML Circle ball;
public void initialize()
{
currentKidPaddleY.set(kidPaddle.getLayoutY());
kidPaddle.layoutYProperty().bind(currentKidPaddleY);
ballCenterX.set(ball.getCenterX());
ballCenterY.set(ball.getCenterY());
ball.centerXProperty().bind(ballCenterX);
ball.centerYProperty().bind(ballCenterY);
initialComputerPaddleY = compPaddle.getLayoutY();
currentComputerPaddleY.set(initialComputerPaddleY);
compPaddle.layoutYProperty().bind(currentComputerPaddleY);
allowedPaddleTopY = PADDLE_MOVEMENT_INCREMENT;
allowedPaddleBottomY = table.getHeight() - kidPaddle.getHeight() - PADDLE_MOVEMENT_INCREMENT;
centerTableY = table.getHeight()/2;
}
public void keyReleasedHandler(KeyEvent event){
KeyCode keyCode = event.getCode();
switch (keyCode){
case UP:
process_key_Up();
break;
case DOWN:
process_key_Down();
break;
case N:
process_key_N();
break;
case Q:
Platform.exit(); // Terminate the application
break;
case S:
process_key_S();
break;
}
}
private void process_key_Up() {
if (currentKidPaddleY.get() > allowedPaddleTopY) {
currentKidPaddleY.set(currentKidPaddleY.get() - PADDLE_MOVEMENT_INCREMENT);
}
}
private void process_key_Down() {
if (currentKidPaddleY.get()< allowedPaddleBottomY) {
currentKidPaddleY.set(currentKidPaddleY.get() + PADDLE_MOVEMENT_INCREMENT);
}
}
private void process_key_N() {
System.out.println("Processing the N key");
}
private void process_key_S() {
ballCenterY.set(currentKidPaddleY.doubleValue() + kidPaddle.getHeight()/2);
ballCenterX.set(kidPaddle.getLayoutX());
moveTheBall();
}
private void moveTheBall(){
Random randomYGenerator = new Random();
double randomYincrement = randomYGenerator.nextInt(BALL_MOVEMENT_INCREMENT);
final boolean isServingFromTop = (ballCenterY.get() <= centerTableY)?true:false;
KeyFrame keyFrame = new KeyFrame(new Duration(10), event -> {
if (ballCenterX.get() >= -20) {
ballCenterX.set(ballCenterX.get() - BALL_MOVEMENT_INCREMENT);
if (isServingFromTop) {
ballCenterY.set(ballCenterY.get() + randomYincrement);
currentComputerPaddleY.set( currentComputerPaddleY.get() + 1);
} else {
ballCenterY.set(ballCenterY.get() - randomYincrement);
currentComputerPaddleY.set(currentComputerPaddleY.get() - 1);
}
if (checkForBallPaddleContact(compPaddle)){
timeline.stop();
currentComputerPaddleY.set(initialComputerPaddleY);
bounceTheBall();
};
} else {
timeline.stop();
currentComputerPaddleY.set(initialComputerPaddleY);
}
});
timeline = new Timeline(keyFrame);
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
}
private boolean checkForBallPaddleContact(Rectangle paddle){
if (ball.intersects(paddle.getBoundsInParent())){
return true;
} else {
return false;
}
}
private void bounceTheBall() {
double theBallOffTheTableX = table.getWidth() + 20;
KeyFrame keyFrame = new KeyFrame(new Duration(10), event -> {
if (ballCenterX.get() < theBallOffTheTableX) {
ballCenterX.set(ballCenterX.get() + BALL_MOVEMENT_INCREMENT);
if (checkForBallPaddleContact(kidPaddle)){
timeline.stop();
moveTheBall();
};
} else {
timeline.stop();
}
});
timeline = new Timeline(keyFrame);
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
}
}
The goal of this project is to keep track and display the game score. I’ll just show you how to print the score on the system console, but you’ll need to research how to display the text right on the ping-pong table.
If the Kid would serve the ball horizontally, he or she would score one point when the ball goes off the table and its x-coordinate is less than zero. But since the ball is served in a random direction, it can go off the table by crossing top or bottom table’s borders. In this case the Kid loses the point. When the method moveTheBall stops playing the timeline, the ball’s x-coordinate is definitely less than zero so you need to check the y-coordinate of the ball.
If the y-coordinate of the ball has a value between zero and the table height, you can assume that the ball crossed the left border of the table and the Kid scored one point. Otherwise the ball crossed either top or bottom table’s borders and the Kid should loose the point.
The Computer scores one point if it bounces the ball and it crosses the right border of the table.
You need to declare two class variable to keep track of the Computer’s and Kid’s scores:
int computerScore;
int kidScore;
You’ll also need to write a method updateScore
and invoke it every time the timeline stops playing (in both methods: moveTheBall
and bounceTheBall
). The method updateScore
can look like this:
private void updateScore(){
if (ballCenterX.get() > table.getWidth()){
// Computer bounced the ball and the Kid didn't hit it back
computerScore ++;
} else if (ballCenterY.get() > 0 && ballCenterY.get() <= table.getHeight()){
// The Kid served the ball and Computer didn't hit it back
kidScore++;
} else{
// The Kid served the ball off the table
computerScore++;
}
System.out.println("Computer: " + computerScore + ", Kid: " + kidScore);
}
The code that comes with the book has this version of the method updateScore
implemented. Your goal is to display the score in a nice font on the bottom left corner of the table. you can learn how to work with the Text
and Font
classes by studying Oracle’s tutorial "Working with Text in JavaFX Applications".
Don’t forget to implement the Start New Game functionality. You’ll need to write a method newGame
, where you should reset the scores and place the paddles and the ball in the starting positions.