Detect most recent input device/type?

I’m trying to integrate/switchto the new input system and I’m stuck at the following issue.

My game supports gamepad, as well as mouse and keyboard controls when running on PC. Whatever input device the player used last, affects what the game displays for help texts.

When the player makes any gamepad input, it causes the game to display gamepad sprites in various texts. If the player then switches from gamepad to keyboard, these texts are updated to no longer display gamepad sprites, but keyboard images for example.

In order further support this, I need to figure out what device type the player used most recently and that’s where I’m currently stuck.

I thought I could use IsActuated to figure this out:

Debug.LogFormat("gamepad: {0}, mouse: {1}, keyboard: {2}",
Gamepad.current?.IsActuated(0),
Mouse.current?.IsActuated(0),
Keyboard.current?.IsActuated(0));

… but Gamepad and Mouse return true always, once input was made. Keyboard on the other hand resets as I expected after the key is released.

How can I detect what device was used most recently?

I think what you should do is have different control scheme for each devices (mouse and keyboard can be together though), and then, if you’re using PlayerInput, you can get the current control scheme to display what you want to display based on that. I believe there is also an event to know when the player switched control scheme.

For the simplest possible setup, I’d recommend using PlayerInput. It has built-in support for automatic control scheme switching and you’ll get notified when a switch happens. Simply define both a keyboard and a gamepad scheme and listen to InputUser.onChange where you will get a InputUserChange.ControlSchemeChanged notification whenever the user switches. From there, you can trigger a refresh of the UI hints you are displaying to match the current control scheme (which you can query from PlayerInput.currentControlScheme or InputUser.controlScheme).

As for detecting the most recently used device, InputDevice.lastUpdateTime will tell you the timestamp of the most recent state event received for the device.

However, even if PlayerInput doesn’t fit what you’re looking for, I’d recommend using InputUser to explicitly track used devices via its pairing mechanism and use InputUser.listenForUnpairedDeviceActivity to detect when the user switches. This is what PlayerInput does internally.

The problem with simply comparing timestamps is that it won’t be very robust. A device being updated does not necessarily equate user interaction. PS4 controllers, for example, will just constantly send noise from their builtin gyro even if the gamepad is not used by anyone. InputUser/PlayerInput have builtin support for filtering that out.

7 Likes

I’m speaking with limited experience using the new input system.

I too would like an easy way to be notified on control scheme change. From my brief use, I have found a host of bugs in PlayerInput (it can even break UI functionality, seems like a big oversight). Considering this, I would prefer to avoid it at all costs but would like to be able to easily tell what scheme I’m using. Your solution above appears to require InputUser.onUnpairedDeviceUsed and InputUser.listenForUnpairedDeviceActivity which both seem to be driven by PlayerInput.

Is there any way to get the current scheme easily without PlayerInput plaguing us, or do we need to strip PlayerInput for relevant code and roll our own solution?

I’m also speaking with limited experience and a lot of problem myself, but I think you can create an InputUser and assign it devices and so on. So you’ll be able to use Inputer.onChange etc. Again, not sure :frowning:

InputUser is an independent API. PlayerInput is built on top of it but the API can be used from anywhere.

Does this require us to create a user then assign devices as Jichaels stated above? Simply registering for an event from the InputUser class will never fire said events unless PlayerInput is in the scene from my limited testing. If we are required to create a user is there a tutorial/documentation somewhere or do we need to dig through PlayerInput to see how that’s happening?

Yes, you need to handle pairing yourself.

Not ATM.

Control schemes are a high-level concept implemented in PlayerInput. If you use actions directly, then the concept of schemes doesn’t really exist.

Our game is only single player and I converted it from a custom system based on the old input manager to the new input system. The way I handled detecting the currently used device is to just use the action themselves.

Without PlayerInput, what the schemes do in the input action editor is simply setting the group on the bindings of the action. I registered a handler with actionTriggered on my InputActionMap and used InputControlScheme.FindControlSchemeForDevice to figure out which scheme that activation belongs to. I use this to know which scheme was used last and when it changes.

This will only work for the actual bound controls, i.e. it won’t detect a change if you press any key on the keyboard or gamepad, you have to press a key that is actually used for your game.

3 Likes

Which I think is a slick workaround, as you don’t have to deal with “noise” such as from the builtin gyro on PS4, that could falsely cause a change.

I came up with a similar workaround, but I don’t test a single action, but all actions I configured for the game. But your method seems to be more clever, just using a single action. I will probably change it to a single action per device as well now, so that “actionTriggered” would include all buttons I want to consider for a valid device change.

