11
\$\begingroup\$

I am using an event-based input handling system with events and delegates. An example:

InputHander.AddEvent(Keys.LeftArrow, player.MoveLeft); //Very simplified code

However, I started wondering about how to deal with 'nested' input. For example in Half-Life 2 (or any Source game, really), you can pick up items with E. When you've picked up an item, you cannot fire with Left Mouse, but instead you throw the object. You can still jump with Space.

(I am saying nested input is where you press a certain key, and the actions you can do change. Not a menu.)

The three cases are:

  • Being able to do the same action as before (like jumping)
  • Not being able to do the same action (like firing)
  • Doing a different action entirely (like in NetHack, where pressing the open door key means you do not move, but select a direction to open the door in)

My original idea was to just change it after the input had been received:

Register input 'A' to function 'Activate Secret Cloak Mode'

In 'Secret Cloak Mode' function:
Unregister input 'Fire'
Unregister input 'Sprint'
...
Register input 'Uncloak'
...

This suffers from large amounts of coupling, repetitive code, and other bad design signs.

I guess the other option is to maintain some kind of input state system - perhaps another delegate on the register function to refactor out those numerous register/deregister into a cleaner place (with some kind of stack on the input system) or maybe arrays of what to keep and what not to.

I'm sure someone here must've encountered this problem. How have you solved it?

tl;dr How can I deal with specific input received after another specific input in an event system?

\$\endgroup\$

5 Answers 5

7
+50
\$\begingroup\$

two options: if "nested input" cases are at most three, four, I'd just use flags. "Holding an object? Can't fire." Anything else is overengineering it.

Otherwise, you can keep a per-input-key stack of event handlers.

Actions.Empty = () => { return; };
if(IsPressed(Keys.E)) {
    keyEventHandlers[Keys.E].Push(Actions.Empty);
    keyEventHandlers[Keys.LeftMouseButton].Push(Actions.Empty);
    keyEventHandlers[Keys.Space].Push(Actions.Empty);
} else if (IsReleased(Keys.E)) {
    keyEventHandlers[Keys.E].Pop();
    keyEventHandlers[Keys.LeftMouseButton].Pop();
    keyEventHandlers[Keys.Space].Pop();        
}

while(GetNextKeyInBuffer(out key)) {
   keyEventHandlers[key].Invoke(); // we invoke only last event handler
}

Or something to this effect :)

Edit: someone mentioned unmanageable if-else constructs. are we going to go full data-driven for an input event handling routine? You surely could, but why?

Anyway, for the heck of it:

void BuildOnKeyPressedEventHandlerTable() {
    onKeyPressedHandlers[Key.E] = () => { 
        keyEventHandlers[Keys.E].Push(Actions.Empty);
        keyEventHandlers[Keys.LeftMouseButton].Push(Actions.Empty);
        keyEventHandlers[Keys.Space].Push(Actions.Empty);
    };
}

void BuildOnKeyReleasedEventHandlerTable() {
    onKeyReleasedHandlers[Key.E] = () => { 
        keyEventHandlers[Keys.E].Pop();
        keyEventHandlers[Keys.LeftMouseButton].Pop();
        keyEventHandlers[Keys.Space].Pop();              
    };
}

/* get released keys */

foreach(var releasedKey in releasedKeys)
    onKeyReleasedHandlers[releasedKey].Invoke();

/* get pressed keys */
foreach(var pressedKey in pressedKeys) 
    onKeyPressedHandlers[pressedKey].Invoke();

keyEventHandlers[key].Invoke(); // we invoke only last event handler

Edit 2

Kylotan mentioned key mapping, which is a basic feature every game should have (think about accessibility, too). Including keymapping is a different story.

Changing behaviour depending on a key press combination or sequence is limiting. I overlooked that part.

Behaviour is related to game logic and not input. Which is fairly obvious, coming to think of it.

Therefore, I'm proposing the following solution:

// //>

