NPE caused by lemur on detaching states


#1

In my game, I have two main app states, a menu state and a game state.

When my app is run, the menu app state is attached in simple init app, and 3 Picture spatials are displayed on screen called quit, settings and start.

Using lemurs scene picking (Lemur Gems #3 : Scene picking), I added a DefaultMouseListener to all picutres, so that when they are clicked, and event is exectuted.

On my “start” Picture, I added the following code within the DefaultMouseListener:

@Override
  protected void click(MouseButtonEvent event, Spatial target, Spatial capture) {
           app.getStateManager().attach(new GameBaseAppState(rootNode,guiNode.appSettings));
  }

Within the initialize method of the game app state, I then try to detach the menu app state using the following code:

app.getStateManager().detach(app.getStateManager().getState(MenuBaseAppState.class));

This generates the following exception, which i haven’t been able to debug, becuase it isn’t specific enough about what the problem is.

java.lang.NullPointerException
	at com.simsilica.lemur.event.PickEventSession.getZBounds(PickEventSession.java:394)
	at com.simsilica.lemur.event.PickEventSession.getPickRay(PickEventSession.java:421)
	at com.simsilica.lemur.event.PickEventSession.cursorMoved(PickEventSession.java:548)
	at com.simsilica.lemur.event.MouseAppState.dispatchMotion(MouseAppState.java:94)
	at com.simsilica.lemur.event.BasePickState.update(BasePickState.java:239)
	at com.jme3.app.state.AppStateManager.update(AppStateManager.java:287)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:236)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:151)
	at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:197)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:232)
	at java.lang.Thread.run(Thread.java:748)

Does anyone know what may have caused this? Am I not doing something correctly in the cleanup for the menu app state, like destroying the lemur components?

my code for cleanup() so far is this:

//startdm, etcl is a new DefaultMouseListener() which is declared earlier and also shown above.
        MouseEventControl.removeListenersFromSpatial(start, startdml);
        MouseEventControl.removeListenersFromSpatial(set, setdml);
        MouseEventControl.removeListenersFromSpatial(qt, qtdml);        
                
        guiNode.detachAllChildren();

Sorry if this is super vague, I am struggling to understand what the cause is.


#2

There is a “bug” in JME where it returns null for the bounds of empty nodes. This exception indicates that the node that is being picked (likely the GUI node) is completely empty.

Adding the stats app state during startup (even if you disable the stats) is enough to work around this bug as it makes sure there is always at least one thing in the gui node.


#3

I’ve re-enabled stats, and also added a bitmap text (with an empty string) to the guiNode, however I still encounter this error.
I stripped down the code, so all that happens in the menu app state is that it loads the Pictures, then immediately the state detaches itself. It produces the exact same error.

Is there any code that trys to “get” the bounds of the guiNode when the state is detached?


#4

Here is the code, if you wish to inspect it, excuse the messiness.

public class MenuBaseAppState extends BaseAppState {
    
    private final Node rootNode;
    private final Node guiNode;
    private final AppSettings settings;
    private AssetManager am;
    private Camera cam;
    private final int w;
    private final int h;//0,h is the uppper left corner.
    private Application app;
    private static final Logger LOGGER = Logger.getLogger(MenuBaseAppState.class.getName());
    
    private Picture start, set, qt;
    
