Confusion with rebinding input with generated C# InputActions at runtime

Hey forum,

using Unity 2021.3.18f1
using Input System 1.4.4

I am trying to implement input rebinding. The important point here is that I dont use the PlayerInput component but instead the generated class of my own InputActionAsset. I create an instance of the class at bootup of the game and this instance persists then for the whole runtime.
In order to implement rebinding I looked at docs and especially the sample provided by Unity and I wrote a custom script where I copied a big part of the sample script. This is the code I came up with:

 /// <summary>
    /// The idea with this script is to attach it to each UI Element in the GameOptions where we want to allow
    /// the player to rebind their input mappings
    /// Each on screen input command in the options should have their own instance of this component attached
    /// </summary>
    public class RebindingInputAction : MonoBehaviour
    {
        [SerializeField]
        private InputActionReference actionReference;

        [SerializeField]
        private InputBinding.DisplayStringOptions displayStringOptions;

        [SerializeField]
        private InputGroupType inputGroupType;

        [SerializeField]
        private GameObject bindBlocker;

        [SerializeField]
        private TextMeshProUGUI bindingText;

        private InputActionRebindingExtensions.RebindingOperation rebindingOperation;
        private Button                                            buttonToTriggerRebinding;
        private InputAction                                       currentAction;
        private string                                            defaultBindingId;
        private string                                            currentBindingId;


        private void OnEnable()
        {
            buttonToTriggerRebinding = GetComponentInChildren<Button>();
            buttonToTriggerRebinding.onClick.AddListener(OnButtonToTriggerRebinding);

            currentAction = GameManagers.InputHandler.MOHInputActions.FindAction(actionReference.action.name);

            InputBinding binding = GetInputBindingForGamepad(currentAction);
            defaultBindingId = binding.id.ToString();
            currentBindingId = defaultBindingId;
        }

        private InputBinding GetInputBindingForGamepad(InputAction inputAction)
        {
            int index = inputAction.bindings.IndexOf(x => string.CompareOrdinal(x.groups, inputGroupType.ToString()) == 0);

            return inputAction.bindings[index];
        }

        private void OnButtonToTriggerRebinding()
        {
            StartRebindOperation();
        }

        private void StartRebindOperation()
        {
            if (!ResolveActionAndBinding(out int bindingIndex))
            {
                return;
            }

            PerformBinding(currentAction, bindingIndex);
        }

        public bool ResolveActionAndBinding(out int bindingIndex)
        {
            bindingIndex = -1;

            if (currentAction == null)
            {
                return false;
            }

            if (string.IsNullOrEmpty(currentBindingId))
            {
                return false;
            }

            // Look up binding index.
            Guid bindingId = new(currentBindingId);
            bindingIndex = currentAction.bindings.IndexOf(x => x.id == bindingId);
            if (bindingIndex == -1)
            {
                Log.Error($"Cannot find binding with ID '{bindingId}' on '{currentAction}'", this);
                return false;
            }

            return true;
        }

        private void PerformBinding(InputAction action, int bindingIndex)
        {
            if (rebindingOperation != null)
            {
                rebindingOperation.Cancel();
            }

            action.Disable();

            rebindingOperation = PerformBindingByGroup(action, inputGroupType, bindingIndex);

            var partName = default(string);
            if (action.bindings[bindingIndex].isPartOfComposite)
                partName = $"Binding '{action.bindings[bindingIndex].name}'. ";
            // Bring up rebind overlay, if we have one.
            bindBlocker?.SetActive(true);
            if (bindingText != null)
            {
                var text = !string.IsNullOrEmpty(rebindingOperation.expectedControlType)
                    ? $"{partName}Waiting for {rebindingOperation.expectedControlType} input..."
                    : $"{partName}Waiting for input...";
                bindingText.text = text;
            }

            // If we have no rebind overlay and no callback but we have a binding text label,
            // temporarily set the binding text label to "<Waiting>".
            if (bindBlocker == null && bindingText == null && bindingText != null)
                bindingText.text = "<Waiting...>";
            rebindingOperation.Start();
        }

        private InputActionRebindingExtensions.RebindingOperation PerformBindingByGroup(InputAction action, InputGroupType groupType,
                                                                                        int bindingIndex)
        {
            if (groupType == InputGroupType.Gamepad)
            {
                return action.PerformInteractiveRebinding(bindingIndex)
                             .OnCancel(operation => RebindCanceled(action))
                             .OnComplete(operation => RebindComplete(action));
            }

            if (groupType == InputGroupType.Keyboard)
            {
                return action.PerformInteractiveRebinding(bindingIndex)
                             .OnCancel(operation => RebindCanceled(action))
                             .OnComplete(operation => RebindComplete(action));
            }

            Log.Error("No Gamepad or Keyboard groupType specified, RebindingOperation returned null");
            return null;
        }

        private void RebindCanceled(InputAction action)
        {
            bindBlocker.SetActive(false);

            CleanupRebinding(action);
        }

        private void RebindComplete(InputAction action)
        {
            bindBlocker.SetActive(false);

            action.Enable();

            rebindingOperation?.Dispose();
            rebindingOperation = null;

            Message.Raise(new RebindInputActionCompleted(GameManagers.InputHandler.MOHInputActions.SaveBindingOverridesAsJson()));
        }

        private void CleanupRebinding(InputAction action)
        {
            action.Enable();

            rebindingOperation?.Dispose();
            rebindingOperation = null;
        }

        private void OnDisable()
        {
            buttonToTriggerRebinding.onClick.RemoveListener(OnButtonToTriggerRebinding);
        }
    }
}

