### Nifty Loading Screen (Progress Bar)

There is a good tutorial about creating a nifty progress bar here:

This example will use the existing hello terrain as an example. It will require these 2 images inside Assets/Interface/ (save them as border.png and inner.png respectively)

This is the progress bar at 90%:

nifty_loading.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <nifty>
        <useStyles filename="nifty-default-styles.xml" />
        <useControls filename="nifty-default-controls.xml" />

        <controlDefinition name = "loadingbar" controller = "jme3test.TestLoadingScreen">
            <image filename="Interface/border.png" childLayout="absolute"
                   imageMode="resize:15,2,15,15,15,2,15,2,15,2,15,15">
                <image id="progressbar" x="0" y="0" filename="Interface/inner.png" width="32px" height="100%"
                       imageMode="resize:15,2,15,15,15,2,15,2,15,2,15,15" />
            </image>
        </controlDefinition>

        <screen id="start" controller = "jme3test.TestLoadingScreen">
            <layer id="layer" childLayout="center">
                <panel id = "panel2" height="30%" width="50%" align="center" valign="center" childLayout="vertical"
                       visibleToMouse="true">
                    <control id="startGame" name="button" backgroundColor="#0000" label="Load Game" align="center">
                        <interact onClick="showLoadingMenu()" />
                    </control>
                </panel>
            </layer>
        </screen>

        <screen id="loadlevel" controller = "jme3test.TestLoadingScreen">
            <layer id="loadinglayer" childLayout="center" backgroundColor="#000000">
                <panel id = "loadingpanel" childLayout="vertical" align="center" valign="center" height="32px" width="70%">
                    <control name="loadingbar" align="center" valign="center" width="100%" height="100%" />
                    <control id="loadingtext" name="label" align="center"
                             text="                                                  "/>
                </panel>
            </layer>
        </screen>

        <screen id="end" controller = "jme3test.TestLoadingScreen">
        </screen>

    </nifty>

#### Understanding Nifty XML

The progress bar and text is done statically using nifty XML. A custom control is created, which represents the progress bar.

        <controlDefinition name = "loadingbar" controller = "jme3test.TestLoadingScreen">
            <image filename="Interface/border.png" childLayout="absolute"
                   imageMode="resize:15,2,15,15,15,2,15,2,15,2,15,15">
                <image id="progressbar" x="0" y="0" filename="Interface/inner.png" width="32px" height="100%"
                       imageMode="resize:15,2,15,15,15,2,15,2,15,2,15,15"/>
            </image>
        </controlDefinition>

This screen simply displays a button in the middle of the screen, which could be seen as a simple main menu UI.

        <screen id="start" controller = "jme3test.TestLoadingScreen">
            <layer id="layer" childLayout="center">
                <panel id = "panel2" height="30%" width="50%" align="center" valign="center" childLayout="vertical"
                       visibleToMouse="true">
                    <control id="startGame" name="button" backgroundColor="#0000" label="Load Game" align="center">
                        <interact onClick="showLoadingMenu()" />
                    </control>
                </panel>
            </layer>
        </screen>

This screen displays our custom progress bar control with a text control

        <screen id="loadlevel" controller = "jme3test.TestLoadingScreen">
            <layer id="loadinglayer" childLayout="center" backgroundColor="#000000">
                <panel id = "loadingpanel" childLayout="vertical" align="center" valign="center" height="32px" width="400px">
                    <control name="loadingbar" align="center" valign="center" width="400px" height="32px" />
                    <control id="loadingtext" name="label" align="center"
                              text="                                                  "/>
                </panel>
            </layer>
        </screen>

### Creating the bindings to use the Nifty XML

There are 3 main ways to update a progress bar. To understand why these methods are necessary, an understanding of the graphics pipeline is needed.

Something like this in a single thread will not work:

    load_scene();
    update_bar(30%);
    load_characters();
    update_bar(60%);
    load_sounds();
    update_bar(100%);

If you do all of this in a single frame, then it is sent to the graphics card only after the whole code block has executed. By this time the bar has reached 100% and the game has already begun ??? for the user, the progressbar on the screen would not have visibly changed.

The 2 main good solutions are:

1. Updating explicitly over many frames
2. Multi-threading