This is more or less what I did.

Here’s my solution for runtime changes of button sprites:

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Users;

// add this into built-in button you can create from menu: UI/Button
public class InputDeviceChangeHandler : MonoBehaviour {
   // refs to Button's components
    private Image buttonImage;

   // refs to your sprites
    public Sprite gamepadImage;
    public Sprite keyboardImage;

    void Awake() {
        buttonImage = GetComponent<Image>();
        PlayerInput input = FindObjectOfType<PlayerInput>();
        updateButtonImage(input.currentControlScheme);
    }

    void OnEnable() {
        InputUser.onChange += onInputDeviceChange;
    }

    void OnDisable() {
        InputUser.onChange -= onInputDeviceChange;
    }

    void onInputDeviceChange(InputUser user, InputUserChange change, InputDevice device) {
        if (change == InputUserChange.ControlSchemeChanged) {
            updateButtonImage(user.controlScheme.Value.name);
        }
    }

    void updateButtonImage(string schemeName) {
       // assuming you have only 2 schemes: keyboard and gamepad
        if (schemeName.Equals("Gamepad")) {
            buttonImage.sprite = gamepadImage;
        }
        else {
            buttonImage.sprite = keyboardImage;
        }
    }
}
34 Likes

Thanks spiritworld,
That was super helpful for getting my approach to this set up, though I took it a little bit further because I’m supporting device-specific icons, as much as I can.

This made me notice though that it seems every time a scene change happens (at least running in the editor) the PlayerInput.currentControlScheme seems to get reset to “Keyboard&Mouse”. Shouldn’t this value stay unpopulated until something actually gets used?

I’m trying to find a valid control scheme for given user input. I tried to use an approach like @Adrian , @Peter77 and @Lavidimus described it: Get notified about any Action that is triggered (either explicitly subscribing to the ActionMaps’ actionTriggered callbacks, or globally to InputSystem.onActionChange), then react to that. However, just querying the device and using that to determine the scheme does not work for me since I would like to differentiate between KeyboardWASD and KeyboardArrow schemes.

Here’s how far I got:

private void ActionMap_actionTriggered(InputAction.CallbackContext obj)
{
    var inputAction = obj.action;
    var binding = inputAction.GetBindingForControl(inputAction.activeControl).Value;
    Debug.Log(binding.groups);
}

…which actually works fine for simple Actions, but fails for Composites because they don’t have the groups (= ControlSchemes) themselves.

Any idea how I could get the actual ‘leaf’ binding from the Composite, or find the Control Scheme for my activeControl in an altogether different way?

1 Like

We’re definitely missing a method that simply gives you an index of the currently active binding. Maybe we should just have an InputAction.activeBinding getter just like InputAction.activeControl.

Hmm, that one hits a peculiarity in how the current data structure is set up. Leaning towards considering this one a bug.

The problem is that in the data, all controls from a composite are associated with it. So when the code looks up the binding for the control, it comes across the composite before it comes across the part bindings. So it ends up returning the index of the composite instead proceeding to look at the part bindings.

I’m thinking, this is something we should fix on our side. GetBindingForControl() should return the part binding here, not the composite.

Here’s how I’m doing it (If I understood your question correctly). I’ve set mine up to support only a composite that’s a “button with one modifier” type. I wanted to show “Tab” and “Shift + Tab” navigation and I have icons for those two keyboard keys. I also intended for this class to be on a child of the button, and have children called “Control Image” and “Modifier Image” - adjust to your needs, of course. The below code is also missing some extra classes I use for storing preferences (PreferenceData) and configuring control icons (ControlDevices), but hopefully gives a sense of how I accomplished these things.

EDIT: Keep in mind that some of this code is because I wanted to support Keyboard&Mouse/Gamepad/Touchscreen as well as allowing the user to decide which Icons they want to see (ie, playing with any gamepad but showing xbox icons if you prefer)

