JavaFX 11 for JME

JavaFX 11 for JME


Download

A JavaFX 11 implementation for JmonkeyEngine that allows you to use a regular JavaFX GUI in your games.

This implementation fully supports FXML and CSS, which means you can use the Gluon SceneBuilder application to create a fully-fledged themed scene with JavaFx Controllers. The JavaFX process of creating objects is unchanged and follows the regular JavaFX workflow.

Add the library to your project

Gradle


// add the openjfx plugin so you can use javafx
plugins {
    id 'org.openjfx.javafxplugin' version '0.0.7'
}

// choose the javafx modules you want to import.
javafx { 
    version = "11.0.2"
    modules = [ 'javafx.controls', 'javafx.fxml' ]
}

// import the jme-jfx dependency
dependencies {
    implementation 'com.jayfella:jme-jfx-11:1.1.4'
}

Integrate it into your game


public class Main extends SimpleApplication {

    public static void main(String... args) {
        Main main = new Main();
        main.start();
    }

    TestJavaFx() {
        super(new StatsAppState());
    }


    @Override
    public void simpleInitApp() {

        JavaFxUI.initialize(this);
        
        // add a javafx control to the GUI scene
        Button button = new Button("Click Me");
        JavaFxUI.getInstance().attachChild(button);
    }
}

Controls

You may create any JavaFX object that extends javafx.scene.Node in either code or fxml.
Controllers and loading fxml from resources are all supported.

Controls are added and removed similar to the jmonkey workflow via the JavaFxUI static class.


// add a control
JavaFxUI.getInstance().attachChild(javafx.scene.Node node);

// remove a control
JavaFxUI.getInstance().detachChild(javafx.scene.Node node);
JavaFxUI.getInstance().detachChild(String fxId);

// get a control
JavaFxUI.getInstance().getChild(String fxId);

Since all JavaFX controls run in the JavaFX thread, you may need to alternate between the JME thread and
the JavaFX thread. There are two convenience methods provided to do this.


// do something in the JavaFX thread (such as update a label text)
JavaFxUI.getInstance().runInJavaFxThread(() -> {
    myLabel.setText("changed text");
});

// do something in the JME thread (such as manipulate the scene)
JavaFxUI.getInstance().runInJmeThread(() -> {
    someJmeNode.setLocalTranslation(x, y, z);
});

Although JavaFX alerts are supported, you may wish to display an in-game custom dialog.
These dialogs allow you to dim the background and stop background objects being clicked.


// display a javafx control that is centered in the screen with a dimmed background.
JavaFxUI.getInstance().showDialog(myJavaFxControl)

// display a javafx control that is centered and NOT dimmed.
JavaFxUI.getInstance().showDialog(myJavaFxControl, false);

// remove the dialog
JavaFxUI.getInstance().removeDialog();

Update Loops

If you need an update loop in your control, implement the JmeUpdateLoop interface and you will be provided with a update(float tpf) method that will be called every frame.

public class MyControl implements JmeUpdateLoop {
    @Override
    public void update(float tpf) {
        // update logic goes here...
    }
}

Add and Remove Notifications

If your control requires notification that it has been added or removed, implement the SceneNotifier interface. You will be provided with an onAttached(Application app) and void onDetached() method that is called when it is added and removed from the scene.

public class MyControl implements SceneNotifier {
    @Override
    public void onAttached(Application app) {
        // called whenever you add this control to the JavaFX scene.
    }

    @Override
    public void onDetached() {
        // called whenever this control is removed from the scene.
    }

}

Source code as always is available on github.

https://github.com/jayfella/jme-jfx-11

12 Likes

@jayfella thank you for the contribution! I have started using your library in my project, rather than maintaining my own JFX management system developed around the jme-jfx bridge. I’m curious about your choice of jme dependency. The build.gradle file is using “[3.3,)” which is not generally a good idea in a released library as things in future jme releases could potentially break some feature in jme-jfx-11, or in my own project. As a result, for the time being I have had to adjust my own build.gradle to exclude the transitive dependency as follows:

    compile('com.jayfella:jme-jfx-11:1.0.3') {
        exclude group: 'org.jmonkeyengine'
    }

Would you consider changing this in a future release?

Yes, and I agree.

In this case I’m a victim of copy-pasting and didn’t even consider it. Sorry about that.

When I update it again I’ll change the version to the latest stable - which I think is 3.2.3?