void Init() {
    // from config file / UI
    // -something events should be set automatically
    // quake 1 ftw.
    // name      family         key      keystate
    "+forward" "movement"   Keys.UpArrow Pressed
    "-forward"              Keys.UpArrow Released
    "+shoot"   "action"     Keys.LMB     Pressed
    "-shoot"                Keys.LMB     Released
    "jump"     "movement"   Keys.Space   Pressed
    "+lstrafe" "movement"   Keys.A       Pressed
    "-lstrafe"              Keys.A       Released
    "cast"     "action"     Keys.RMB     Pressed
    "picknose" "action"     Keys.X       Pressed
    "lockpick" "action"     Keys.G       Pressed
    "+crouch"  "movement"   Keys.LShift  Pressed
    "-crouch"               Keys.LShift  Released
    "chat"     "user"       Keys.T       Pressed      
}  

void ProcessInput() {
    var pk = GetPressedKeys();
    var rk = GetReleasedKeys();

    var actions = TranslateToActions(pk, rk);
    PerformActions(actions);
}                

void TranslateToActions(pk, rk) {
    // use what I posted above to switch actions depending 
    // on which keys have been pressed
    // it's all about pushing and popping the right action 
    // depending on the "context" (it becomes a contextual action then)
}

actionHandlers["movement"] = (action, actionFamily) => {
    if(player.isCasting)
        InterruptCast();    
};

actionHandlers["cast"] = (action, actionFamily) => {
    if(player.isSilenced) {
        Message("Cannot do that when silenced.");
    }
};

actionHandlers["picknose"] = (action, actionFamily) => {
    if(!player.canPickNose) {
        Message("Your avatar does not agree.");
    }
};

actionHandlers["chat"] = (action, actionFamily) => {
    if(player.isSilenced) {
        Message("Cannot chat when silenced!");
    }
};

actionHandlers["jump"] = (action, actionFamily) => {
    if(player.canJump && !player.isJumping)
        player.PerformJump();

    if(player.isJumping) {
        if(player.CanDoubleJump())
            player.PerformDoubleJump();
    }

    player.canPickNose = false; // it's dangerous while jumping
};

void PerformActions(IList<ActionEntry> actions) {
    foreach(var action in actions) {
        // we pass both action name and family
        // if we find no action handler, we look for an "action family" handler
        // otherwise call an empty delegate
        actionHandlers[action.Name, action.Family]();    
    }
}

// //<

This could be improved in many ways by people smarter than me, but I believe it's also a good starting point.

\$\endgroup\$
4
  • \$\begingroup\$ Works fine for simple games - now how are you going to code an key-mapper for this? :) That alone can be a good enough reason to go data-driven for your input. \$\endgroup\$
    – Kylotan
    Jun 8, 2011 at 19:11
  • \$\begingroup\$ @Kylotan, that's a good observation indeed, I'm going to edit my answer. \$\endgroup\$
    – raine
    Jun 8, 2011 at 19:54
  • \$\begingroup\$ This is a great answer. Here, have the bounty :P \$\endgroup\$ Jun 14, 2011 at 10:43
  • \$\begingroup\$ @The Communist Duck - thanks chap, hope this helps. \$\endgroup\$
    – raine
    Jun 14, 2011 at 19:27
11
\$\begingroup\$

We used a state system, as you mentioned before.

We would create a map that would contain all the keys for a specific state with a flag that would allow pass through of previously mapped keys or not. When we changed states the new map would be pushed on or a previous map would be popped off.

Quick simple example of input states would be Default, In-Menu and Magic-Mode. Default is where you are running around and playing the game. In-Menu would be when you are at the start menu, or when you have opened up a shop menu, the pause menu, an options screen. In-Menu would contain the no pass through flag because as you navigate a menu you do not want your character moving around. On the other side, much like your example with the carrying of the item, the Magic-Mode would simply remap the action/item use keys to instead cast spells (we would also tie that to sound and particle effects but that is a bit beyond your question).

