loading_screen
Nifty Loading Screen (Progress Bar)
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).
You need to add the jme3-niftygui and jme3-test-data libraries.
You will need to set your projects source to JDK 8.
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:
-
Updating explicitly over many frames
-
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.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.niftygui.NiftyJmeDisplay;
import static com.jme3.niftygui.NiftyJmeDisplay.newNiftyJmeDisplay;
import com.jme3.renderer.Camera;
import com.jme3.terrain.geomipmap.TerrainLodControl;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.heightmap.AbstractHeightMap;
import com.jme3.terrain.heightmap.ImageBasedHeightMap;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;
import de.lessvoid.nifty.Nifty;
import de.lessvoid.nifty.controls.Controller;
import de.lessvoid.nifty.controls.Parameters;
import de.lessvoid.nifty.elements.Element;
import de.lessvoid.nifty.elements.render.TextRenderer;
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 java.util.ArrayList;
import java.util.List;
/**
* This is the TestLoadingScreen Class of your Game. You should only do
* initialization here. Move your Logic into AppStates or Controls
*
* @author normenhansen
*/
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 = newNiftyJmeDisplay(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").findElementById(
"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<>();
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").findElementById(
"progressbar");
}
// methods for Controller
@Override
public boolean inputEvent(final NiftyInputEvent inputEvent) {
return false;
}
@Override
public void onFocus(boolean getFocus) {
}
@Override
public void bind(Nifty nifty, Screen screen, Element elmnt,
Parameters prmtrs) {
progressBarElement = elmnt.findElementById("progressbar");
}
@Override
public void init(Parameters prmtrs) {
}
}
Try and add all controls near the end, as their update loops may begin executing. |
Using multithreading
For more info on multithreading: The jME3 Threading Model
Make sure to change the XML file to point the controller to TestLoadingScreen*1*.
package jme3test;
import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.niftygui.NiftyJmeDisplay;
import static com.jme3.niftygui.NiftyJmeDisplay.newNiftyJmeDisplay;
import com.jme3.renderer.Camera;
import com.jme3.terrain.geomipmap.TerrainLodControl;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.heightmap.AbstractHeightMap;
import com.jme3.terrain.heightmap.ImageBasedHeightMap;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;
import de.lessvoid.nifty.Nifty;
import de.lessvoid.nifty.controls.Controller;
import de.lessvoid.nifty.controls.Parameters;
import de.lessvoid.nifty.elements.Element;
import de.lessvoid.nifty.elements.render.TextRenderer;
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 java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
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 ScheduledExecutorService exec = Executors.newScheduledThreadPool(2);
private Future loadFuture = null;
private TextRenderer textRenderer;
private static final Logger LOG = Logger.getLogger(TestLoadingScreen1.class.
getName());
public static void main(String[] args) {
TestLoadingScreen1 app = new TestLoadingScreen1();
app.start();
}
@Override
public void simpleInitApp() {
flyCam.setEnabled(false);
niftyDisplay = newNiftyJmeDisplay(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, submit Callable to 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>() {
@Override
public Void call() {
Element element = nifty.getScreen("loadlevel").findElementById(
"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<>();
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(() -> {
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").findElementById(
"progressbar");
}
// methods for Controller
@Override
public boolean inputEvent(final NiftyInputEvent inputEvent) {
return false;
}
@Override
public void onFocus(boolean getFocus) {
}
@Override
public void destroy() {
super.destroy();
shutdownAndAwaitTermination(exec);
}
//standard shutdown process for executor
private void shutdownAndAwaitTermination(ExecutorService pool) {
pool.shutdown(); // Disable new tasks from being submitted
try {
// Wait a while for existing tasks to terminate
if (!pool.awaitTermination(6, TimeUnit.SECONDS)) {
pool.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if (!pool.awaitTermination(6, TimeUnit.SECONDS)) {
LOG.log(Level.SEVERE, "Pool did not terminate {0}", pool);
}
}
} catch (InterruptedException ie) {
// (Re-)Cancel if current thread also interrupted
pool.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
}
@Override
public void bind(Nifty nifty, Screen screen, Element elmnt,
Parameters prmtrs) {
progressBarElement = elmnt.findElementById("progressbar");
}
@Override
public void init(Parameters prmtrs) {
}
}