The source code is also available on github (I’ve updated the main post with the link), so if you feel like filing a bug report it will remind me :slight_smile:

2 Likes

Done!

1 Like

@jayfella an issue I hit hard on my branch (took days to figure out what was going on) was that if there is no scene open for a while, the jfx thread kills itself.
My solution was to add Platform.setImplicitExit(false); on the initFx() function in the JmeFxContainerImpl

private void initFx() {
        PlatformImpl.startup(() -> {
            switch (Pixels.getNativeFormat()) {
                case Pixels.Format.BYTE_ARGB:
                    try {
                        nativeFormat.complete(Format.ARGB8);
                        reorderData = null;
                    } catch (final Exception exc1) {
                        nativeFormat.complete(Format.ABGR8);
                        reorderData = JmeFxContainerImpl::reorder_ARGB82ABGR8;
                    }
                    break;
                case Pixels.Format.BYTE_BGRA_PRE:
                    try {
                        nativeFormat.complete(Format.BGRA8);
                        reorderData = null;
                    } catch (final Exception exc2) {
                        nativeFormat.complete(Format.ABGR8);
                        reorderData = JmeFxContainerImpl::reorder_BGRA82ABGR8;
                    }
                    break;
                default:
                    throw new IllegalArgumentException("Not supported javaFX pixel format " + Pixels.getNativeFormat());
            }
        });
        Platform.setImplicitExit(false);
    }

I was wondering if you ran into that same issue and fixed it somewhere, otherwise it is a bug that will popup with JFX11 & 12

1 Like

I just noticed that you’re using log4j and slf4j in your library. Would it be possible to replace the log4j jar with the log4j-over-slf4j jar? This would redirect all log4j calls through the slf4j library (as described here) and thus making it easier to control logging from a single configuration.

2 Likes

That’s usually a bad idea… because then if you want slf4j to write to log4j you have some weird cycle to deal with.

To me, a library should use sl4j or not. And if it uses slf4j then it should not also be using other logging packages directly.

2 Likes

I’ve set the jmonkey version to the latest release and removed the unnecessary dependencies for logging.

I’ll upload a new version to jcenter over the next few days if I don’t find anything else.

Thank you for filing the report :slight_smile:

In my instance I use an anchor pane as an overlay that stretches to the size of the window/display, and when you add a control to the scene it simply adds to this parent, so there is always a control “alive” at all times.

I’m not certain that this circumvents the issue you highlighted, but up to now I’ve not come across it. Good to know, though. I’ll keep it in mind.

JavaFxUI.refreshSceneBounds() is completely commented out but it is being referenced in JavaFxUI.update(). Is this intended for future development?

It’s commented out because I need to figure out why the mouse input is ignored when the Jme context gets restarted.

If the resolution is changed in-game (not at startup but in a settings screen for example) - e.g. from 1280x720 to 1600x900 - the Jme context has to be restarted. When that happens the click events are no longer applied to the javafx controls - all mouse interaction is completely ignored. I need to figure out why - probably because the jfx input handler gets removed from the context on restart.

Unless the resolution is changed, though, this part is completely un-used. It would be nice to support in-game resolution changes, though, which is why I left it in for investigation.

Just switched my project from JavaFXUtils to this. Really like the direction you took it - going to give dialogs a shot here in a bit.

1 Like

Well dialogs just center a control and dim the background. You have to actually “make” the dialog control yourself - which makes sense from a “I don’t want to hold your hand too much” point of view, so you can dictate pretty much exactly how you want everything, however for things like draggable minimizable windows I feel like that kind of thing would be something that might come in useful - even if it’s just a reference object to see how it’s done, rapid development, etc (I just want a moveable window right now). Also things like graphs (which I use for FPS monitors, memory monitors, etc) are handy. I might end up making a jfx-controls package. I’m not certain…

An example of draggable minimizable windows…

5 Likes

That’s ideal, imho. UI, especially in games, benefits a lot from flexibility. Making a reasonable dialog control in SceneBuilder is about a 5 minute job (and not much longer in code, for that matter).

1 Like

Updated and pushed a new version.

New version is 1.1.0 - meaning breaking changes.

  • The JavaFxUI object now follows a loose singleton pattern. You now must access the class after initialization using the .getInstance() method. Slightly annoying if you’re already using this library, but the class got bigger than I anticipated and makes the code a lot cleaner. Better to make the change now as early as possible.