#### Updating progress bar over a number of frames

The idea is to break down the loading of the game into discrete parts

    package jme3test;

    import com.jme3.niftygui.NiftyJmeDisplay;
    import de.lessvoid.nifty.Nifty;
    import de.lessvoid.nifty.elements.Element;
    import de.lessvoid.nifty.input.NiftyInputEvent;
    import de.lessvoid.nifty.screen.Screen;
    import de.lessvoid.nifty.screen.ScreenController;
    import de.lessvoid.nifty.tools.SizeValue;
    import com.jme3.app.SimpleApplication;
    import com.jme3.material.Material;
    import com.jme3.renderer.Camera;
    import com.jme3.terrain.geomipmap.TerrainLodControl;
    import com.jme3.terrain.heightmap.AbstractHeightMap;
    import com.jme3.terrain.geomipmap.TerrainQuad;
    import com.jme3.terrain.heightmap.ImageBasedHeightMap;
    import com.jme3.texture.Texture;
    import com.jme3.texture.Texture.WrapMode;
    import de.lessvoid.nifty.controls.Controller;
    import de.lessvoid.nifty.elements.render.TextRenderer;
    import de.lessvoid.xml.xpp3.Attributes;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Properties;
    import jme3tools.converters.ImageToAwt;

    public class TestLoadingScreen extends SimpleApplication implements ScreenController, Controller {

        private NiftyJmeDisplay niftyDisplay;
        private Nifty nifty;
        private Element progressBarElement;
        private TerrainQuad terrain;
        private Material mat_terrain;
        private float frameCount = 0;
        private boolean load = false;
        private TextRenderer textRenderer;

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

        @Override
        public void simpleInitApp() {
            flyCam.setEnabled(false);
            niftyDisplay = new NiftyJmeDisplay(assetManager,
                    inputManager,
                    audioRenderer,
                    guiViewPort);
            nifty = niftyDisplay.getNifty();

            nifty.fromXml("Interface/nifty_loading.xml", "start", this);

            guiViewPort.addProcessor(niftyDisplay);
        }

        @Override
        public void simpleUpdate(float tpf) {

            if (load) { //loading is done over many frames
                if (frameCount == 1) {
                    Element element = nifty.getScreen("loadlevel").findElementByName("loadingtext");
                    textRenderer = element.getRenderer(TextRenderer.class);

                    mat_terrain = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");
                    mat_terrain.setTexture("Alpha", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));
                    setProgress(0.2f, "Loading grass");

                } else if (frameCount == 2) {
                    Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");
                    grass.setWrap(WrapMode.Repeat);
                    mat_terrain.setTexture("Tex1", grass);
                    mat_terrain.setFloat("Tex1Scale", 64f);
                    setProgress(0.4f, "Loading dirt");

                } else if (frameCount == 3) {
                    Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");

                    dirt.setWrap(WrapMode.Repeat);
                    mat_terrain.setTexture("Tex2", dirt);
                    mat_terrain.setFloat("Tex2Scale", 32f);
                    setProgress(0.5f, "Loading rocks");

                } else if (frameCount == 4) {
                    Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");

                    rock.setWrap(WrapMode.Repeat);

                    mat_terrain.setTexture("Tex3", rock);
                    mat_terrain.setFloat("Tex3Scale", 128f);
                    setProgress(0.6f, "Creating terrain");

                } else if (frameCount == 5) {
                    AbstractHeightMap heightmap = null;
                    Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png");
                    heightmap = new ImageBasedHeightMap(heightMapImage.getImage());

                    heightmap.load();
                    terrain = new TerrainQuad("my terrain", 65, 513, heightmap.getHeightMap());
                    setProgress(0.8f, "Positioning terrain");

                } else if (frameCount == 6) {
                    terrain.setMaterial(mat_terrain);

                    terrain.setLocalTranslation(0, -100, 0);
                    terrain.setLocalScale(2f, 1f, 2f);
                    rootNode.attachChild(terrain);
                    setProgress(0.9f, "Loading cameras");

                } else if (frameCount == 7) {
                    List<Camera> cameras = new ArrayList<Camera>();
                    cameras.add(getCamera());
                    TerrainLodControl control = new TerrainLodControl(terrain, cameras);
                    terrain.addControl(control);
                    setProgress(1f, "Loading complete");

                } else if (frameCount == 8) {
                    nifty.gotoScreen("end");
                    nifty.exit();
                    guiViewPort.removeProcessor(niftyDisplay);
                    flyCam.setEnabled(true);
                    flyCam.setMoveSpeed(50);
                }

                frameCount++;
            }
        }

        public void setProgress(final float progress, String loadingText) {
            final int MIN_WIDTH = 32;
            int pixelWidth = (int) (MIN_WIDTH + (progressBarElement.getParent().getWidth() - MIN_WIDTH) * progress);
            progressBarElement.setConstraintWidth(new SizeValue(pixelWidth + "px"));
            progressBarElement.getParent().layoutElements();

            textRenderer.setText(loadingText);
        }

        public void showLoadingMenu() {
            nifty.gotoScreen("loadlevel");
            load = true;
        }

        @Override
        public void onStartScreen() {
        }

        @Override
        public void onEndScreen() {
        }

        @Override
        public void bind(Nifty nifty, Screen screen) {
            progressBarElement = nifty.getScreen("loadlevel").findElementByName("progressbar");
        }

        // methods for Controller
        @Override
        public boolean inputEvent(final NiftyInputEvent inputEvent) {
            return false;
        }

        @Override
        public void bind(Nifty nifty, Screen screen, Element elmnt, Properties prprts, Attributes atrbts) {
            progressBarElement = elmnt.findElementByName("progressbar");
        }

        @Override
        public void init(Properties prprts, Attributes atrbts) {
        }

        public void onFocus(boolean getFocus) {
        }
    }

