I have an InputDevice and an InputAction. How do I filter the action’s bindings that work for this specific device?
I know I can iterate over all bindings, but I cannot figure out any kind of connection between the properties of the device and the information provided by the InputBinding struct. For example, the path given in a binding struct (e.g., “/…” or “/…”) does not use the same pattern as those in the device’s controls (e.g., “Keyboard/” or “/XInputControllerWindows”), so I can’t use paths for matching. I cannot find any helper functions that allow this.
The GetBindingDisplayString helper isn’t of any use if I don’t know what index to pass to it matching the given device - the example (under “You can also use this method to replace the text string with images.”) is outdated to that end. Further, I don’t know how to fill the second argument of InputSystem.IsFirstLayoutBasedOnSecond given an InputDevice that may be a subtype of what I’m seeking. The deviceLayout in that example also returns garbage for composites.
This is a crucial query for user interfaces, e.g., if I want to show a help text like “Press to continue or to cancel”. So clearly there must be some way to do this?
This is what I came up with and it’s working. It feels very cumbersome and produces unnecessary garbage, so I hope somebody will come in to tell me that there’s a more straightforward and/or more efficient way. If there isn’t, please, InputSystem team, consider implementing a straightforward API for this no-brainer of a use case.
using System.Collections.Generic;
using UnityEngine.InputSystem;
public static class InputSystemUtil
{
private static readonly HashSet<string> _reportedCompositeParts = new HashSet<string>();
public static int GetPrimaryBindingForDevice(this InputAction action, InputDevice device, string[] outControlPaths)
{
// use global buffer to avoid re-allocations
_reportedCompositeParts.Clear();
var numControlPaths = 0;
foreach (var binding in action.bindings)
{
// make sure output buffer fits more
if (numControlPaths >= outControlPaths.Length) break;
// ignore composite bindings, as their device layout and control path are not meaningful
if (!binding.isComposite)
{
// the binding's name corresponds to its composite part, if any
var compositePart = binding.name;
if (!_reportedCompositeParts.Contains(compositePart))
{
// we don't care about the display string, we just want the layout and control path
// FIXME: this probably produces unnecessary garbage
binding.ToDisplayString(out var bindingDeviceLayout, out var bindingControlPath);
// test if the given device supports the binding's device layout
if (InputSystem.IsFirstLayoutBasedOnSecond(device.layout, bindingDeviceLayout))
{
// report binding
outControlPaths[numControlPaths++] = bindingControlPath;
// for any composite part, we only want to report first binding
_reportedCompositeParts.Add(compositePart);
}
}
}
}
// we reported this many control paths
return numControlPaths;
}
}
For anyone interested: this will report the control paths of all bindings of the action that work for the given device. For composites, it only reports the first binding found for each composite part, so for example, if you have composites “W/A/S/D” and “up/left/down/right” for the same action, only the first will be reported for each direction. That’s what I use the hash set for.
Now it can be used like this:
var buf = new string[4];
var num = action.GetPrimaryBindingForDevice(device, buf);
For my example, the function returns 4 and the buffer would contain “w”, “a”, “s”, “d” for keyboards, or it’d return 1 and the buffer contains “leftStick” if the device is a gamepad. You’ll probably want to allocate the buffer statically, similar as for the Physics.RaycastAll API.
foreach (var binding in action.bindings)
{
var control = InputControlPath.TryFindControl(device, binding.effectivePath);
if (control != null)
Debug.Log($"Path {binding.effectivePath} matches {control} on {device}");
}
In general, the recommended way is to use GetBindingDisplayString() and employ control schemes to limit the set of devices to those actively used. For actions that only have a single binding in a given control scheme, that should be sufficient. For actions that have primary and secondary bindings (or even more), this currently requires manually fishing out the binding indices by control scheme.
As far as I understood it, control schemes are those checkboxes in the Properties inspector of an action (and they appear to be stored in the group property of the binding). If I want to “limit the set of devices to those actively used”, I suppose this can be done using this line from the example: InputBinding.MaskByGroup("Gamepad"). The question then is how do I get the correct group string (e.g., “Gamepad”) from my InputDevice instance - is it always equivalent to its layout property, or would I have to hardcode them somewhere and find them via type checks?
EDIT: OK, for keyboards, the layout is “Keyboard”, but the name of the control scheme is “Keyboard&Mouse”, so simply using the layout won’t work.
Hm, thinking about this, with a properly formatted display string and some conventions that I could employ on the order in the action map, I think I could also use the combined list of primary/secondary bindings, e.g., as a localization key, and then only display the primary binding. This would reduce the “fishing out” part to a hashtable lookup.
I just wanted to come in here again and stress this, because this seems to be the only API that actually applies “localization” in terms of keyboard layout, since the InputSystem works with scan codes rather than letters.
To clarify in my case: I have a German layout (QWERTZ). When I press Z, the Input System (correctly) identifies this as the (US) Y key internally and the control path contains “y” correspondingly. However, for UI display, you’ll want the actual current keyboard layout to be respected so it displays “Z” rather than “Y”.