    private final DefaultMouseListener startdml = new DefaultMouseListener() {
                    @Override
                        public void mouseEntered( MouseMotionEvent event, Spatial target, Spatial capture ) {
                            Picture p = (Picture) target;
                            p.setImage(am, LemurSettings.STARTO, true);//these are just references to paths stored as strings
                        }

                    @Override
                        public void mouseExited( MouseMotionEvent event, Spatial target, Spatial capture ) {
                            Picture p = (Picture) target;
                            p.setImage(am, LemurSettings.START, true);
                        }
                    @Override
                        protected void click(MouseButtonEvent event, Spatial target, Spatial capture) {
                           app.getStateManager().attach(new GameBaseAppState(rootNode,guiNode,settings));    
            
                        }
                    }, setdml =  
                    new DefaultMouseListener() {
                    @Override
                        public void mouseEntered( MouseMotionEvent event, Spatial target, Spatial capture ) {
                            Picture p = (Picture) target;
                            p.setImage(am, LemurSettings.SETTINGSO, true);
                        }

                    @Override
                        public void mouseExited( MouseMotionEvent event, Spatial target, Spatial capture ) {
                            Picture p = (Picture) target;
                            p.setImage(am, LemurSettings.SETTINGS, true);
                        }
                    @Override
                        protected void click(MouseButtonEvent event, Spatial target, Spatial capture) {
                            
                        } 
                    }, qtdml  =
                    new DefaultMouseListener() {
                    @Override
                        public void mouseEntered( MouseMotionEvent event, Spatial target, Spatial capture ) {
                            Picture p = (Picture) target;
                            p.setImage(am, LemurSettings.QUITO, true);
                        }

                    @Override
                        public void mouseExited( MouseMotionEvent event, Spatial target, Spatial capture ) {
                            Picture p = (Picture) target;
                            p.setImage(am, LemurSettings.QUIT, true);
                        }
                    @Override
                        protected void click(MouseButtonEvent event, Spatial target, Spatial capture) {
                            app.stop();
                            System.exit(0);
                        } 
                    };
    
    public MenuBaseAppState (Node rootNode, Node guiNode, AppSettings settings) {  
        this.rootNode = rootNode;          
        this.guiNode = guiNode;        
        this.settings = settings;
        
        w = settings.getWidth();
        h = settings.getHeight();
            }
    
    @Override
    protected void initialize(Application app) {
        //It is technically safe to do all initialization and cleanup in the         
        //onEnable()/onDisable() methods. Choosing to use initialize() and         
        //cleanup() for this is a matter of performance specifics for the         
        //implementor.        
        //TODO: initialize your AppState, e.g. attach spatials to rootNode    
        Logger.getLogger("").setLevel(Level.SEVERE);
        
        this.app = app;//for access by other components
        this.am = app.getAssetManager();
        this.cam = app.getCamera();
   
            initMenuGui();
 
    }
    
    @Override    
    protected void cleanup(Application app) {
        //TODO: clean up what you initialized in the initialize method,        
        //e.g. remove all spatials from rootNode    
        
        //System.out.println("Detaching Menu App State");
        
        MouseEventControl.removeListenersFromSpatial(start, startdml);
        MouseEventControl.removeListenersFromSpatial(set, setdml);
        MouseEventControl.removeListenersFromSpatial(qt, qtdml);
        

                
        guiNode.detachAllChildren();
        
    }
   
    //onEnable()/onDisable() can be used for managing things that should     
    //only exist while the state is enabled. Prime examples would be scene     
    //graph attachment or input listener attachment.    
    @Override  
    protected void onEnable() {
        //Called when the state is fully enabled, ie: is attached and         
        //isEnabled() is true or when the setEnabled() status changes after the         
        //state is attached.  
        
        app.getStateManager().detach(this);
    }
    
    @Override
    protected void onDisable() {
        //Called when the state was previously enabled but is now disabled         
        //either because setEnabled(false) was called or the state is being         
        //cleaned up.    
    }

   
    
    @Override
    public void update(float tpf) {
        //.    
    }
    
    
    private void initMenuGui() {// Note: Lemur GUI elements grow down from the upper left corner.            
            Label title = new Label(Settings.TITLE + " by " + LemurSettings.AUTHOR);
            guiNode.attachChild(title);            
            title.setLocalTranslation(6, title.getFontSize()*1.5f, 0);
            
            Picture logo = new Picture("logo");
            logo.setImage(am, LemurSettings.TITLE_LOGO, true);
            float wp = w/1920f, hp = h/1080f;//resolution scaling params, so far only works on 16:9
            float lw = LemurSettings.LOGO_WIDTH*wp, lh = LemurSettings.LOGO_HEIGHT*hp;
            //positions center and down from the top
            logo.setPosition((w/2)-(lw/2),((float) h-(lh))*0.9765f);
            logo.setHeight(lh);
            logo.setWidth(lw);
            guiNode.attachChild(logo);
            
            start = new Picture("start");
            start.setImage(am, LemurSettings.START, true);
            float wps = w/1920f, hps = h/1080f;//resolution scaling params, so far only works on 16:9
            final float lws = LemurSettings.LOGO_WIDTH*wps*.5f, lhs = LemurSettings.LOGO_HEIGHT*hps*.5f;
            //positions center and down from the top
            start.setPosition((w/2)-(lws/2),((float) h-(lhs))*0.55f);
            start.setHeight(lhs);
            start.setWidth(lws);
            MouseEventControl.addListenersToSpatial(start,startdml);
            guiNode.attachChild(start);
            
            set = new Picture("set");
            set.setImage(am, LemurSettings.SETTINGS, true);
            float wpt = w/1920f, hpt = h/1080f;//resolution scaling params, so far only works on 16:9
            final float lwt = LemurSettings.LOGO_WIDTH*wpt*.5f, lht = LemurSettings.LOGO_HEIGHT*hpt*.5f;
            //positions center and down from the top
            set.setPosition((w/2)-(lwt/2),((float) (h-(lhs))*0.40f));
            set.setHeight(lht);
            set.setWidth(lwt);
            MouseEventControl.addListenersToSpatial(set,setdml);
            guiNode.attachChild(set);
            
            qt = new Picture("qt");
            qt.setImage(am, LemurSettings.QUIT, true);
            float wpq = w/1920f, hpq = h/1080f;//resolution scaling params, so far only works on 16:9
            final float lwq = LemurSettings.LOGO_WIDTH*wpq*.5f, lhq = LemurSettings.LOGO_HEIGHT*hpq*.5f;
            //positions center and down from the top
            qt.setPosition((w/2)-(lwq/2),((float) (h-(lhs))*0.25f));
            qt.setHeight(lhq);
            qt.setWidth(lwq);
            MouseEventControl.addListenersToSpatial(qt,qtdml);
            guiNode.attachChild(qt);
    }


#5

I’m telling you, some pick root node is empty. It could be the gui node. It could be the app’s root node… but some node is empty. So JME will return a null bounds in this case and Lemur will throw an NPE.

So on the one hand, this is easy to fix in Lemur by adding “yet another null check” for this JME bug… but I think it’s truly a bug in JME that it returns null for empty nodes that have a position. And that bug has been fixed in JME master already.

In the mean time, unless you want to run from JME master, make sure your root nodes (GUI or otherwise) are never empty.


#6

Right, I understand now. I dug through some code in my game app state, and I realised that a guiNode.detachAllChildren(); is called when the HUD is created, I changed it to only detach spatials from the menu app state, and now the issue is resolved :smiley:


#7

As a rule, app states (especially GUI ones) should:

  1. extends BaseAppState
  2. create stuff in initialize() or onEnable()
  3. add stuff to the GUI/scene in onEnable(), add global listeners, etc.
  4. remove just the stuff they added in onDisable(), remove gloval listeners, etc.
  5. in cleanup(), cleanup whatever they did in initialize() that should be cleaned up.
  6. upon removal, make sure that the app is essentially in the same state it was in before they were first attached

If you follow these rules then your life will be easier and your app states will more likely be more reusable (though that’s another topic).


#8

That’s what I was going for yes, but I obviously went too over the top on resetting the app back to a blank state.

Many thanks for your help this evening!


#9

Ahah! But “blank state” is different than “state when the app state was attached”. An important distinction to keep in mind… only undo what you’ve done. :slight_smile: