Mapping and unmapping function groups

For my current project, I’ve finally started to learn Lemur.

I have an appstate that handles a set of related input functions, specifically the hotkeys I use for debugging. I’ve grouped the functions together using a Lemur input group. In the appstate’s onEnable() and onDisable() methods, I can conveniently (de)activate all functions in the group using InputMapper.activateGroup() and InputMapper.deactivateGroup().

In my initialize() method I want to map all functions in the group. I wound up doing:

        Set<FunctionId> functions = inputMapper.getFunctionIds();
        for (FunctionId function : functions) {
            String group = function.getGroup();
            switch (group) {
                case G_DUMP:
                    inputMapper.addStateListener(this, function);
            }
        }

In my cleanup() method I want to unmap all the functions and remove them from the state listener. I wound up doing:

        InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper();
        Set<FunctionId> functions = inputMapper.getFunctionIds();
        for (FunctionId function : functions) {
            String group = function.getGroup();
            switch (group) {
                case G_DUMP:
                    Set<InputMapper.Mapping> mappings
                            = inputMapper.getMappings(function);
                    for (InputMapper.Mapping mapping : mappings) {
                        inputMapper.removeMapping(mapping);
                    }
                    inputMapper.removeStateListener(this, function);
            }
        }

Seems to me there ought to be simpler ways to accomplish these basic tasks. Is there? And if so, what would it be? Or am I misusing function groups somehow?

I was trying to think about the paths I take because I somehow never needed this yet.

I think two things are at play a little bit:

  1. (right or wrong) I think of the input to function mapping as global. The prtrscrn means FunctionId(“print screen”) is just a global default. Whether that mapping means anything or not is entirely dependent on whether or not something is listening to the function and the group is activated.
  2. I do not think of group → listener as a one-to-one mapping. They have more to do with logical grouping and application modality. So “player controls” might be handled by more than one app state but I still want to enable/disable all “player controls” together.

Beyond that, it’s my own idiom to never use the app state as the listener itself. Old listener coding habit from way back when on large teams some bozo would decide they needed to call the listener method directly from their own code to achieve some end rather than doing the job right. So I either end up with an inner class, a separate reusable object, or most likely just mapping directly to a method.

The method-mapping approach is nice because then you’ve also created an API for directly triggering that same function. If “Esc” is hooked up to openInGameMenu() then even code can decide it wants to open the in game menu just by calling that method.

Consequently, my initialize/onEnable methods tend to look like some combination of:

inputMapper.addDelegate(someFunctionId1, this, "someMethod1");
inputMapper.addDelegate(someFunctionId2, this, "someMethod2");
inputMapper.addStateListener(myListener, functionId1, functionId2, functionId3);
inputMapper.addAnalogListener(myOtherListener, functionId4, functionId5);
inputMapper.activateGroup(myGroup);

Group activation may be in the same method or in the onEnable() method or in a different state entirely.
The reciprocal is done in the appropriate cleanup/onDisable method.

Some of this can be seen ‘in action’ in SiO2’s standard camera input handler:

In your code example:

Set<FunctionId> functions = inputMapper.getFunctionIds();

Already implies that some code registered the input → function ID mappings already and so was already aware of the function IDs. Trying to loop over all of them to then force them to one listener seems to lose some flexibility without really gaining that much brevity.

1 Like

Thanks for the reply.

I wasn’t aware of addDelegate(). Perhaps that’s what I should be using. Is there any documentation for it?

Unfortunately, I haven’t gotten around to javadoc’ing it… but usage is straight forward:

public void addDelegate(FunctionId func,
                        java.lang.Object target,
                        java.lang.String methodName,
                        boolean passArgument)

‘func’ will call the method denoted by ‘methodName’ on ‘target’. It need not be public.

Is ‘passArgument’ is true then it will expect the method denoted by ‘methodName’ to take an InputState argument. It will also get all up/down events.

Actually that method is just a passthrough for the StateMethodDelegate listener implementation which is slightly better documented:
http://jmonkeyengine-contributions.github.io/Lemur/javadoc/Lemur/com/simsilica/lemur/input/StateMethodDelegate.html

1 Like

Thanks. That makes sense.

I’m reluctant to rewrite my UI around delegates because that would defeat a lot of static checks, not to mention automated code refactoring. What would be ideal, I think, would be a simple mechanism to hook a pair of lambdas to each FunctionId: one for the positive transition and one for the negative. Then I could easily eliminate most of my listeners.

1 Like

Or perhaps I should create a distinct listener for each FunctionId…

Either way, it looks like if I organize my functions into groups, I’m expected to keep track of which functions are in each group.

The problem with this is removing them again later. Like anonymous inner classes, you have no handle to go back and clean them up.

That doesn’t necessarily make sense, either. For things like WASD+joystick axes, it’s often simpler to have one listener. (Note that Lemur lets you use the same listener for joystick, WASD, etc. unlike JME default input management.)

I don’t know what static checks you mean but in general the tradeoff for reflection here tends to be worth it. The delegates will ‘fail fast’ in that the method resolution is done at initialization time versus when called… so you’re app will fail during startup if the method has moved/changed. And for me, I find the convenience worth the ‘magic’… these methods tend to change rarely, too.

That being said, I don’t use delegates for everything. They are especially good for wiring up single click things like app state toggles or hot-key actions.

