How to make loading screen

i have app state
and load from this app state next up state
while next level load i need add loading screen

setEnabled(false);
            
            GameLevel appLevel = new GameLevel(levelName,id,x,y,z);
            appLevel.setEnabled(true);
            app.getStateManager().attach(appLevel);

Your question is not really clear in my opinion, but general you could

  1. Attach a FadeFilter to a FilterPostProcessor and attach them to the Viewport (you may reuse the same filter each loading)
  2. Deactivate inputs etc.
  3. Use the FadeFilter to fade the visibility of the rootNode out (FadeFilter#fadeOut(), the guiNode is not affected)
  4. When done (FadeFilter#getValue()), attach the Loading screen components to the guiNode and start the loading task
  5. When loading is done, detach the Loading screen components from guiNode and call FadeFilter#fadeIn().
  6. When fading in is done, activate input etc.

I created just for fun an example. It is except “FadeAppState”, which I use in my own project, untested and not strain-forward but very object oriented (polymorphic and reusable), it may helps and if not, it doesn’t matter. It use some trivial classes from my own apis. Please note, the callback of the load-Method is called in the loading thread.

The Callback of FadeAppState is either called if the fading is done or interrupted by calling one of the fade-Methods again, whatever occurs first. So you must be sure, the fading is not interrupted by another call.

private void load(final String levelConfig, final Runnable callback) {
	final FilterPostProcessor fpp = new FilterPostProcessor(getAssetManager());
	getViewPort().addProcessor(fpp);
	
	final FadeFilter fade = new FadeFilter();
	//fade.setValue(0.0f); // To initial fade the screen out on initializing.
	fpp.addFilter(fade);
	getStateManager().attach(new FadeAppState(fade));
	
	// ^ Prerequise, reuse until here
	
	final Spatial loadScreenParent = createLoadScreen();
	
	// ^ Reuse, if you always use the same loading screen
	
	final FadeAppState state = getStateManager().getState(FadeAppState.class);
	state.fadeOut(new MultiRunnable(
			new DeactivateInputsRunnable(), // <- Implement as you want
			new AttachToNodeEnqueuedRunnable(app, getGuiNode(), loadScreenParent),
			new StartThreadRunnable(new MultiRunnable(
					new LoadLevelRunnable(app, levelConfig), // <- Implement as you want (app is for enqueueing, you could also create a parent node for the level and add them via in seperated classes)
					new DetachToNodeEnqueuedRunnable(app, guiNode, loadScreenParent),
					new FadeAppStateInRunnable(
							state,
							new ActivateInputsRunnable(), // <- Implement as you want,
							callback
					)
			))
	));
}
public final class FadeAppState implements AppState {
	private final FadeFilter filter;
	
	private boolean initialized;
	private boolean enabled;
	
	private boolean awaitListener;
	private float listenerTarget;
	private Runnable listener;
	
	public FadeAppState(final FadeFilter filter) {
		this.filter = Requires.notNull(filter, "filter == null");
		this.enabled = true;
	}
	
	@Override
	public void update(final float tpf) {
		if(awaitListener) {
			if(filter.getValue() == listenerTarget) {
				listener.run();
				listener = new NullRunnable();
				awaitListener = false;
			}
		}
	}
	
	public void fadeIn(final Runnable l) {
		Requires.notNull(l, "l == null");
		if(awaitListener) {
			listener.run();
		} else {
			awaitListener = true;
		}
		
		listenerTarget = 1.0f;
		listener = l;
		filter.fadeIn();
	}
	public void fadeOut(final Runnable l) {
		Requires.notNull(l, "l == null");
		if(awaitListener) {
			listener.run();
		} else {
			awaitListener = true;
		}
		
		listenerTarget = 0.0f;
		listener = l;
		filter.fadeOut();
	}
	public void setDuration(final float duration) {
		filter.setDuration(duration);
	}
	
	@Override
	public void initialize(final AppStateManager stateManager, final Application app) {
		initialized = true;
	}
	@Override
	public boolean isInitialized() {
		return(initialized);
	}
	@Override
	public String getId() {
		return(null);
	}
	@Override
	public void setEnabled(final boolean active) {
		this.enabled = active;
	}
	@Override
	public boolean isEnabled() {
		return(enabled);
	}
	@Override
	public void stateAttached(final AppStateManager stateManager) {
		
	}
	@Override
	public void stateDetached(final AppStateManager stateManager) {
		
	}
	@Override
	public void render(final RenderManager rm) {
		
	}
	@Override
	public void postRender() {
		
	}
	@Override
	public void cleanup() {
		
	}
}
public final class AttachToNodeEnqueuedRunnable implements Runnable {
	private final Application app;
	private final Node parent;
	private final Spatial toAttach;
	
	public AttachToNodeEnqueuedRunnable(final Application app, final Node parent, final Spatial toAttach) {
		this.app = Requires.notNull(app, "app == null");
		this.parent = Requires.notNull(parent, "parent == null");
		this.toAttach = Requires.notNull(toAttach, "toAttach == null");
	}
	@Override
	public void run() {
		app.enqueue(new Runnable() {
			@Override
			public void run() {
				parent.attachChild(toAttach);
			}
		});
	}
}
public final class DetachToNodeEnqueuedRunnable implements Runnable {
	private final Application app;
	private final Node parent;
	private final Spatial toDetach;
	
	public DetachToNodeEnqueuedRunnable(final Application app, final Node parent, final Spatial toDetach) {
		this.app = Requires.notNull(app, "app == null");
		this.parent = Requires.notNull(parent, "parent == null");
		this.toDetach = Requires.notNull(toDetach, "toDetach == null");
	}
	@Override
	public void run() {
		app.enqueue(new Runnable() {
			@Override
			public void run() {
				parent.detachChild(toDetach);
			}
		});
	}
}
public final class FadeAppStateInRunnable implements Runnable {
	private final FadeAppState state;
	private final Runnable callback;
	
	public FadeAppStateInRunnable(final FadeAppState state) {
		this(state, new NullRunnable());
	}
	public FadeAppStateInRunnable(final FadeAppState state, final Runnable callback) {
		this.state = Requires.notNull(state, "state == null");
		this.callback = Requires.notNull(callback, "callback == null");
	}
	@Override
	public void run() {
		state.fadeIn(callback);
	}
}
public final class StartThreadRunnable implements Runnable {
	private final Runnable parent;
	
	public StartThreadRunnable(final Runnable parent) { // <- May add a model containing informations like is a deamon or priority
		this.parent = Requires.notNull(parent, "parent == null");
	}
	@Override
	public void run() {
		new Thread(parent).start(); // <- May change to non-native thread implement
	}
}

Or extend BaseAppState and make your life 100x simpler.

1 Like

Hard.

I need somthing like this

  1. detach models
  2. attach loading screen
  3. attach new app state

can you show createLoadScreen(); ?

i put this

        fpp = new FilterPostProcessor(assetManager);
        fade = new FadeFilter(10); // e.g. 2 seconds
        fpp.addFilter(fade);
        viewPort.addProcessor(fpp);

what next?

This depend, what you loading screen should contain.

Basically, it should create a Node as parent and attach all you need on your loading screen, say a BitmapText to show Text or a Picture to show a Picture. The node is returned and just attached to the guiNode when it is needed.

It is technical the same as a HUD, just the usage is another:

@pspeed Thank you for the tip. I just used always a Template to create AppStates from. ^^"

PS: According to the JavaDoc of FadeFilter, the duration is in Second, so “10” should be 10 seconds:

    /**
     * Creates a FadeFilter with the given duration
     *
     * @param duration the desired duration (in seconds, default=1)
     */

Are you trying to display a loading screen while the game takes a long time to load something?

Are you loading that stuff on the same update thread that rendering happens or are you doing that on a background thread? If the first, then a loading screen won’t matter because update() will not run to display it.

Are you trying to display a loading screen while the game takes a long time to load something?
Yes

i make this

                        Picture pic = new Picture("HUD Picture");
                        pic.setImage(app.getAssetManager(), "textures/loading.jpg", true);
                        pic.setWidth(app.getContext().getSettings().getWidth()/2);
                        pic.setHeight(app.getContext().getSettings().getHeight()/2);
                        pic.setPosition(app.getContext().getSettings().getWidth()/4, app.getContext().getSettings().getHeight()/4);
                        app.getGuiNode().attachChild(pic);
                        
                         Thread thread = new Thread(){
                            public void run(){
                                GameLevel appLevel = new GameLevel(levelName,id,x,y,z);
                                appLevel.setEnabled(true);
                                app.getStateManager().attach(appLevel);
                                app.getGuiNode().detachChild(pic);
                            }
                         };

                        thread.start();

Nakano
pspeed
Thanks

You CANNOT modify the scene graph from a separate thread. Attaching the app state is ok. Modifying the guiNode directly is not.

edit: also, re: “appLevel.setEnabled(true);”
If you are extending BaseAppState then you do not have to manually enable the app state as they are always enabled by default.

But it works :slight_smile:
how to do it right?

Look in my big example into “AttachToNodeEnqueuedRunnable”.

                       pic.setImage(app.getAssetManager(), "textures/loading.jpg", true);
                       pic.setWidth(app.getContext().getSettings().getWidth()/2);
                       pic.setHeight(app.getContext().getSettings().getHeight()/2);
                       pic.setPosition(app.getContext().getSettings().getWidth()/4, app.getContext().getSettings().getHeight()/4);
                       
                       AttachToNodeEnqueuedRunnable runnableScreen = new AttachToNodeEnqueuedRunnable(app, app.getGuiNode(), pic);
                       Thread thread = new Thread(runnableScreen);  
                       thread.start();  
                       GameLevel appLevel = new GameLevel(levelName,id,x,y,z);
                       appLevel.setEnabled(true);
                       app.getStateManager().attach(appLevel);

Try this not work

The steps are:

  1. do your long running process on another thread
  2. at END of that long running thread app.enqueue() the cleanup/attach stuff.

No, you should only use the enqueue, starting this in a new Thread makes no sense. ^^" Sorry, I sometimes cannot express myself very good in English.

	Thread thread = new Thread(new Runnable() { // <- Never override classes if it is not required.
			public void run(){
				GameLevel appLevel = new GameLevel(levelName,id,x,y,z);
				appLevel.setEnabled(true);
				
				app.enqueue(new Runnable() {
					@Override
					public void run() {
						app.getStateManager().attach(appLevel);
						app.getGuiNode().detachChild(pic);
					}
				});
			}
		});

What exatly does “GameLevel”?

1 Like

What exatly does “GameLevel”?

level, character and collision

This code freezes

This are all seperated things (Level = Spatials, Character = Spatial/Controls like BetterCharacterControl, Collision = CollisionShape/BulletAppState), so you mean loading and connecting the stuff?

I think, it is wrong to do this in an AppState, it just block the JME thread as pspeed mentioned some Posts before. Just create all, Spatials, CollisionsShape, Controls, other AppStates and so on direct in the Thread and attach them at the end per enqueue.

If you ever worked with Swing: It is the same as doing a long-running or GUI-Changing stuff in the EventDispatchThread (like a Swing-called Listener), since it blocks the GUI that cannot react on other Events like input or repaint.

Sorry for the crap code at begin of the Thread, I think this should show, how it could work. I fixed also a small bug in FadeAppState that could cause a StackOverflowError:

import java.util.concurrent.TimeUnit;

import com.jme3.app.SimpleApplication;
import com.jme3.font.BitmapText;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.post.FilterPostProcessor;
import com.jme3.post.filters.FadeFilter;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;

public final class LoadingTest extends SimpleApplication {
	private Node loadingScreenNode;
	
	@Override
	public void simpleInitApp() {
		// Set up initial content
		loadContent(ColorRGBA.Red);
		
		// Set up FadeFilter & co.
		final FilterPostProcessor fpp = new FilterPostProcessor(getAssetManager());
		getViewPort().addProcessor(fpp);
		
		final FadeFilter fade = new FadeFilter();
		fpp.addFilter(fade);
		getStateManager().attach(new FadeAppState(fade));
		
		// Set up loading screen
		loadingScreenNode = new Node();
		
		final BitmapText text = new BitmapText(assetManager.loadFont("Interface/Fonts/Default.fnt"));
		text.setText("Loading...");
		text.setLocalTranslation(5, 400, 0);
		loadingScreenNode.attachChild(text);
		
		// Start process
		startThread(new Runnable() {
			@Override
			public void run() {
				sleep(1L, TimeUnit.SECONDS);
				
				load(ColorRGBA.Blue, new Runnable() {
					@Override
					public void run() {
						startThread(new Runnable() {
							@Override
							public void run() {
								sleep(1L, TimeUnit.SECONDS);
								
								load(ColorRGBA.Green, new Runnable() {
									@Override
									public void run() {
										// Null-Object
									}
								});
							}
						});
					}
				});
			}
		});
	}
	
	private void load(final ColorRGBA color, final Runnable callback) {
		// Call fadeIn and fadeOut only in the JME thread, else the previous callback could get called
		// two times, with very low chance.
		enqueue(new Runnable() {
			@Override
			public void run() {
				load0(color, callback);
			}
		});
	}
	private void load0(final ColorRGBA color, final Runnable callback) {
		// May deactivate input
		
		final FadeAppState state = getStateManager().getState(FadeAppState.class);
		state.fadeOut(new Runnable() {
			@Override
			public void run() {
				// Not required but just for safety
				enqueue(new Runnable() {
					@Override
					public void run() {
						for(final Spatial c:rootNode.getChildren()) { // Remove all childs and other level-specific stuff like AppStates, HUD and collision shapes.
							c.removeFromParent();
						}
						
						getGuiNode().attachChild(loadingScreenNode);
						
						// Deactivate input if still not done
						
						startThread(new Runnable() {
							@Override
							public void run() {
								loadContent(color);
								
								sleep(1L, TimeUnit.SECONDS);
								
								enqueue(new Runnable() {
									@Override
									public void run() {
										getGuiNode().detachChild(loadingScreenNode);
										
										state.fadeIn(new Runnable() {
											@Override
											public void run() {
												// Activate input
												enqueue(callback);
											}
										});
									}
								});
							}
						});
					}
				});
			}
		});
	}
	private void loadContent(final ColorRGBA color) {
		if(color == null) {
			throw new IllegalArgumentException("color == null");
		}
		
		final Node gameNode = new Node();
		
		gameNode.attachChild(createBox(color));
		
		enqueue(new Runnable() {
			@Override
			public void run() {
				rootNode.attachChild(gameNode);
			}
		});
	}
	
	private Spatial createBox(final ColorRGBA c) {
		final Box box = new Box(1,1,1);
		final Geometry geo = new Geometry("Box", box);
		final Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
		mat.setColor("Color", c);
		geo.setMaterial(mat);
		
		return(geo);
	}
	
	private void startThread(final Runnable r) {
		createNativeThread(r).start();
	}
	private Thread createNativeThread(final Runnable r) {
		return(new Thread(r));
	}
	
	private void sleep(final long time, final TimeUnit unit) {
		try {
			unit.sleep(time);
		} catch (final InterruptedException e) {
			throw new RuntimeException(e);
		}
	}
	
	public static void main(final String[] args) {
		new LoadingTest().start();
	}
}
import com.jme3.app.Application;
import com.jme3.app.state.BaseAppState;
import com.jme3.post.filters.FadeFilter;

public final class FadeAppState extends BaseAppState {
	private final FadeFilter filter;
	
	private float listenerTarget;
	private Runnable listener;
	
	public FadeAppState(final FadeFilter filter) {
		if(filter == null) {
			throw new IllegalArgumentException("filter == null");
		}
		this.filter = filter;
	}
	
	@Override
	public void update(final float tpf) {
		if(listener != null) {
			if(filter.getValue() == listenerTarget) {
				final Runnable r = listener;
				listener = null;
				r.run();
			}
		}
	}
	
	public void fadeIn(final Runnable l) {
		if(l == null) {
			throw new IllegalArgumentException("l == null");
		}
		
		if(listener != null) {
			listener.run();
		}
		
		listenerTarget = 1.0f;
		listener = l;
		filter.fadeIn();
	}
	public void fadeOut(final Runnable l) {
		if(l == null) {
			throw new IllegalArgumentException("l == null");
		}
		
		if(listener != null) {
			listener.run();
		}
		
		listenerTarget = 0.0f;
		listener = l;
		filter.fadeOut();
	}
	public void setDuration(final float duration) {
		if(duration < 0) {
			// Reverses the fading. Could it makes sense to allow this?
			throw new IllegalArgumentException("duration < 0");
		}
		filter.setDuration(duration);
	}
	
	@Override
	protected void cleanup(final Application app) {
		
	}
	@Override
	protected void initialize(final Application app) {
		
	}
	@Override
	protected void onDisable() {
		
	}
	@Override
	protected void onEnable() {
		
	}
}

Edit: Fix, if loading is too fast.
Edit 2: Cleaned up the example, if somebody other find and use it in the future.