What causes the maps to be pushed and popped is up to you, and I also will honestly say is we had certain 'clear' events to make sure the map stack was kept clean, level loading being the most obvious time (Cutscenes as well at times).

Hope this helps.

TL;DR - Use states and an input map that you can push and/or pop. Include a flag to say whether or not the map completely removes previous input or not.

\$\endgroup\$
2
  • 5
    \$\begingroup\$ THIS. Pages and pages of nested if statements are the devil. \$\endgroup\$ Jun 6, 2011 at 5:37
  • \$\begingroup\$ +1. When I think on input, I always have Acrobat Reader in mind - Select Tool, Hand Tool, Marquee Zoom. IMO, using a stack might be overkill at times. GEF hides this via AbstractTool. JHotDraw has a nice hierarchy view of their Tool implementations. \$\endgroup\$ Jan 28, 2014 at 9:37
2
\$\begingroup\$

This looks like a case where inheritance could solve your problem. You could have a base class with a bunch of methods that implement the default behavior. You could then extends this class and override some methods. Switching mode is then just a matter of switching the current implementation.

Here's some pseudo-code

class DefaultMode
    function handle(key) {/* call the right method based on the given key. */}
    function run() { ... }
    function pickup() { ... }
    function fire() { ... }


class CarryingMode extends DefaultMode
      function pickup() {} //empty method, so no pickup action in this mode
      function fire() { /*throw object and switch to DefaultMode. */ }

This is similar to what James proposed.

\$\endgroup\$
0
\$\begingroup\$

I'm not writing the exact code in any particular language. I'm giving you the idea.

1) Map your key actions to your events.

(Keys.LeftMouseButton, left_click_event), (Keys.E, e_key_event), (Keys.Space, space_key_event)

2) Assign/Modify your events as given below

def left_click_event = fire();
def e_key_event = pick_item();
def space_key_event = jump();

pick_item() {
 .....
 left_click_action = throw_object();
}

throw_object() {
 ....
 left_click_action = fire();
}

fire() {
 ....
}

jump() {
 ....
}

Let your jump action remain decoupled with other events like jump and fire.

Avoid if..else.. conditional checks here as it'd lead to unmanageable code.

\$\endgroup\$
5
  • \$\begingroup\$ This doesn't seem to aid me at all. It has the problem of high levels of coupling and seems to have repetitive code. \$\endgroup\$ Jun 4, 2011 at 14:01
  • \$\begingroup\$ Just to get a better understanding - Can you explain what is "repetitive" in this and where do you see "high levels of coupling". \$\endgroup\$
    – inRazor
    Jun 4, 2011 at 15:27
  • \$\begingroup\$ If you see my code example, I have to remove all the actions in the function itself. I am hardcoding it all to the function itself - what if two functions want to share the same register/unregisters? I will either have to duplicate code OR have to couple them. Also there would be a large amount of code to remove all the unwanted actions. Lastly, I would have to have some place that 'remembers' all the original actions to replace them. \$\endgroup\$ Jun 4, 2011 at 15:30
  • \$\begingroup\$ Can you give me two of the actual functions from your code (like the secret cloak function) with the complete register/deregister statements. \$\endgroup\$
    – inRazor
    Jun 4, 2011 at 15:49
  • \$\begingroup\$ I don't actually have code for those actions at this time. However, the secret cloak would require things such as fire, sprint, walk, and change weapon to be unregistered, and uncloak to be registered. \$\endgroup\$ Jun 4, 2011 at 15:58
0
\$\begingroup\$

Instead of unregistering, just make a state and then re-register.

In 'Secret Cloak Mode' function:
Grab the state of all bound keys- save somewhere
Unbind all keys
Re-register the keys/etc that we want to do.

In `Unsecret Cloak Mode`:
Unbind all keys
Rebind the state that we saved earlier.

Of course, extending this simple idea would be that you might have separate states for moving, and such, and then instead of dictating "Well, here's all the things I can't do whilst in Secret Cloak Mode, here's all the things I can do in Secret Cloak Mode.".

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .