Wiring physical devices to abstract inputs

In an earlier post, we discussed different methods of gathering keyboard input in Windows. Today, we will cover how multi-platform and multi-device input can be handled in a straightforward way, showing a very efficient implementation of the underlying parts.

Before we can start wiring physical devices to logical controllers, we need to cover some of the lower level parts first. Molecule’s low-level input API consists of several physical devices available to each platform. For example, on Windows the available devices would be keyboard (using Raw Input), mouse (using Raw Input and Windows Messages), joysticks/gamepads (using DirectInput) and Xbox360 controllers (using XInput).

Physical devices

Each of those devices is exposed by its corresponding class, which can directly be used by clients if they have no need for higher level structures:

Keyboard keyboard;
Mouse mouse;
Joystick joystick(0);
Xbox360Controller controller(0);

if (controller.WentDown(Xbox360Controller::Button::A))
  // action

if (mouse.IsDoubleClicked(Mouse::Button::MIDDLE))
  // other action

if (keyboard.IsPressed(Keyboard::Key::ENTER))
  // yet another action

Some important properties about these classes should be noted:

  • Each of these classes offers IsPressed(), WentDown() and WentUp() methods, all sharing the exact same signature – this becomes important when wiring inputs to higher level structures.
  • None of these implements a virtual interface, or is implemented as a singleton. There can be more than one keyboard/mouse attached, and the same is of course true for joysticks and gamepads.

The latter is especially important, because interfaces (base classes and virtual functions) would just add unneccessary overhead, without solving any real problems. Just because each of the classes offers methods sharing the same name and signature doesn’t mean they have to be squeezed into an interface!

Furthermore, keep in mind that even though the devices (and their corresponding class names) are different, you can already build a multi-platform solution if all you’re interested in are simple buttons (or combinations thereof):

#if _WINDOWS
  typedef Keyboard MenuDevice;
#elif _XBOX360
  typedef Xbox360Controller MenuDevice;
#endif

ConfigSettingInt g_menuBackButton("input_menuBack", "Exits the current menu.", someConfiguredValue);

// initialize the device somewhere
MenuDevice menuDevice;

if (menuDevice.WentDown(g_menuBackButton))
  // action

The above already enables you to use different devices on different platforms, and have configurable input layouts as well – without ever introducing any interfaces or similar.

Of course, as soon as we start dealing with more complicated input configurations, we need something which allows us to tie different devices and their possible inputs into a common, platform-independent structure – enter logical devices.

Logical devices

A logical device serves the following purposes:

  • “Abstract” inputs can be added to a logical device. These inputs can be any of the physical device inputs, or completely arbitrary, user-defined inputs (e.g. gesture detection).
  • The state of each of these inputs can be checked via the logical device, with each of the inputs having an identifier of some kind.
  • The logical device memorizes the state of each input at the beginning of a frame. Checking for toggled inputs should be a simple lookup-and-compare, without having to jump through any additional hoops.

Let us cover each of these points one by one.

Abstract inputs

How can we store inputs from different physical devices in a logical device, and allow user-defined inputs as well? Some user inputs might need to access the keyboard, others need to access the mouse, others need both – same for controllers on any of the consoles.

Of course there is no type in C++ that allows us to achieve the above, but we can use function pointers for that. An abstract input in Molecule is nothing more than any function exhibiting the following signature:

bool MyInputFunction(const LogicalDevice* device);

The function simply has to return true when an input is triggered, and false when it isn’t. Because the LogicalDevice provides access to the underlying physical devices, we can easily use any of the available devices on the platform we’re dealing with. For example, simply checking for a button combination could look something like this:

bool CheckMenuBack(const LogicalDevice* device)
{
#if _WINDOWS
  // configurable key
  if (device->GetKeyboard()->WentDown(g_menuBack) ||
      device->GetKeyboard()->IsPressed(g_menuBack2))
    return true;
#elif _XBOX360
  // non-configurable button
  if (device->GetController(0)->WentDown(Controller::Button::B))
    return true;
#endif
}

Of course, you can check for arbitrarily complex inputs such as gesture recognition, because you’re allowed to do pretty much anything you want inside your implementation.

Adding abstract inputs to a logical device is as simple as calling AddInput(), handing it a pointer to a function (or a delegate):

logicalDevice.AddInput(&CheckMenuBack);