EDIT2: sorry for all the “this.xyz” … I’m a Java software developer by trade and brought some of my habits with me.

    public class InputActionIcon : MonoBehaviour
    {
        // this is required to work around the fact that OnActionChange is static.
        private static List<InputActionIcon> s_InputActionIcons;

        private Image controlImage;
        private Image modifierImage;
        private List<string> controls;
        private List<string> modifierControls;

        /// <summary>
        /// Reference to the action that is to be rebound.
        /// </summary>
        public InputActionReference actionReference
        {
            get => m_Action;
            set
            {
                m_Action = value;
                //UpdateActionLabel();
                UpdateControlDisplay();
            }
        }
        [Tooltip("Reference to action that is to be rebound from the UI.")]
        [SerializeField]
        private InputActionReference m_Action;

        [Tooltip("Control path to use if no action is provided above.")]
        public string controlPath;

//preference data is a scriptable object that stores preferences
        public PreferenceData preferenceData;
//control devices is a scriptable object that maps input devices to collections of sprites
        public ControlDevices controlDevices;

        void Awake()
        {
            this.controls = new List<string>();
            this.modifierControls = new List<string>();
            this.controlImage = this.transform.Find("Control Image").GetComponent<Image>();
            this.modifierImage = this.transform.Find("Modifier Image").GetComponent<Image>();
        }

        protected void OnEnable()
        {
            if (s_InputActionIcons == null)
                s_InputActionIcons = new List<InputActionIcon>();
            s_InputActionIcons.Add(this);
            if (s_InputActionIcons.Count == 1) {
                InputSystem.onActionChange += OnActionChange;
                InputUser.onChange += OnInputDeviceChange;
            }
            UpdateControlDisplay();
        }

        protected void OnDisable()
        {
            s_InputActionIcons.Remove(this);
            if (s_InputActionIcons.Count == 0) {
                s_InputActionIcons = null;
                InputSystem.onActionChange -= OnActionChange;
                InputUser.onChange -= OnInputDeviceChange;
            }
        }

        public void UpdateControlDisplay()
        {
            this.controls.Clear();
            this.modifierControls.Clear();

            // examine bindings and separate controls and modifiers.
            var action = m_Action?.action;
            if (action != null) {
                for (int i = 0; i < action.bindings.Count; i++) {
                    //Debug.Log("i: " + ", composite: " + action.bindings[i].isComposite + ", partOfComposite: " + action.bindings[i].isPartOfComposite + ", path: " + action.bindings[i].path + ", name: " + action.bindings[i].name);
                    if (action.bindings[i].name == "modifier") {
                        this.modifierControls.Add(action.bindings[i].effectivePath.Substring(action.bindings[i].effectivePath.IndexOf("/") + 1));
                    } else {
                        this.controls.Add(action.bindings[i].effectivePath.Substring(action.bindings[i].effectivePath.IndexOf("/") + 1)); 
                    }
                }
            } else {
                this.controls.Add(this.controlPath.Substring(this.controlPath.IndexOf("/") + 1));
            }

           // here you'll need something that can return a sprite for the desired image given a device name and a control path.  I do it with some scriptable objects but the below section of code won't work if copy pasted.


            // determine which icon set to draw from (either auto-detect or use preferred set).
            ControlIcons controlIcons = null;
            if (this.preferenceData.controlIconSelection == PreferenceData.PREFERENCE_CONTROL_ICONS_AUTO) {
                foreach (DeviceMapping deviceMapping in this.controlDevices.deviceMap) {
                    if (this.preferenceData.lastUsedDevice.Contains(deviceMapping.deviceName)) {
                        controlIcons = deviceMapping.controlIcons;
                    }
                }

                // If last device didn't match a mapping with the right controls, see if we have a control path,
                // and try to use that to match. The most likely scenario for this is when the last device is set
                // to keyboard, but the on-screen controls are being displayed.
                if (controlIcons == null && this.controlPath != null) {
                    foreach (DeviceMapping deviceMapping in this.controlDevices.deviceMap) {
                        if (this.controlPath.Contains(deviceMapping.deviceName)) {
                            controlIcons = deviceMapping.controlIcons;
                        }
                    }
                }
            } else {
                controlIcons = this.controlDevices.deviceMap[this.preferenceData.controlIconSelection].controlIcons;
            }

            if (controlIcons != null) {
                foreach (string control in this.controls) {
                    Sprite sprite = controlIcons.GetSprite(control);
                    if (sprite != null) {
                        this.controlImage.sprite = sprite;
                    }
                }
                this.modifierImage.gameObject.SetActive(false);
                foreach (string control in this.modifierControls) {
                    Sprite sprite = controlIcons.GetSprite(control);
                    if (sprite != null) {
                        this.modifierImage.sprite = sprite;
                        this.modifierImage.gameObject.SetActive(true);

                    }
                }
            }
        }

        // When the action system re-resolves bindings, we want to update our UI in response. While this will
        // also trigger from changes we made ourselves, it ensures that we react to changes made elsewhere. If
        // the user changes keyboard layout, for example, we will get a BoundControlsChanged notification and
        // will update our UI to reflect the current keyboard layout.
        private static void OnActionChange(object obj, InputActionChange change)
        {
            if (change != InputActionChange.BoundControlsChanged)
                return;

            //Debug.Log("OnActionChange - change: " + change);

            var action = obj as InputAction;
            var actionMap = action?.actionMap ?? obj as InputActionMap;
            var actionAsset = actionMap?.asset ?? obj as InputActionAsset;

            for (var i = 0; i < s_InputActionIcons.Count; ++i)
            {
                var component = s_InputActionIcons[i];

                var referencedAction = component.actionReference?.action;
                if (referencedAction == null)
                    return;

                if (referencedAction == action ||
                    referencedAction.actionMap == actionMap ||
                    referencedAction.actionMap?.asset == actionAsset) {
                    component.UpdateControlDisplay();
                }
            }
        }

        private static void OnInputDeviceChange(InputUser user, InputUserChange change, InputDevice device)
        {
            if (change == InputUserChange.DevicePaired
                && !device.ToString().Contains("Mouse")) {
                //Debug.Log("device paired: " + device);
                for (var i = 0; i < s_InputActionIcons.Count; ++i) {
                    var component = s_InputActionIcons[i];
                    if (device.ToString() != component.preferenceData.lastUsedDevice) {
                        string previousControlScheme = component.preferenceData.lastUsedControlScheme;
                        string previousDevice = component.preferenceData.lastUsedDevice;

                        component.preferenceData.lastUsedDevice = device.ToString();
                    }
                    component.UpdateControlDisplay();
                }
            }

            if (change == InputUserChange.ControlSchemeChanged) {
                //Debug.Log("control scheme changed to: " + user.controlScheme.Value.name);
                for (var i = 0; i < s_InputActionIcons.Count; ++i) {
                    var component = s_InputActionIcons[i];
                    if (user.controlScheme.Value.name != component.preferenceData.lastUsedControlScheme) {
                        string previousControlScheme = component.preferenceData.lastUsedControlScheme;
                        string previousDevice = component.preferenceData.lastUsedDevice;
         
                        component.preferenceData.lastUsedControlScheme = user.controlScheme.Value.name;
                    }
                    component.UpdateControlDisplay();
                }
            }
        }
    }
}

