Look out, Duke! Part 3: Compile a Java game to create a native mobile application

June 3, 2022 | 8 minute read
Text Size 100%:

Leverage GraalVM to recompile an arcade game to run just about anywhere.

Download a PDF of this article

Pew! Pew! Pew! You may recall previous articles that showed how to create a game with the JavaFX Game Development Framework (FXGL) library in “Look out, Duke! Part 1: Build a Java game with JavaFX and FXGL” and later made it run on a Raspberry Pi with an arcade joystick and the Pi4J library in “Look out, Duke! Part 2: Control a Java game with a Raspberry Pi.”

I’ll conclude this series by showing how to create native versions of the game, leveraging Java 17 and GraalVM 22.

Where does GraalVM come into this? To make use of Java’s write-once-run-everywhere capabilities, you normally need a Java runtime for each platform, but GraalVM turns this approach upside down. While the HotSpot JVM takes care of executing the bytecode on the specific platform at runtime, GraalVM creates a platform-specific application at compile time.

This article describes the final result of the project described in this series, running on an Android phone. You can see how the game plays here.

Refresher on the FXGL game development framework

FXGL clearly illustrates that JavaFX is a great extension to the Java world. Not only can you create user interfaces, but with FXGL you can also create full-fledged games.

The following are three examples of games created with FXGL:

  • Territory: Animals Genetic Strategy is a single-player strategy game on Steam about genes and animal species. Animals are reproducing, eating, and dying the way they would in nature. You can mutate them and indirectly control them to achieve various goals such as dominance in the ecosystem. The creator, Almas Baimagambetov, shared some of his experience with FXGL in the discussion board.
  • Game Tower Defense is a tower defense game with JavaFX and FXGL.
  • Min0taur: PvP is a small turn-based multiplayer combat game built with an FXGL engine.

Changes in the project

For this article, which targets an Android phone instead of a Raspberry Pi single-board computer, a separate branch called native was used in the same FDelporte/JavaMagazineFXGL project on GitHub. You can see how it looks in Figure 1. Let’s go through the changes compared to the desktop and Raspberry Pi versions.

The rendering of the game showing an onscreen joystick and shoot button

Figure 1. The game is extended with an onscreen joystick and shoot button.

Gluon libraries. To assist the conversion of the JavaFX and FXGL project into a native application with GraalVM, I used several tools provided by Gluon.

  • Gluon Attach is the component that addresses the integration with low-level platform APIs in an end-to-end Java mobile device solution; it provides a uniform, platform-independent API to access device and hardware features. At runtime, the appropriate implementation makes sure the platform-specific code is used to deliver the functionality.
  • The GluonFX plugin for Maven leverages GraalVM, OpenJDK, and JavaFX 11+ by compiling into native code the Java client application and all its required dependencies, so it can directly be executed as a native application on the target platform.

Dependencies in the pom.xml file. This version of the game’s application uses Java 17. The FXGL gaming framework is now version 17.1, which includes GluonFX, Gluon Attach 4.0.13, and JavaFX 18. That combination helps create the native application. See the release notes for FXGL 17.1.

Additional build plugin and profile settings are required with specific settings for all the platforms you want to support. A full description of all the available options is on the Gluon documentation website.

Code changes

The game’s code changes are minimal because I wanted to add only some onscreen controllers to be able to play the game. With Gluon Attach, it would also be possible to use the accelerometer of the device to move the Duke character on the screen, but because this is just a proof-of-concept application, I used a simple joystick control. Consider adding the accelerometer functionality as an exercise.

Onscreen joystick and shoot button. FXGL includes several joystick controllers, such as some inspired by the Sony PlayStation and Microsoft Xbox controllers. For this project, I selected a simple controller, which I combined with a standard JavaFX button. I used some resizing to place the buttons at the left and right at the bottom of the screen, as follows:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
VirtualJoystick joystick = getInput().createVirtualJoystick();
joystick.setTranslateX(25);
joystick.setTranslateY(getAppHeight() - 220D);

var shoot = new Button("Shoot");
shoot.setMinWidth(100);
shoot.setMinHeight(100);
shoot.setTranslateX(getAppWidth() - 120D);
shoot.setTranslateY(getAppHeight() - 120D);
shoot.setTranslateZ(0.2);

getGameScene().addUINodes(scoreLabel, scoreValue, livesLabel, livesValue, joystick, shoot);

Similar to what I did with the physical joystick in the Raspberry Pi version of this game, the code links the action of this button to the key presses previously configured. This way, you can still test the game using the keyboard on your PC, as well as with the onscreen controls on your mobile device.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
shoot.setOnMousePressed(e -> getInput().mockKeyPress(KeyCode.SPACE));
shoot.setOnMouseReleased(e -> getInput().mockKeyRelease(KeyCode.SPACE));

For the joystick, a different approach is needed to handle the mobile controller. Within the onUpdate method, you can change the orientation of the player entity according to the direction vector of the virtual joystick.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
/**
* Gets called every frame _only_ in Play state.
*/
@Override
protected void onUpdate(double tpf) {
    ...
    var joystickDirection = joystick.getVector();
    if (joystickDirection.getX() < 0) {
        player.getComponent(PlayerComponent.class).left();
    } else if (joystickDirection.getX() > 0) {
        player.getComponent(PlayerComponent.class).right();
    }
    if (joystickDirection.getY() < 0) {
        player.getComponent(PlayerComponent.class).up();
    } else if (joystickDirection.getY() > 0) {
        player.getComponent(PlayerComponent.class).down();
    }
}