Input identifiers

After having added several inputs to one of our logical devices, we need to be able to check whether they were toggled or not. But how can we identify both inputs provided by physical devices, as well as user-defined ones?

One solution I have seen used in the past is using strings as an identifier, but this is both inefficient (string compares) and error-prone (typos in identifiers are hard to find), so clearly not something I wanted to use in Molecule.

An alternative, somewhat better solution is to use enums – by putting the enumerators for each possible input inside a common namespace, the user is able to extend the available inputs. However, because the enumerators must be visible to the user, each change in a header triggers a recompile, and adding new inputs is error-prone as well if their values are stored on disk somewhere.

There is a better solution, which is neither error-prone nor subject to recompiles, and is very efficient. Whenever a new input is added to the logical device, an identifier for the given input is returned. Internally, this identifier is nothing more than an integer which points directly into the array of inputs – hence, checking whether an input is toggled is a simple lookup-and-compare operation.

Let me show you an example in order to understand what I am talking about:

Controller::ID chordID = logicalDevice.AddInput(&ComplicatedChord);
Controller::ID gestureID = logicalDevice.AddInput(&CheckGesture);

// at this point, the logical device internally stores an array with two entries.
// chordID = 0, and gestureID = 1.

if (logicalDevice.IsTriggered(chordID))
  // action

if (logicalDevice.IsTriggered(gestureID))
  // action

IsTriggered() is an inline function doing the following:

inline bool LogicalDevice::IsTriggered(ID id) const
{
  return m_inputResults[id];
}

So the lookup is merely an array access (no hash table or map or similar) using the given ID – it doesn’t get any simpler than that.

Chords and aliased/mapped inputs

There are several types of inputs which are very common in games: chords (button combinations) and aliased/mapped inputs (different buttons or combinations which trigger the same action). Because those are so common, having to write a function each time we want to check some simple inputs quickly becomes boring and tiresome.

So let us try to write some helper functions which take care of the work, and can easily be “configured” by the user.

Chord helpers

Any chord can be checked by just &&-ing the result of several input functions together, as in the following examples:

// two-input chord
bool Chord(const LogicalDevice* device)
{
  if (((Function1)(device)) && ((Function2)(device)))
    return true;

  return false;
};

// three-input chords
bool Chord(const LogicalDevice* device)
{
  if (((Function1)(device)) && ((Function2)(device)) && ((Function3)(device)))
    return true;

  return false;
};

// other chords omitted...

As can be seen, the pattern is always the same. But how can the functions actually checking the inputs (Function1, Function2 and Function3 in the code above) be configured? By using non-type template arguments, like we did for our delegates and events:

template <bool (*Function1)(const LogicalDevice*), bool (*Function2)(const LogicalDevice*)>
bool Chord(const LogicalDevice* device)
{
  if (((Function1)(device)) && ((Function2)(device)))
    return true;

  return false;
};

template <bool (*Function1)(const LogicalDevice*), bool (*Function2)(const LogicalDevice*), bool (*Function3)(const LogicalDevice*)>
bool Chord(const LogicalDevice* device)
{
  if (((Function1)(device)) && ((Function2)(device)) && ((Function3)(device)))
    return true;

  return false;
};

Each template argument accepted by the Chord() function has to exhibit the same signature like abstract inputs – this allows us to build so-called forwarding functions, which can in turn be used by the user to configure his chords.

Let us quickly take a look at those forwarding functions, and a later example will make it clear:

namespace Key
{
  template <Keyboard::Key::Enum KEY>
  ME_INLINE bool IsPressed(const LogicalDevice* device)
  {
    return device->GetKeyboard()->IsPressed(KEY);
  }

  template <Keyboard::Key::Enum KEY>
  ME_INLINE bool WentDown(const LogicalDevice* device)
  {
    return device->GetKeyboard()->WentDown(KEY);
  }

  template <Keyboard::Key::Enum KEY>
  ME_INLINE bool WentUp(const LogicalDevice* device)
  {
    return device->GetKeyboard()->WentUp(KEY);
  }
}

namespace MouseButton
{
  template <Mouse::Button::Enum BUTTON>
  ME_INLINE bool IsPressed(const LogicalDevice* device)
  {
    return device->GetMouse()->IsPressed(BUTTON);
  }

  template <Mouse::Button::Enum BUTTON>
  ME_INLINE bool WentDown(const LogicalDevice* device)
  {
    return device->GetMouse()->WentDown(BUTTON);
  }

  template <Mouse::Button::Enum BUTTON>
  ME_INLINE bool WentUp(const LogicalDevice* device)
  {
    return device->GetMouse()->WentUp(BUTTON);
  }
}

Each of the forwarding functions takes either a keyboard key, mouse button, or joystick/gamepad button as template argument. This is important – we need to pass these as template arguments, because if they had been ordinary function arguments, the signature of the forwarding functions would no longer match the ones expected by the Chord() templates!

Another thing to keep in mind is that we need to define a forwarding function for each of the physical devices, because they all need to call different methods. The above just shows the implementation for keyboard and mouse – joysticks and gamepads have a similar implementation.

In case this was hard to follow, here is an example of how the Chord() template helpers can be used:

Controller::ID chordID = logicalDevice.AddInput
(
  &Chord
  <
    &Key::IsPressed<Keyboard::Key::LEFT_CONTROL>,
    &ConfigKey::WentDown<&g_myKey>
  >
);

The abstract input we add is still an ordinary function – the Chord() function template. But the functions used internally are passed as template arguments, making it possible to configure arbitrary chords using a single line of code for each new input.

The above adds a chord as abstract input: in order for the chord to trigger, the left control key must be held down, while an additional configurable key just went down. One thing I didn’t show in the forwarding functions’ implementation is that we can also add additional functions which take a ConfigSettingInt as template argument:

namespace ConfigKey
{
  template <ConfigSettingInt* T>
  ME_INLINE bool IsPressed(const LogicalDevice* device)
  {
    return device->GetKeyboard()->IsPressed(*T);
  }

  template <ConfigSettingInt* T>
  ME_INLINE bool WentDown(const LogicalDevice* device)
  {
    return device->GetKeyboard()->WentDown(*T);
  }

  template <ConfigSettingInt* T>
  ME_INLINE bool WentUp(const LogicalDevice* device)
  {
    return device->GetKeyboard()->WentUp(*T);
  }

  // other implementations omitted
}

This means we can build input functions at compile-time using variables as template arguments which are used during run-time! Quasi-configurable template arguments, think about that for a second.

Aliased buttons helpers

In a similar manner, we can conjure up Map() function templates:

template <bool (*Function1)(const LogicalDevice*), bool (*Function2)(const LogicalDevice*)>
bool Map(const LogicalDevice* device)
{
  if (((Function1)(device)) || ((Function2)(device)))
    return true;

  return false;
};

template <bool (*Function1)(const LogicalDevice*), bool (*Function2)(const LogicalDevice*), bool (*Function3)(const LogicalDevice*)>
bool Map(const LogicalDevice* device)
{
  if (((Function1)(device)) || ((Function2)(device)) || ((Function3)(device)))
    return true;

  return false;
};

All that has changed is that inputs are no longer &&-ed together, but ||-ed instead, because a map triggers as soon as any of the underlying inputs is toggled. Of course, all the forwarding functions can be used as well, allowing us to define completely arbitrary aliases of different inputs using a few lines of code:

Controller::ID mapID = logicalDevice.AddInput
(
  &Map
  <
    &ConfigMouseButton::IsPressed<&g_myMouseButton>,
    &JoystickButton::WentDown<0>,
    &ConfigXbox360ControllerButton::WentDown<&g_controllerButton>,
    &UserDefinedGestureInput
  >
);

Again, inputs from completely different devices can be mixed and matched as desired, and buttons/keys can either be hardcoded or configured at run-time (the above example shows a combination of both).

If you’re still following, you might also have noticed why we bothered giving each input function the same signature in the first place? The reason is that you can also mix and match Chord() and Map() functions:

Controller::ID mapID = defaultController.AddInput
(
  &Map
  <
    &ConfigMouseButton::IsPressed<&g_myMouseButton>,
    &Chord
    <
      &Key::IsPressed<input::Keyboard::Key::LEFT_CONTROL>,
      &ConfigKey::WentDown<&g_myKey>
    >,
    &ConfigJoystickButton::IsPressed<&g_joystickButton>,
    &ConfigXbox360ControllerButton::WentDown<&g_controllerButton>,
    &UserDefinedGestureInput
  >
);

As can be seen in the example, the Chord() function template can also be used as a template argument to the Map() function template, and vice versa. This allows us to alias chords with single buttons, or user-defined inputs, or similar.

One last thing to note: Using the Chord() and Map() function templates is exactly as efficient as if you had written the actual functions yourself (profiled using VS 2010). This is because all arguments are passed in templates, and the forwarding functions are tagged with ME_INLINE (__forceinline).

Let us quickly recap:

  • The above shows a very efficient implementation that is straightforward to use, and easy to configure.
  • Inputs from several different physical devices can be configured at will, supporting both hardcoded constants (where desired), and run-time configurable layouts.
  • Arbitrary user-defined inputs can be mixed and matched with other more simple inputs.
  • The implementation is completely interface and virtual-function free, hence new inputs can be added easily without having to write a ton of code.

And that’s it for today, hope you enjoyed the post.

Advertisements

7 thoughts on “Wiring physical devices to abstract inputs

    • Hi Riley, a valid question.

      To be honest, I find polling easier and simpler to use. I can poll from free functions, from inside classes, and hence can carry state around.

      Event-based input adds overhead because each event listener needs to be registered/stored somewhere, resulting in memory allocations (and deallocations whenever a listener unregisters itself from an event). It’s nothing too bad, and it’s nothing I would worry about performance-wise, but I somehow find polling easier/simpler to use, but that may very well be personal preference.

      It would be simple to just add events on top of my implementation, but I’ve not had any reason to do so yet.

      What’s your take on that? I would very much like to hear your opinion on polling vs. event-based inputs.

  1. Hi Stefan,

    first of all thank for sharing this blog post.

    I think a downside is, that you have to write a Chord or Map function for each amount of test functions. Well, it is a very small downside and I would live with it, but nevertheless I thought about implementing this for the general case.

    So, with C++11 and variadic templates I came up with:

    typedef bool (*InputFunc)(Input const*);

    template
    struct Logic {
    static inline bool conjunction(Input const* input) {
    return Function1(input) && Logic::conjunction(input);
    }
    static inline bool disjunction(Input const* input) {
    return Function1(input) || Logic::disjunction(input);
    }
    };

    template
    struct Logic {
    static inline bool conjunction(Input const* input) {
    return Function1(input);
    }
    static inline bool disjunction(Input const* input) {
    return Function1(input);
    }
    };

    Usage example:
    if (
    Logic<
    Logic<
    Key::pressed,
    Key::wentDown
    >::conjunction,
    Mouse::wentDown
    >::disjunction(&input)
    ) {
    printf(“LCtrl + C or right mouse pressed.\n”);
    }

    • Thanks for the input, Carsten!

      I changed the implementation a bit in the meantime, so you don’t have to write different Chord() or Map() functions. In fact, they are no longer needed at all. The new implementation works with the following syntax:

      Controller copyController;
      copyController.AddInput(&keyboard, &Keyboard::IsPressed, Keyboard::Key::LEFT_CONTROL);
      copyController.AddInput(&keyboard, &Keyboard::WentDown, Keyboard::Key::C);

      Controller enterMenuController;
      enterMenuController.AddInput(&keyboard, &Keyboard::IsPressed, Keyboard::Key::ENTER);
      enterMenuController.AddInput(&mouse, &Mouse::WentUp, Mouse::Button::LEFT);

      As can be seen, you can now freely mix different functions of completely different devices/classes. It even allows hooking up functions coming from your own classes (without having to derive from a common base class):

      GestureInput gesture(&mouse);
      Controller cancelController;
      cancelController.AddInput(&keyboard, &Keyboard::WentDown, Keyboard::Key::ESCAPE);
      cancelController.AddInput(&gesture, &GestureInput::Evaluate);

      The controller implementation now offers both a IsChordTriggered() and IsAnyTriggered() function.

      Of course, all of the above is handled in a completely type-safe manner, no void* or anything like that involved. There’s documentation and sample code in the SDK if you want to check it out.

      • How does this work without inheriting from a common base class and without using void*?

        Controller::AddInput must store the parameter values in some kind of container, but I can’t imagine how to do this, if the device types could be completely different.

      • The implementation uses type erasure in conjunction with a common base class and function pointers internally, but this is completely transparent to the user.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s