= loading_screen :revnumber: 2.1 :revdate: 2020/07/24 == Nifty Loading Screen (Progress Bar) //There is a good tutorial about creating a nifty progress bar here: //link:http://sourceforge.net/apps/mediawiki/nifty-gui/index.php?title=Create_your_own_Control_%28//A_Nifty_Progressbar%29[http://sourceforge.net/apps/mediawiki/nifty-gui/index.php?title=Create_yo//ur_own_Control_%28A_Nifty_Progressbar%29] 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). image:gui/inner1.png[inner1.png,width="",height=""] image:gui/border1.png[border1.png,width="",height=""] You need to add the jme3-niftygui and xref:sdk:sample_code#jme3testdata-assets.adoc[jme3-test-data] libraries. You will need to set your projects source to JDK 8. This is the progress bar at 90%: image:gui/loadingscreen.png[loadingscreen.png,width="",height=""] nifty_loading.xml [source,xml] ---- ---- === Understanding Nifty XML The progress bar and text is done statically using nifty XML. A custom control is created, which represents the progress bar. [source,xml] ---- ---- This screen simply displays a button in the middle of the screen, which could be seen as a simple main menu UI. [source,xml] ---- ---- This screen displays our custom progress bar control with a text control. [source,xml] ---- ---- == 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: [source,java] ---- 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. [source,java] ---- 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 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) { } } ---- NOTE: Try and add all controls near the end, as their update loops may begin executing. === Using multithreading For more info on multithreading: xref:app/multithreading.adoc[The jME3 Threading Model] Make sure to change the XML file to point the controller to TestLoadingScreen*1*. [source,java] ---- 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 assetmanager is threadsafe, it can be used to load data from //any thread. //We do *not* attach the objects to the rootNode here! Callable loadingCallable = new Callable() { @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 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) { } } ----