title |
---|
Nifty Loading Screen (Progress Bar) |
There is a good tutorial about creating a nifty progress bar here: http://sourceforge.net/apps/mediawiki/nifty-gui/index.php?title=Create_your_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)
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>
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>
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
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
For more info on multithreading: The jME3 Threading Model
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(); } }