When I tested the code, rebinding did not work. I couldnt figure out why so I debugged it and what was really weird was that the rebinding DID work, but apparently not the way I wanted it?
So I performed the rebinding, in my case I tried to rebind the “PlayerInteraction/Interact” InputAction from the A-Button on a Xbox Controller to the Y-Button (buttonSouth to buttonNorth basically)
Then I debugged the code and this line here gave me surprising results:

currentAction = GameManagers.InputHandler.MOHInputActions.FindAction(actionReference.action.name);

When I analysed the “MOHInputActions” object I saw that the Interact Action still was mapped to buttonSouth, but interestingly the currentAction was mapped to buttonNorth now.
Superconfused now since the buttons on my controller still reacted the same way.

Now I restarted Unity. Tested it. Same behaviour. Disabled “EnterPlayModeOptions”, tested it. Same behaviour. Described the problem to ChatGPT becaus I was interested in if this “complex” problem could be solved by ChatGPT → it couldnt, the AI was totally overwhelmed with the task to find a solution and just gave me nonsense.

So I looked into the docs and searched forums until I found this topic here:

** Rebind not working **

Here Rene is saying this:

And I was like “Oooohhhh, yeah, makes perfect sense”. But also was still confused since the .FindAction method takes a string as argument, and I do perform this method on the runtime instance of my inputactions.

Also found this topic:
** Saving / Loading input actions with generated class **

Where andrew suggested to use the InputActionReference.Create() method and so I did, now the code looks like this:

currentAction = InputActionReference.Create(InputUtils.GetInputActionByName(GameManagers.InputHandler.MOHInputActions, inputActionName));

I also made a slight change, the field InputActionReference actionReference was changed to string inputActionName

The InputUtils.GetInputActionByName Method looks like this as of now for quick testing:

public static InputAction GetInputActionByName(MOHInputActions actions, string actionName)
        {
            if (actionName == nameof(actions.PlayerInteraction.Interact))
            {
                return actions.PlayerInteraction.Interact;
            }

            return null;
        }

And this works and I am not sure if I understand why it does. I also tested this:

currentAction = InputActionReference.Create(GameManagers.InputHandler.MOHInputActions.FindAction(inputActionName));

And this did not work.

So here’s a summary:

            // Version 1 - This works
            currentAction = InputActionReference.Create(InputUtils.GetInputReferenceByAction(
                                                            GameManagers.InputHandler.MOHInputActions, inputActionName));

            // Version 2 - This does not work
            currentAction = GameManagers.InputHandler.MOHInputActions.FindAction(inputActionName);

            // Version 3 - This does not work
            currentAction = InputActionReference.Create(GameManagers.InputHandler.MOHInputActions.FindAction(inputActionName));

As of now I dont really understand WHY exactly only the first version works. I understand that if I try to rebind things with an actual InputActionReference I do reference the InputActionAsset file itself basically and that for runtime I need to reference the very instance. But I thought I am doing that in Version 2 or 3 (especially 3!) at least in some way, since I am even using a strict string as of now and no InputActionReference object.

Would appreciate some clarity on this :slight_smile:

Bump

Bump

Bump, maybe @Schubkraft could chime in and give some insight? :slight_smile:

Aaand another bump, would really like to understand this :slight_smile:

Bumpy bump? Pinging maybe @Schubkraft again?

You can just use the rebind from the sample script, and then call this after rebind to update your generated C# class instance:

string json = _inputActionAsset.SaveBindingOverridesAsJson();
_myInputActions.asset.LoadBindingOverridesFromJson(json);
2 Likes

hey @visca_c
Eh, I know I can do that, but that is not at all the problem I had. The issue isnt saving or loading or the rebinding itself but WHY a specific way works while the others dont as layed out in the last code bit I provided in the OP, here it is again:

            // Version 1 - This works
            currentAction = InputActionReference.Create(InputUtils.GetInputReferenceByAction(
                                                            GameManagers.InputHandler.MOHInputActions, inputActionName));
            // Version 2 - This does not work
            currentAction = GameManagers.InputHandler.MOHInputActions.FindAction(inputActionName);
            // Version 3 - This does not work
            currentAction = InputActionReference.Create(GameManagers.InputHandler.MOHInputActions.FindAction(inputActionName));

my guess is that the code could be performing on different instances, maybe one is performing on the scriptable object instance of your input action asset, and the other one is performing on your generated c# instance.

Well, yes. That’s what I want to understand, that’s the reason I made this topic,hoping that someone from the Input Team from Unity might give me some context on this :slight_smile:

They are busy duplicating their UI in the project settings…

1 Like

bump
Pinging @Schubkraft once again to get some insight on the difference on these calls :smile: