Getting Started with JavaFX Game Programming (for Java Programmers)

Getting Started with JavaFX Game Programming

[i]I’m a Java programmer in the process of learning JavaFX, and was surprised by the lack of current tutorials on this subject. Much of what I found online was for an obsolete version of JavaFX, not the JavaFX current in Java 8. The following is meant to help with some of the annoying speed bumps encountered, not to teach game programming. I’m assuming you already know what a game loop is, for example.

Sections that follow:
I. Basic setup and installation (with notes on an Eclipse bug)
II. Basic graphics
III. Basic game loop (AnimationTimer)
IV. Simple Keyboard Input
[/i]

Intro: Why JavaFX?

  • JavaFX is relatively fast and powerful. As an example, check out this particle generator written by Roland C. “The video shows 28000 particles running at 60 fps in full-hd screen resolution.”

  • JavaFX has advantages over AWT/Swing. Oracle has stopped development of AWT/Swing, and is committed to advancing JavaFX as the main GUI for Java, going forward. As a rewrite of Java’s GUI, the API is better designed and easier to learn and to code than Swing. While I’m told that not all of Swing’s features have been implemented, the vast majority of the functionality has, and JavaFX makes 3D programming and many additional special effects available.

  • JavaFX has some advantages over LWJGL-based graphics systems. The main advantage is that it is easy to integrate widgets (buttons, sliders, etc.) with the 3D graphics. Adding Swing widgets to a LWJGL-based system is difficult. There is also an easier learning curve because the coding, even for 3D, remains “Java-like”, whereas with LWJGL one has to get into learning Open-GL and deal with a C-influenced syntax. There are probably legitimate arguments that a system, such as LibGDX will be more powerful, but maybe not by much, as well as back and forth about the comparative advantages of the OpenGL paradigm versus the JavaFX 3D system. But this is something that I am unqualified to address.


[b]I. Basic Setup & Installation[/b]

JavaFX now comes standard with Java 8. If you use Java 8 or better, no additional or external libraries need to be imported. I’m not aware of a way to make use of the current JavaFX with earlier Java builds.

As of this writing, Eclipse (and this includes my current installation of NEON) requires an annoying extra step in order to access the JavaFX library. For some reason, the JRE System Library must be removed and reattached before JavaFX will be found. The following steps only have to be done once per project.

After creating a new Java Project:

  • Open the Java Build Path property (right click the Project, select Build Path > Configure Build Path…)

  • Open the Libraries tab, select the JRE System Library and “Remove” it. The system library may be named something like “JRE System Library[JavaSE-1.8]”.

  • Click “Add Library…”, in the next popup, select “Java System Library” (should be the first choice), and on the next screen “Workspace default JRE (jre[VERSION#])” (should be the last radio button), and “Finish”.

If you are making use of another JRE, I’m guessing you will probably know how to alter the above steps to load your preferred JRE System Library. I just use of the default.

At this point, Eclipse should have no problems finding JavaFX objects and methods.


[b]II. BASIC GRAPHICS[/b]

The entry class for a JavaFX project has to extend javafx.application.Application. Then, we “launch” the JavaFX GUI from the main method. The method launch executes the start method. This method provides the programmer with a Stage instance. All of the JavaFX functionality occurs within the context of this Stage instance.


    import javafx.application.Application;
    import javafx.scene.Group;
    import javafx.scene.Scene;
    import javafx.stage.Stage;

    public class BasicGraphicsDemo extends Application{

        public static void main(String[] args) {

            launch(args);
        }

        @Override
        public void start(Stage stage) throws Exception {

            Group root = new Group();
            Scene scene = new Scene(root, 600, 400);

            stage.setScene(scene);
            stage.show();
        }     
    }

A Stage functions as a top level frame. A Stage can display a Scene. A program may have more than one Scene.

A Scene holds a tree of the type Node. Most of the objects we use in JavaFX are subclasses of Node. By convention, the very first Node of the tree is named root, and is the object Group. A Group is an object that holds a collection of Nodes.

In the above code, the constructor for the Scene includes the root, and the dimension in pixels (width, height) of a display window. There are various other constructors one can use, as can be found in the API for Scene.

The following code adds a blue circle to the display.


              Circle circle = new Circle();
              circle.setCenterX(100);
              circle.setCenterY(200);
              circle.setRadius(40);
              circle.setFill(Color.BLUE);

              root.getChildren().add(circle);

The object Circle is one of several javafx.scene.shape options. Setting attributes is pretty straightforward. Experienced Java programmers should have no problem consulting the API for additional properties and variations.

Adding the Circle node to the root node is a little tricky, but once the pattern is learned, you will find that the same form is consistent over many situations. The collection of nodes held by the Group variable is returned by the method getChildren. Standard practice is to chain the add method onto this collection, as done in the above example. You can consult the API to learn about different forms of adding.

In the next code fragment, a caption is given to the top-level Stage, and the blue Circle is added to the display. When run, the image following this code fragment is displayed.


    @Override
    public void start(Stage stage) throws Exception {

        stage.setTitle("Basic JavaFX demo");

        Group root = new Group();
        Scene scene = new Scene(root, 600, 400);

        Circle circle = new Circle();
        circle.setCenterX(100);
        circle.setCenterY(200);
        circle.setRadius(40);
        circle.setFill(Color.BLUE);
        root.getChildren().add(circle);
    
        stage.setScene(scene);
        stage.show();
    }      


[b]III. Basic Game Loop[/b]

JavaFX offers several animation methods, with varying degrees of usefulness for game programming. I initially tried the “Many Balls” animation from “JavaFX for Dummies”. This otherwise excellent book (the explanation of Lambdas is the best I’ve found anywhere) makes use of a method of animation that employs a KeyFrame and a Timeline. The resulting animation is unsatisfactory, as there are noticeable jitters. So, I recommend skipping that and making use of the vastly superior animation method employed in the Particle Generator program cited at the top of this article.

The key Object is named AnimationTimer. The most common practice I’ve seen is to create an AnimationTimer as in the following:


    AnimationTimer animator = new AnimationTimer()
    {
        @Override
        public void handle(long arg0) 
        {
            // update
            // render
        }
    };

The code in the handle method is executed repeatedly when the start method of the AnimationTimer is called, and the repetitions are halted when the stop method is called. The argument arg0 that becomes available to the coder is the clock time at the start of the given repetition, measured in nanoseconds.

I was thrown, initially, by the fact that there is no way to set the frequency or timing of the repetitions. What the JavaFX designers have done is to implement a target repetition rate of 60 frames per second, where a frame corresponds to one iteration of the handle method. If your handle method takes longer than that to execute, then the frame rate slows down accordingly. A new iteration does not start until the current iteration has completed. If your handle method executes more quickly, the system waits rather than running at an fps that is faster than 60.

The following code animates the ball in the previous example, from side to side.


    import javafx.animation.AnimationTimer;
    import javafx.application.Application;
    import javafx.scene.Group;
    import javafx.scene.Scene;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Circle;
    import javafx.stage.Stage;

    public class BasicGraphicsDemo2 extends Application{

        final int WIDTH = 600;
        final int HEIGHT = 400;
        
        double ballRadius = 40;
        double ballX = 100;
        double ballY = 200;  
        double xSpeed = 4;
    
        public static void main(String[] args) {
    
            launch(args);
        }
        
        @Override
        public void start(Stage stage) throws Exception {

            stage.setTitle("Basic JavaFX demo");

            Group root = new Group();
            Scene scene = new Scene(root, WIDTH, HEIGHT);

            Circle circle = new Circle();
            circle.setCenterX(ballX);
            circle.setCenterY(ballY);
            circle.setRadius(ballRadius);
            circle.setFill(Color.BLUE);
            root.getChildren().add(circle);

            stage.setScene(scene);
            stage.show();

            AnimationTimer animator = new AnimationTimer(){

                @Override
                public void handle(long arg0) {

                    // UPDATE
                    ballX += xSpeed;

                    if (ballX + ballRadius >= WIDTH)
                    {
                        ballX = WIDTH - ballRadius;
                        xSpeed *= -1;
                    } 
                    else if (ballX - ballRadius < 0) 
                    {
                        ballX = 0 + ballRadius;
                        xSpeed *= -1;
                    }

                    // RENDER
                    circle.setCenterX(ballX);
                }      
            };

            animator.start();     
        }
    }


[b]IV. SIMPLE KEYBOARD INPUT[/b]

The JavaFX Event System has many similarities to Java. There are plenty of good and clear examples of usage in the Java Tutorials, JavaFX section. But the tutorial example provided for KeyEvent was overly complicated for my taste. The following is provided as a quick introduction to the basic form.

In this example, EventHandler is implemented by the main class (BasicGraphicsDemo3). Alert! When importing KeyEvent, make sure to import from the javafx library (javafx.scene.input.KeyEvent) and not java.awt.event.KeyEvent. I inadvertently did this and wasted a fair bit of time puzzling over the resulting error messages.

A class which implements EventHander must override the method handle(KeyEvent keyEvent).


    @Override
    public void handle(KeyEvent arg0) {

        if (arg0.getCode() == KeyCode.SPACE )
        {
            xSpeed *= -1;
        }
    }

In this example, the KeyEvent variable provided by the event is inspected. If the code matches KeyCode.SPACE, we reverse the direction of the ball. The JavaFX API documents all the available KeyCodes, and includes all the letters, and code names for the arrow keys: KeyCode.UP, KeyCode.DOWN, KeyCode.RIGHT, KeyCode.LEFT.

You can also inspect the API of KeyEvent for other information provided by this variable the event occurs. I don’t illustrate more common usages, such as separately tracking pressing and releasing, or handling simultaneous keys. These issues are similar enough to the Java equivalents.

How is the handle method triggered? This is done from a Node, using the method setOnKeyPressed. Other options are also available, e.g., setOnKeyReleased, setOnKeyTyped. There are two requirements for the Node: it must be part of the Scene that is currently being shown, and, it has to contain the focus.


    // need to attach KeyEvent caller to a Node of some sort.
    // How about an invisible Box? (arbitrary choice)
    final Box keyboardNode = new Box();
    keyboardNode.setFocusTraversable(true);
    keyboardNode.requestFocus();

    keyboardNode.setOnKeyPressed(this); // call to the EventHandler

    root.getChildren().add(keyboardNode);

The choice of Box was totally arbitrary on my part. There is probably a more appropriate subclass of Node to use. The JavaFX tutorial code example uses a StackPane. I suppose I could have also used the existing Circle node for the ball. But it seems more sensible that the keyboard handler be a dedicated Node rather than serve multiple functions.

Giving the node the focus required two steps. First, the node is set to be “FocusTraversable”. Once that is done, it is given the focus with the method requestFocus.

Complete program BasicGraphicsDemo3.java:


    import javafx.animation.AnimationTimer;
    import javafx.application.Application;
    import javafx.event.EventHandler;
    import javafx.scene.Group;
    import javafx.scene.Scene;
    import javafx.scene.input.KeyCode;
    import javafx.scene.input.KeyEvent;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Box;
    import javafx.scene.shape.Circle;
    import javafx.stage.Stage;
     
    public class BasicGraphicsDemo3 extends Application
                  implements EventHandler <KeyEvent>
    {
        final int WIDTH = 600;
        final int HEIGHT = 400;
       
        double ballRadius = 40;
        double ballX = 100;
        double ballY = 200;  
        double xSpeed = 4;
       
        public static void main(String[] args) {
              
            launch(args);
        }
       
        @Override
        public void start(Stage stage) throws Exception {
       
            stage.setTitle("Basic JavaFX demo");
              
            Group root = new Group();
            Scene scene = new Scene(root, WIDTH, HEIGHT);
              
            // Bouncing Ball
            Circle circle = new Circle();
            circle.setCenterX(ballX);
            circle.setCenterY(ballY);
            circle.setRadius(ballRadius);
            circle.setFill(Color.BLUE);
            root.getChildren().add(circle);
              
            // need to attach KeyEvent caller to a Node of some sort.
            // How about an invisible Box?
            final Box keyboardNode = new Box();
            keyboardNode.setFocusTraversable(true);
            keyboardNode.requestFocus();
            keyboardNode.setOnKeyPressed(this);
            
            root.getChildren().add(keyboardNode);
              
            stage.setScene(scene);
            stage.show();
            
            AnimationTimer animator = new AnimationTimer(){
 
                @Override
                public void handle(long arg0) {
       
                    // UPDATE
                    ballX += xSpeed;
                           
                    if (ballX + ballRadius >= WIDTH)
                    {
                        ballX = WIDTH - ballRadius;
                        xSpeed *= -1;
                    } else if (ballX - ballRadius < 0) {
                        ballX = 0 + ballRadius;
                        xSpeed *= -1;
                    }
       
                    // RENDER
                    circle.setCenterX(ballX);
                }
            };
 
            animator.start();
        }
 
        @Override
        public void handle(KeyEvent arg0) {
              
            if (arg0.getCode() == KeyCode.SPACE )
            {
                xSpeed *= -1;
            }
        }
    }