JavaFxUI.initialize(app);
JavaFxUI.getInstance().attachChild(javafx.scene.Node node);

A changelog has been added to the repository, but i’ll list them here for this update:

  • Set jmonkey version to latest release instead of latest version.
  • Remove unnecessary logging dependencies.
  • Add optional String… argument to JavaFxUI.initialize to allow importing global css stylesheets.
  • Replace System.out.println outputs to logging system
  • Use singleton pattern. All methods are now called from .getInstance()

The only other notable change is the ability to register global css styles in the initialization method - which makes theming a lot cleaner.

JavaFxUI.initialize(app, String... cssThemePaths)

So to theme your apps you can now just use:

JavaFxUI.initialize(app, "/themes/my-theme.css", "/controls/window-theme.css");

and your css will be applied to all controls added to the scene.

2 Likes

Tested this out and was getting this error initially:

IllegalAccessError: class com.jayfella.jme.jfx.injme.JmeFxContainerImpl (in unnamed module @0x34129c78) cannot access class com.sun.javafx.application.PlatformImpl (in module javafx.graphics) because module javafx.graphics does not export com.sun.javafx.application to unnamed module @0x34129c78

Ok well that’s a com.sun package so fair enough. But how’s it working for others?

I had to modify the Gradle “run” task like below to get it to cooperate with JVM args. Any idea what I missed to need this? GitHub says Java 11 required so I used OpenJDK 11. This modular stuff is killing me, clearly I’m going to need to RTFM a few times to finally “get” it.

plugins {
    id "java"
    id "jacoco"
    id "application"
    id "org.openjfx.javafxplugin" version "0.0.7"
}

mainClassName = 'test.jmejfx.TestJmeJFX'

repositories {
    jcenter()
}

def jme3 = [v: '3.2.3-stable', g: 'org.jmonkeyengine']
dependencies {
    implementation "${jme3.g}:jme3-core:${jme3.v}"
    implementation "${jme3.g}:jme3-desktop:${jme3.v}"
    implementation "${jme3.g}:jme3-lwjgl3:${jme3.v}"
    
    implementation 'com.jayfella:jme-jfx-11:1.1.0'
    
    testImplementation 'junit:junit:4.12'
}

javafx {
    version = "11.0.2"
    modules = [ 'javafx.controls', 'javafx.fxml' ]
}

run {
    doFirst {
        jvmArgs = [
                '--add-exports', 'javafx.graphics/com.sun.javafx.application=ALL-UNNAMED'
        ]
    }
}

This “demo” not exactly rocket science, nothing to see here:

package test.jmejfx;

import com.jayfella.jme.jfx.JavaFxUI;
import com.jme3.app.SimpleApplication;
import com.jme3.app.StatsAppState;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import javafx.scene.control.Button;

public class TestJmeJFX extends SimpleApplication {

    public static void main(String[] args) {
        TestJmeJFX app = new TestJmeJFX();
        app.showSettings = false;
        app.start();
    }

    TestJmeJFX() {
        super(new StatsAppState());
    }

    @Override
    public void simpleInitApp() {
        JavaFxUI.initialize(this);

        Button button = new Button("Test");
        JavaFxUI.getInstance().attachChild(button);

        Box b = new Box(1, 1, 1);
        Geometry geom = new Geometry("Box", b);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        geom.setMaterial(mat);
        rootNode.attachChild(geom);
    }
}

In intellij idea (not sure of netbeans/etc) you can just run it as an application instead of a gradle task - which skips all that stuff. Right-Click the main class and select “Run Main.main()”. Again, I’m not sure about netbeans/etc. I don’t use those IDEs.

Yeah, you can do the same in NetBeans too with a right click->run.
Try “run” via Gradle, nope - Jigsaw smackdown.

But I’m a lazy clicker and I wanted my big one-click Run button to work…

I’ll look into it, because it seems to have “erratic” behavior. On one project I must “right-click → run” and on another I can “gradle run”. It would be nice to have both, and to know exactly what is happening/why it’s so picky.

I think I understand why it was complaining to me about the sun package access (that’s supposed to be a no-no now, Oracle wanted those old Sun packages walled off), that one makes some sense to me in the 9+ world. (Even though I hate it.)

But what’s confusing me is how it’s working for others, and it works fine when “run” directly from an IDE. What the heck’s it doing different…