InputMapper and AppState Removal

Although I’ve found a solution, I’m not sure if it’s a bug or intended behaviour, so for my own education if anything else, so I understand why and avoid this situation again, it would be much appreciated if anyone could explain this behaviour.

I was experiencing an issue when I switched between vehicle controls and player controls.

enter vehicle: disable character controls, enable vehicle controls.
exit vehicle: disable vehicle controls, enable character controls.

Below are three appstates: the main example (for encapsulation) and two inner states that switch between each other. Pressing A and S switches between the inner states.

This is the line that causes and solves the issue.

private InputState inputState = InputState.Positive;

If the inputState value = InputState.Positive then the keybindings do not appear to be removed and will result in the println being printed the same amount of times it’s been added - meaning they don’t actually get removed. This is the result of switching them six times.

Enabled 1
Enabled 2
Enabled 1
Enabled 2
Enabled 2
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 2
Enabled 2
Enabled 2
Enabled 2
Enabled 2
Enabled 2
Enabled 2
Enabled 2
Enabled 2
Enabled 2
Enabled 2
Enabled 2
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1
Enabled 1

If the inputState value = InputState.Off it works as intended. The keys are unregistered and behave as intended. The println’s display what I would expect. This is the result of switching them six times.

Enabled 1
Enabled 2
Enabled 1
Enabled 2
Enabled 1
Enabled 2
Enabled 1

Here is a test case that exposes the issue.

To clarify: changing the variable inputState to InputState.Positive causes the repeating issue, and changing the inputState variable to InputState.Off causes the correct (expected) behaviour.

package com.jayfella.modular.debug;

import com.jme3.app.Application;
import com.jme3.app.state.BaseAppState;
import com.jme3.input.KeyInput;
import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.input.FunctionId;
import com.simsilica.lemur.input.InputMapper;
import com.simsilica.lemur.input.InputState;
import com.simsilica.lemur.input.StateFunctionListener;

public class TestLemurMapping extends BaseAppState {

    private static final String GROUP_NAME_1 = "G_STATE_ONE";
    private static final FunctionId F_1 = new FunctionId(GROUP_NAME_1, "Function 1");

    private static final String GROUP_NAME_2 = "G_STATE_TWO";
    private static final FunctionId F_2 = new FunctionId(GROUP_NAME_2, "Function 2");

    private InputMapper inputMapper;

    private InputState inputState = InputState.Positive;

    public TestLemurMapping() {

    }

    @Override
    protected void initialize(Application app) {
        GuiGlobals.initialize(app);
        this.inputMapper = GuiGlobals.getInstance().getInputMapper();

    }

    @Override
    protected void cleanup(Application app) {

    }

    @Override
    protected void onEnable() {
        getStateManager().attach(new StateOne());
    }

    @Override
    protected void onDisable() {
        
    }

    private class StateOne extends BaseAppState implements StateFunctionListener {

        @Override
        protected void initialize(Application app) {

            inputMapper.map( F_1 , KeyInput.KEY_A);
            inputMapper.addStateListener(this, F_1);
            
        }

        @Override
        protected void cleanup(Application app) {
            
            inputMapper.removeMapping( F_1, KeyInput.KEY_A );
            inputMapper.removeStateListener(this, F_1);
            
        }

        @Override
        protected void onEnable() {
            
            inputMapper.activateGroup( GROUP_NAME_1 );
            System.out.println("Enabled 1");
            
        }

        @Override
        protected void onDisable() {
            inputMapper.deactivateGroup( GROUP_NAME_1 );
        }


        @Override
        public void valueChanged(FunctionId func, InputState value, double tpf) {

            if (func == F_1 && value == inputState) {
                
                getStateManager().detach(this);
                getStateManager().attach(new StateTwo());
                
            }

        }

    }

    private class StateTwo extends BaseAppState implements StateFunctionListener {

        @Override
        protected void initialize(Application app) {
            
            inputMapper.map( F_2 , KeyInput.KEY_S);
            inputMapper.addStateListener(this, F_2);
            
        }

        @Override
        protected void cleanup(Application app) {
            
            inputMapper.removeMapping( F_2, KeyInput.KEY_S );
            inputMapper.removeStateListener(this, F_2);
            
        }

        @Override
        protected void onEnable() {
            
            inputMapper.activateGroup( GROUP_NAME_2 );
            System.out.println("Enabled 2");
            
        }

        @Override
        protected void onDisable() {
            inputMapper.deactivateGroup( GROUP_NAME_2 );
        }

        @Override
        public void valueChanged(FunctionId func, InputState value, double tpf) {

            if (func == F_2 && value == inputState) {
                
                getStateManager().detach(this);
                getStateManager().attach(new StateOne());
                
            }

        }

    }

}


I suspect if you added system out printlns to valueChanged() you might better see what’s going on.

The issue is that when you are holding down a key there are repeated events being send that the key is pressed. There is only ever one release. So internally there are many InputState.Positive and only one InputState.Off.

InputMapper will debounce these for a particular function. In this case, you have two functions.

First down event:
-Function1 receives the down, turns itself off, and enables function2.
Second down event:
-Function2 never saw a down so receives the new down event, turns itself off, and enables function1
…repeat as long as key is pressed.

It’s not really a bug.

Without knowing more specifics, it seems a bit unnatural to enable/disable controls on a down event anyway.

There’s no specific reason for calling it when the button is down, I just wanted it to be a bit more “reactive” - but it’s no problem. If it were a button on a joypad for example, one would expect the action to occur when pressed, not depressed, but to be honest it’s no biggie.

Thanks for the clarification.

You can still do it that way but you will have to debounce it yourself.

Don’t allow switching controls unless the previous switch has finished… and don’t ‘finish’ the switch until the key is released. ie: just set a transition flag = value and add that check to your current if statement. Set the transition flag to value either way.

Edit: alternately, you could make entering and exiting the vehicle different keys… I’ve seen that also.