These are the only code changes needed to turn the FXGL game into a mobile game!

Additional GraalVM files

With mvn gluonfx:runagent you can start the project on your development PC and then use javafx-maven-plugin with GraalVM’s JVM and with the native-image agent to record the behavior of the Java application. These tools generate the configuration files for reflection, Java Native Interface (JNI), resource, proxy, and serialization that will be used by the native-image generation. These files can be found in src/resources/META-INF/native-image/.

By the way, the GluonFX plugin provides a basic set of configuration files that can be modified manually. In any case, the configuration files generated by the tracing agent will be picked and merged with those generated by the plugin (as in most cases the content of both will be duplicated).

A bug and fix. While creating and testing this game, I discovered a problem on Android that produced the error cannot locate symbol "JNI_OnLoad_javajpeg". You may or may not encounter the same issue on your device. The error is related to the issue noted at https://github.com/gluonhq/substrate/pull/1000 about missing awt symbols. A fix is available by adding an extra C file to the project (android/missing_symbols.c) containing these extra methods, as follows:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
#include <stdlib.h>

void JNI_OnLoad_javajpeg() {
    fprintf(stderr, "We should never reach here (JNI_OnLoad_javajpeg)\n");
}

void JNI_OnLoad_lcms() {
    fprintf(stderr, "We should never reach here (JNI_OnLoad_lcms)\n");
}

...

This file needs to be compiled for Android using the Android Native Development Kit (NDK). To install CMake and the default NDK in Android Studio, do the following:

  • With a project open, click Tools > SDK Manager.
  • Click the SDK Tools tab.
  • Select the NDK (Side by side) and CMake checkboxes.

Compilation can now be done with the following shell command:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
$ ~/Android/Sdk/ndk/23.1.7779620/toolchains/llvm/prebuilt/linux-x86_64/bin/clang -c -target aarch64-linux-android -I. android/missing_symbols.c

Check that the file missing_symbols.o has been created in your project, and include it in the Android build profile in the pom.xml file, as follows:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
<profile>
    <id>android</id>
    <properties>
        <client.target>android</client.target>
        <linker.arg>/home/runner/work/JavaMagazineFXGL/JavaMagazineFXGL/android/missing_symbols.o</linker.arg>
    </properties>
</profile>

Build the native game project

You can build the project to a native PC application with the following shell command:

mvn -Pdesktop gluonfx:build gluonfx:package

If you are working on a Linux PC, you can also build the Android application, as follows:

mvn -Pandroid gluonfx:build gluonfx:package

If you have the Android platform tools installed, you can even push the created .apk file directly to an Android device and check the log if you want to debug your game, as follows:

  1. Connect the Android device to your PC with a USB cable.
  2. Enable USB debug in the settings of your phone.
  3. Install Android Studio on your PC.
  4. In a terminal window on your PC, use the following commands:
    $ ~/Android/Sdk/platform-tools/adb install JavaMagazineFXGLDemo.apk
    $ ~/Android/Sdk/platform-tools/adb logcat

Scripts for GitHub actions

You have now tested and built the project on your computer, but you can also make GitHub do the boring work. The native branch of the project contains workflows for native builds for Windows, macOS, Linux, and Android as separate files. Of course, these can be combined for further optimization, but use these as examples to create the flows according to your preferred way of working. Check each file for platform-specific settings.

Please note that each workflow uses a Gluon-provided build of GraalVM, with some small improvements regarding the compilation from JavaFX to a native application. Here is the .yml file.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
# Make sure the latest GraalVM is installed.
# After this step env.JAVA_HOME and env.GRAALVM_HOME will point to the GraalVM location
- name: Setup GraalVM built by Gluon
  uses: gluonhq/setup-graalvm@master

Conclusion

The two previous articles in this series created a simple JavaFX arcade game for the desktop and expanded it to a Raspberry Pi game with an arcade-style joystick and button. In this article, you went a big step further by compiling the game to a native application for Android.

For me, the quality of the tools, libraries, and community in the Java world keep proving they are on a higher level compared to other approaches for mobile applications. The stability of the Java language should get more love and credit when an architecture must be selected to create a new application. It’s my hope that more and more JavaFX-based smartphone applications get published!

Finally, many thanks to Johan Vos, José Pereda, and Almas Baimagambetov for their assistance and for improvements in both FXGL and the GraalVM version to simplify the creation of native JavaFX applications while I was working on this article.

Dig deeper

Frank Delporte

Frank Delporte is the author of Getting Started with Java on Raspberry Pi and works at Azul Systems. He is also lead coach of CoderDojo Belgium in Ieper.


Previous Post

Bruce Eckel on Java pattern matching guards and dominance

Bruce Eckel | 8 min read

Next Post


Programming lightweight IoT messaging with MQTT in Java

Eric J. Bruno | 19 min read