To an extent… groups were meant to let you enable and disable a bunch of functions at once. They weren’t really meant for ‘managing’ some set of actions. The functions that are in a particular group could be scattered across the whole application and may have little to do with one another other than “I want to disable these all at the same time”. It’s usually the case that they tend to be closely related but it’s not always so.

1 Like

Thank you for helping to clarify my thinking.

My pleasure. I like it when smart folks use my libraries and ask questions like this because they make me think about things a different way.

1 Like

I’ve refactored the hotkey/button input system of More Advanced Vehicles with the following objectives:

  1. group related functions together in a single class
  2. simple listeners (one per function, typically) defined using lambda notation
  3. separate the hotkey/button assignments from listeners, to simplify customization

The current solution looks somewhat like this:

/**
 * An AppState which, when enabled, handles state changes for a set of
 * functions. New instances are disabled by default.
 */
abstract class InputMode extends BaseAppState {
    final private Map<Button, FunctionId> buttonToFunction = new HashMap<>(9);
    final private Map<FunctionId, StateFunctionListener> functionToHandler
            = new HashMap<>(25);
    final private Map<Integer, FunctionId> keyToFunction = new HashMap<>(20);

    /**
     * Instantiate a disabled InputMode to handle the specified functions.
     *
     * @param functions the functions to be handled
     */
    protected InputMode(FunctionId... functions) {
        super.setEnabled(false);
        for (FunctionId function : functions) {
            functionToHandler.put(function, null);
        }
    }

    /**
     * Assign the specified function to the specified Button. Allowed only when
     * the mode is disabled. Replaces any function previously assigned to the
     * Button in this mode.
     *
     * @param function the desired function (not null, alias created)
     * @param button the Button (not null, alias created)
     */
    public void assign(FunctionId function, Button button) {
        if (!functionToHandler.containsKey(function)) {
            String message = "Function isn't handled by this mode: " + function;
            throw new IllegalArgumentException(message);
        }
        if (isEnabled()) {
            String message = "Can't modify InputMode while enabled.";
            throw new IllegalStateException(message);
        }

        buttonToFunction.put(button, function);
    }

    /**
     * Assign the specified function to the specified hotkeys. Allowed only when
     * the mode is disabled. Replaces any functions previously assigned to the
     * hotkeys in this mode.
     *
     * @param function the desired function (not null, alias created)
     * @param keyCodes the codes of the hotkeys
     */
    public void assign(FunctionId function, int... keyCodes) {
        if (!functionToHandler.containsKey(function)) {
            String message = "Function isn't handled by this mode: " + function;
            throw new IllegalArgumentException(message);
        }
        if (isEnabled()) {
            String message = "Can't modify InputMode while enabled.";
            throw new IllegalStateException(message);
        }

        for (int keyCode : keyCodes) {
            keyToFunction.put(keyCode, function);
        }
    }

    /**
     * Assign the specified handler to the specified functions. Allowed only
     * when the mode is disabled. Replaces any handlers previously assigned to
     * the functions in this mode.
     *
     * @param handler the desired handler (not null, alias created)
     * @param functions the functions
     */
    final protected void assign(StateFunctionListener handler,
            FunctionId... functions) {
        if (isEnabled()) {
            String message = "Can't modify InputMode while enabled.";
            throw new IllegalStateException(message);
        }

        for (FunctionId function : functions) {
            if (!functionToHandler.containsKey(function)) {
                String message
                        = "Function isn't handled by this mode: " + function;
                throw new IllegalArgumentException(message);
            }
            functionToHandler.put(function, handler);
        }
    }

    @Override
    protected void cleanup(Application application) {
    }

    @Override
    protected void initialize(Application application) {
    }

    @Override
    protected void onDisable() {
        InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper();

        for (Map.Entry<FunctionId, StateFunctionListener> entry
                : functionToHandler.entrySet()) {
            StateFunctionListener listener = entry.getValue();
            if (listener != null) {
                FunctionId function = entry.getKey();
                inputMapper.removeStateListener(listener, function);
            }
        }

        for (Map.Entry<Button, FunctionId> entry
                : buttonToFunction.entrySet()) {
            Button button = entry.getKey();
            FunctionId function = entry.getValue();
            inputMapper.removeMapping(function, button);
        }

        for (Map.Entry<Integer, FunctionId> entry
                : keyToFunction.entrySet()) {
            int keyCode = entry.getKey();
            FunctionId function = entry.getValue();
            inputMapper.removeMapping(function, keyCode);
        }
    }

    @Override
    protected void onEnable() {
        InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper();

        for (Map.Entry<Button, FunctionId> entry
                : buttonToFunction.entrySet()) {
            Button button = entry.getKey();
            FunctionId function = entry.getValue();

            inputMapper.map(function, button);
        }

        for (Map.Entry<Integer, FunctionId> entry
                : keyToFunction.entrySet()) {
            int keyCode = entry.getKey();
            FunctionId function = entry.getValue();

            inputMapper.map(function, keyCode);
        }

        for (Map.Entry<FunctionId, StateFunctionListener> entry
                : functionToHandler.entrySet()) {
            StateFunctionListener listener = entry.getValue();
            if (listener != null) {
                FunctionId function = entry.getKey();
                inputMapper.addStateListener(listener, function);
            }
        }
    }
}
1 Like