Note:

- Try and add all controls near the end, as their update loops may begin executing

#### Using multithreading

For more info on multithreading:

Make sure to change the XML file to point the controller to TestLoadingScreen1

    package jme3test;

    import com.jme3.niftygui.NiftyJmeDisplay;
    import de.lessvoid.nifty.Nifty;
    import de.lessvoid.nifty.elements.Element;
    import de.lessvoid.nifty.input.NiftyInputEvent;
    import de.lessvoid.nifty.screen.Screen;
    import de.lessvoid.nifty.screen.ScreenController;
    import de.lessvoid.nifty.tools.SizeValue;
    import com.jme3.app.SimpleApplication;
    import com.jme3.material.Material;
    import com.jme3.renderer.Camera;
    import com.jme3.terrain.geomipmap.TerrainLodControl;
    import com.jme3.terrain.heightmap.AbstractHeightMap;
    import com.jme3.terrain.geomipmap.TerrainQuad;
    import com.jme3.terrain.heightmap.ImageBasedHeightMap;
    import com.jme3.texture.Texture;
    import com.jme3.texture.Texture.WrapMode;
    import de.lessvoid.nifty.controls.Controller;
    import de.lessvoid.nifty.elements.render.TextRenderer;
    import de.lessvoid.xml.xpp3.Attributes;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Properties;
    import java.util.concurrent.Callable;
    import java.util.concurrent.Future;
    import java.util.concurrent.ScheduledThreadPoolExecutor;
    import jme3tools.converters.ImageToAwt;

    public class TestLoadingScreen1 extends SimpleApplication implements ScreenController, Controller {

        private NiftyJmeDisplay niftyDisplay;
        private Nifty nifty;
        private Element progressBarElement;
        private TerrainQuad terrain;
        private Material mat_terrain;
        private boolean load = false;
        private ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(2);
        private Future loadFuture = null;
        private TextRenderer textRenderer;

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

        @Override
        public void simpleInitApp() {
            flyCam.setEnabled(false);
            niftyDisplay = new NiftyJmeDisplay(assetManager,
                    inputManager,
                    audioRenderer,
                    guiViewPort);
            nifty = niftyDisplay.getNifty();

            nifty.fromXml("Interface/nifty_loading.xml", "start", this);

            guiViewPort.addProcessor(niftyDisplay);
        }

        @Override
        public void simpleUpdate(float tpf) {
            if (load) {
                if (loadFuture == null) {
                    //if we have not started loading yet, submit the Callable to the executor
                    loadFuture = exec.submit(loadingCallable);
                }
                //check if the execution on the other thread is done
                if (loadFuture.isDone()) {
                    //these calls have to be done on the update loop thread,
                    //especially attaching the terrain to the rootNode
                    //after it is attached, it's managed by the update loop thread
                    // and may not be modified from any other thread anymore!
                    nifty.gotoScreen("end");
                    nifty.exit();
                    guiViewPort.removeProcessor(niftyDisplay);
                    flyCam.setEnabled(true);
                    flyCam.setMoveSpeed(50);
                    rootNode.attachChild(terrain);
                    load = false;
                }
            }
        }
        //this is the callable that contains the code that is run on the other thread.
        //since the assetmananger is threadsafe, it can be used to load data from any thread
        //we do *not* attach the objects to the rootNode here!
        Callable<Void> loadingCallable = new Callable<Void>() {

            public Void call() {

                Element element = nifty.getScreen("loadlevel").findElementByName("loadingtext");
                textRenderer = element.getRenderer(TextRenderer.class);

                mat_terrain = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");
                mat_terrain.setTexture("Alpha", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));
                //setProgress is thread safe (see below)
                setProgress(0.2f, "Loading grass");

                Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");
                grass.setWrap(WrapMode.Repeat);
                mat_terrain.setTexture("Tex1", grass);
                mat_terrain.setFloat("Tex1Scale", 64f);
                setProgress(0.4f, "Loading dirt");

                Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");

                dirt.setWrap(WrapMode.Repeat);
                mat_terrain.setTexture("Tex2", dirt);
                mat_terrain.setFloat("Tex2Scale", 32f);
                setProgress(0.5f, "Loading rocks");

                Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");

                rock.setWrap(WrapMode.Repeat);

                mat_terrain.setTexture("Tex3", rock);
                mat_terrain.setFloat("Tex3Scale", 128f);
                setProgress(0.6f, "Creating terrain");

                AbstractHeightMap heightmap = null;
                Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png");
                heightmap = new ImageBasedHeightMap(heightMapImage.getImage());

                heightmap.load();
                terrain = new TerrainQuad("my terrain", 65, 513, heightmap.getHeightMap());
                setProgress(0.8f, "Positioning terrain");

                terrain.setMaterial(mat_terrain);

                terrain.setLocalTranslation(0, -100, 0);
                terrain.setLocalScale(2f, 1f, 2f);
                setProgress(0.9f, "Loading cameras");

                List<Camera> cameras = new ArrayList<Camera>();
                cameras.add(getCamera());
                TerrainLodControl control = new TerrainLodControl(terrain, cameras);
                terrain.addControl(control);
                setProgress(1f, "Loading complete");

                return null;
            }
        };

        public void setProgress(final float progress, final String loadingText) {
            //since this method is called from another thread, we enqueue the changes to the progressbar to the update loop thread
            enqueue(new Callable() {

                public Object call() throws Exception {
                    final int MIN_WIDTH = 32;
                    int pixelWidth = (int) (MIN_WIDTH + (progressBarElement.getParent().getWidth() - MIN_WIDTH) * progress);
                    progressBarElement.setConstraintWidth(new SizeValue(pixelWidth + "px"));
                    progressBarElement.getParent().layoutElements();

                    textRenderer.setText(loadingText);
                    return null;
                }
            });

        }

        public void showLoadingMenu() {
            nifty.gotoScreen("loadlevel");
            load = true;
        }

        @Override
        public void onStartScreen() {
        }

        @Override
        public void onEndScreen() {
        }

        @Override
        public void bind(Nifty nifty, Screen screen) {
            progressBarElement = nifty.getScreen("loadlevel").findElementByName("progressbar");
        }

        // methods for Controller
        @Override
        public boolean inputEvent(final NiftyInputEvent inputEvent) {
            return false;
        }

        @Override
        public void bind(Nifty nifty, Screen screen, Element elmnt, Properties prprts, Attributes atrbts) {
            progressBarElement = elmnt.findElementByName("progressbar");
        }

        @Override
        public void init(Properties prprts, Attributes atrbts) {
        }

        public void onFocus(boolean getFocus) {
        }

        @Override
        public void stop() {
            super.stop();
            //the pool executor needs to be shut down so the application properly exits.
            exec.shutdown();
        }
    }

view online version