SceneManager.LoadSceneAsync - let's talk about

Hi everyone, I wanted to share with you a simple architecture to dynamically load and unload scenes at Runtime.

Let me know if it has been useful to you so that I can understand what topics you are interested in and if you have ideas on how to improve the features shown :grinning:

I designed the classes using Unity engine as an example.

The Scene class contains the name of the scene and a list of appStates that compose it.

public class Scene {
    
    public String name;
    public List<Class<? extends AppState>> systemPrefabs;
    
}

The SceneManager class is used to load and unload scenes in synchronous or asynchronous mode.

The methods:

  • public AsyncOperation unloadSceneAsync(Scene newScene) {}
  • public AsyncOperation loadSceneAsync(Scene newScene) {}

return an object of type AsyncOperation that you can use to check if the asynchronous operation is done or its progress. Inside it contains a CompletableFuture to which you can chain an action that is invoked upon operation completion.

The GameAppState class is used to manage the states of your application. You can customize it as needed, adding a background image if you want during asynchronous operations.

public class GameAppState extends SimpleAppState implements ActionListener {
    
    public enum GameState { PREGAME, LOADING, RUNNING, PAUSED }
    private GameState _currGameState = GameState.PREGAME;
    
    private SceneManager sceneManager;
    Scene[] scenes = new Scene[] { Boot.scene_01, Boot.scene_02 };
    int sceneIndex = 0;
    Scene currScene = scenes[sceneIndex];
    AsyncOperation asyncOperation;

    @Override
    public void simpleInit() {
        // TODO Auto-generated method stub
        sceneManager = stateManager.getState(SceneManager.class);
    }

    @Override
    public void update(float tpf) {
        //To change body of generated methods, choose Tools | Templates.
        if (this._currGameState == GameState.LOADING) {
			// do something...
			
			if (asyncOperation.isDone()) {
				System.out.println("Press the space bar to continue.";
			}
			
			//Output the current progress
			float progress = ((float) asyncOperation.getProgress() / 100);
			System.out.println("Loading progress: " + progress + "%");
			// bitmap.setText(...);
        }
    }
    
    private void onLoadSceneComplete(boolean sceneLoaded) {
        if (!sceneLoaded) {
            System.out.println("An error occurred while loading scene: " + currScene.name);
        }
		
		this._currGameState = GameState.RUNNING;
        System.out.println("loadLevel completed");
    }
    
    private void loadSceneAsync() {
        asyncOperation = sceneManager.loadSceneAsync(currScene);
        asyncOperation.onCompleted(b -> onLoadSceneComplete((boolean) b));
        System.out.println("loading scene: " + currScene.name);
    }
    
    private void onUnloadSceneComplete(boolean sceneUnloaded) {
        if (!sceneUnloaded) {
            System.out.println("An error occurred while unloading scene: " + currScene.name);
        }

		this._currGameState = GameState.PREGAME;
		System.out.println("unloadLevel completed");
    }
    
    private void unloadSceneAsync() {
        asyncOperation = sceneManager.unloadSceneAsync(currScene);
        asyncOperation.onCompleted(b -> onUnloadSceneComplete((boolean) b));
        System.out.println("unloading scene: " + currScene.name);
    }
    
    @Override
    public void onAction(String action, boolean isPressed, float tpf) {
        //To change body of generated methods, choose Tools | Templates.
		if (action.equals(KeyMapping.START_GAME) && isPressed) {
			if (this._currGameState == GameState.PREGAME)
				startGame();
			
		} else if (action.equals(KeyMapping.RESTART_GAME) && isPressed) {
			if (this._currGameState == GameState.RUNNING)
				stopGame();
		}
    }

    public void stopGame() {
        this._currGameState = GameState.LOADING;
        unloadSceneAsync();
    }

    public void startGame() {
        this._currGameState = GameState.LOADING;
        loadSceneAsync();
    }
    
}

The Boot class contains the definition of the elements that make up the various scenes of the game. For convenience I used a java class but it could also be a json.

public class Boot {

    public static final Scene scene_01 = new Scene();
    public static final Scene scene_02 = new Scene();
    
    static {
        scene_01.name = "Scene 1";
        scene_01.systemPrefabs = Arrays.asList(
                PostProcessingAppState.class,
                LevelManager.class,
                ParticleManager.class,
                PlayerManager.class,
                PlayerMovement.class,
                PlayerShooting.class,
                InventoryAppState.class
        );
        
        scene_02.name = "Scene 2";
        scene_02.systemPrefabs = Arrays.asList(...);
    }
}

Limitations of Future