I found this other thread by @Adrian , suggesting to use InputControlPath.Matches, so here’s where I’m at now - calling this from either the ActionMaps’ actionTriggered or the global InputSystem.onActionChange callbacks:

private static void GetPossibleSchemes(InputAction action)
{
    char[] separator = new char[] { ';' };
    foreach (var binding in action.bindings)
    {
        if (InputControlPath.Matches(binding.effectivePath, action.activeControl))
        {
            // A binding can be assigned to multiple InputSchemes - loop over them
            foreach (string group in binding.groups.Split(separator, StringSplitOptions.RemoveEmptyEntries))
            {
                // Do whatever with the candidate InputScheme
            }
        }
    }
}

For now, I think this works well enough for my case of ‘I want to know which InputScheme was used most recently’.

I agree, both ideas sound like they would be very helpful.

Thanks for the code, I’ll dig through it. My goal, too, is to display appropriate sprites for the current input configuration, and you mentioned one important point: If ‘Gamepad’ is the current InputScheme, I might still want to display different sprites based on the actual type of controller. I’ll have to put some thought into this.

@Rene-Damm
I have code that suddenly stopped working when I updated to 1.0.0 - preview 7

The ControlSchemes are no longer found by InputControlScheme.FindControlSchemeForDevice. What’s happened?

    private static void OnActionTriggered(InputAction.CallbackContext _callbackContext)
    {
        var device = _callbackContext.control.device;
        var actionName = _callbackContext.action.actionMap.name;
        var scheme = InputControlScheme.FindControlSchemeForDevice(device, _callbackContext.action.actionMap.controlSchemes);
        if (scheme.HasValue)
        {
            Debug.Log($"{actionName}'s scheme: {scheme.Value}");
        }
        else
        {
            Debug.Log($"{actionName} has no scheme!?"); // THIS IS ALWAYS CALLED NOW !
        }
     }

@Rene-Damm I still have the issue presented above. Are you aware of this bug?

What’s the device and the requirements of the control scheme you’re expecting to match?

IIRC there was a change where the matching now takes the full list of requirements into account. I’m thinking that API where you give it a single device should probably be extended to allow specifying something like “give me any one that matches the device even if there’s additional requirements not met with just that one device”.