  • It cannot be manually completed
  • You cannot perform further action on a Future’s result without blocking
  • Multiple Futures cannot be chained together
  • You can not combine multiple Futures together
  • No Exception Handling

CompletableFuture implements Future and CompletionStage interfaces and provides a huge set of convenience methods for creating, chaining and combining multiple Futures. It also has a very comprehensive exception handling support.

To find out more about how CompletableFuture works, here is an interesting explanation.

2 Likes

Unity seems to lead people down some really strange patterns.

2 Likes

Also, what is the point of SimpleAppState? Seems entirely unnecessary.

@capdevon

you just use:

//thread start
//load scene code
app.enqueue(() → {
//attach scene to game Node code
});
//thread end

its in wiki there:

I also agree, seems like Unity lead people to strange patterns.

Dont get me wrong @capdevon, i like that you want contribute, but try think more “JME way”.
As you can see its even more simple in some cases than Unity.
I agree its harder in many cases tho too, but IMO core core, is very solid and easy in JME if you know about them. Thats why wiki come here to help, to help understand of possible solutions.

1 Like

I appreciate your help and your positive encouragement @oxplay2. I didn’t like the pointless criticism of @pspeed. Writing the topic cost me time and effort. I didn’t just ask a question and wait for the answer. My intent was to share ideas with detailed examples, in the hope that my research could be help someone. I don’t see many JME tutorials on youtube. Thanks for the sarcasm, it seems users like it more than my attempts to share useful ideas.

1 Like

Dont mind that. Anything you read can be interpreted in any kind of mood, depending on both your own state of mind and the response. The community is open to questions and willing to help out. By posting a pattern here, that mimics Unity, you could potentially mislead others to think that its the right way to do that :slight_smile:

I think the sarcasm you detect is a consequence of a post that bears no questions. There’s no “what do you guys think?” or “is this the right way to do it?”.

Keep up the good energy

2 Likes

I’m sorry it was interpreted that way. You did indeed put a lot of time and effort into converting a “Unity approach” to JME. I wasn’t criticizing you. I was criticizing Unity.

Unity’s monolithic approach to game creation means users must do all kinds of strange things or follow the strange patterns that Unity expects you to follow. That doesn’t mean that those are good ways. Your post still might be helpful to Unity users.

It also might be helpful as a launching point for “What would the JME recommended approach be?” It wasn’t clear to me if you are even open to that discussion.

(When I was trying to help my son learn Unity for a summer camp he was taking, I watched some tutorials and so often found them very painful to watch because sometimes they have to do a bunch of weird stuff to do something simple.)

JME doesn’t really prescribe a particular approach which is why you can use it to redo Unity-like approaches. But there are going to be some “as designed” approaches that will make some things easier.

5 Likes

For additional reference…

BaseAppState is the recommended base class for app states: BaseAppState (jMonkeyEngine3)

From that it is easy to access the application (getApplication()) and thus all of its subparts like InputManager, AssetManager, etc…

It also has a convenient way of looking up other app states:
https://javadoc.jmonkeyengine.org/v3.3.2-stable/com/jme3/app/state/BaseAppState.html#getState-java.lang.Class-
https://javadoc.jmonkeyengine.org/v3.3.2-stable/com/jme3/app/state/BaseAppState.html#getState-java.lang.Class-boolean-

A “well behaved” app state will tend to create long-lived/long-loading stuff in initialize() and release them in terminate() but actually do the scene adjustment in onEnable() (and clean that up in onDisable()). For example, a main menu app state might create the menu UI in initialize() but attach it to the GUI node only in onEnable().

For the “Boot” related stuff in your post, you might consider how that could/would be different with a composite app state like: SiO2/CompositeAppState.java at master · Simsilica/SiO2 · GitHub

ie: could just attach that app state with the children in it and let it manage them.

2 Likes

thanks for the clarification @pspeed , I extend my hand to you for a renewed friendship. I have many other topics that I would like to discuss with the community and I would like to continue to think it is a positive environment. I hope I can count on your help in the future.

7 Likes

It’s always nice to see a new and knowledgeable user join the community. I’m sorry if we got off on the wrong foot.

Edit: in case you missed it, I posted a second reply with some JME pointers. I don’t know exactly how/if that affects your original template but it’s something to think about.

4 Likes

yes I saw, thank you. I take some time to review my solution :wink:

1 Like