Rebinding of Composite does not work - Buggy BindingIndex?

Hey forum,

using Unity 2021.3.18f1
using Input System 1.5.1

I have an issue with rebinding composite actions. I have a custom script where I copied the interesting parts from the the rebinding sample provided by Unity.
My script has these two methods:

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

            // If the binding is a composite, we need to rebind each part in turn.
            if (actionToRebind.bindings[bindingIndex].isComposite)
            {
                int firstPartIndex = bindingIndex + 1;
                if (firstPartIndex < actionToRebind.bindings.Count && actionToRebind.bindings[firstPartIndex].isPartOfComposite)
                {
                    PerformBinding(actionToRebind, firstPartIndex, true);
                }
            }
            else
            {
                PerformBinding(actionToRebind, bindingIndex);
            }
        }

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

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

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

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

            return true;
        }

In my test case I want to rebind the “PrimaryMovement” Action, the bindings look like this:

Now the rebinding doesnt work at all, so I started the debugger and monitored some values and the issue is the binding index apparently.

When ResolveActionAndBinding() is called in StartRebindingOperation, this line here:

            bindingIndex = actionToRebind.bindings.IndexOf(x => x.id == bindingId);

returns an int of 11, so the bindinIndex becomes 11.
And then StartRebindingOperation goes on with this bindingIndex and in this line here:

            if (actionToRebind.bindings[bindingIndex].isComposite)

returns false, since that binding with this index is not a composite, but apparently it seems to be part of a composite.

These are all the bindings by index when looking into them via the debugger:

8929163--1223855--RebindingComposite_Issue_01.png

Now when I inspect the bindings of the actionToRebind and looked into the 11th index and to what it belongs it seems the binding index I would need is 10, since this binding’s composite bool value is true, but for some reason I do get 11:

I am not sure if this is maybe a tiny bug here? Or if my setup is maybe wrong?

And while I am here, another question would be: As you can see in the binding screenshot above I have two mappings for both the Keyboard and the Gamepad?
How would I need to adjust the code in order to be able to rebind BOTH variations, the binding with the left stick and the binding with the dpad for example?

Is there any reason you don’t use the method (actionToRebind.GetBindingIndex) they made for exactly this?

See: GetBindingIndex
Similar usage as in the mask (so you just give a mapping binding new InputBinding { id = "<id>" })

@ Well, you got me there.
The reason I didnt use it is that I just copied from the sample…but with this approach I have to pass in the hardcoded id path as a string, but I’d like to avoid that or is there no other way?
And is this approach actually a solution? What’s the difference to the IndexOf call in this context?

? I’m not sure about what you mean by this, but new InputBinding { id = bindingId } should work.

But, I don’t know if this is a solution, and reading up on InputBinding in the documentation, I doubt it.
What you can do is this:
You take what you find by your search. Then check if it’s a composite binding by checking its isComposite property.
If it is, you have what you’re looking for.
If not, then check it’s isPartOfComposite property. If it’s false, you exit since there is no composite binding for this binding.
If isPartOfComposite property is true, you can start to lower your index one by one and get the previous binding in the bindings array until you find the very first (closest) previous binding with the isComposite == true.

You can do this, because

In your previous post you wrote this:

new InputBinding { id = “” }

And this seemed like I had to hardcode the string, that’s why I said that.

Yeah I thought about doing it like this, yes, but my question is:
why do I get the wrong index in the first place? When I look at the sample script provided by Unity which is basically the same code and the sample scene and debug it there I get the correct bindingIndex.
Maybe @Schubkraft can shed some light on this? I would like to know if this is maybe buggy behaviour, if so I’d report this asap :slight_smile:

Since you didn’t really share what you have stored about these, I’m not sure you get the wrong index. You probably store the id of the first binding of the composite you’re looking for.

I think I cant follow you @ , what do you mean by “you didnt really share what you have stored about these”?

I mean I have no way of validating that the bindingId you are using to find a particular binding is the composite binding’s id (index 10) or the first binding inside of that composite (index 11).
So where do you get the value of the bindingId?

Alright for full context, this is the whole class:

    /// <summary>
    /// The idea with this script is to attach it to each UI Element in the Input Game Settings 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
    /// Use this when you want to remap gamepad actions
    /// </summary>
    public class RebindingInputActionGamepad : MonoBehaviour
    {
        [UsedImplicitly]
        [ValueDropdown("@InputEditorUtils.ActionMapDropdown")]
        [SerializeField]
        private string inputMap;

        [ValueDropdown("@InputEditorUtils.ActionDropdown(inputMap)")]
        [SerializeField]
        private string inputActionName;

        [SerializeField]
        private GameObject bindBlocker;

        [SerializeField]
        private Button rebindButton;

        [SerializeField]
        private Button resetToDefaultsButton;

        [SerializeField]
        private TextMeshProUGUI bindingText;

        private readonly InputGroupType                                    gamepadGroup = InputGroupType.Gamepad;
        private          InputActionRebindingExtensions.RebindingOperation rebindingOperation;
        private          InputAction                                       actionToRebind;
        private          string                                            actionBindingId;


        private void OnEnable()
        {
            rebindButton.onClick.AddListener(OnTriggerRebinding);
            resetToDefaultsButton.onClick.AddListener(OnResetToDefaults);

            actionToRebind = InputActionReference.Create(InputUtils.GetInputReferenceByAction(
                                                             GameManagers.InputHandler.MOHInputActions, inputActionName));

            InputBinding binding = InputUtils.GetInputBindingForInputGroup(actionToRebind, gamepadGroup);
            actionBindingId = binding.id.ToString();
        }

        private void OnTriggerRebinding()
        {
            StartRebindOperation();
        }

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

            // If the binding is a composite, we need to rebind each part in turn.
            if (actionToRebind.bindings[bindingIndex].isComposite)
            {
                int firstPartIndex = bindingIndex + 1;
                if (firstPartIndex < actionToRebind.bindings.Count && actionToRebind.bindings[firstPartIndex].isPartOfComposite)
                {
                    PerformBinding(actionToRebind, firstPartIndex, true);
                }
            }
            else
            {
                PerformBinding(actionToRebind, bindingIndex);
            }
        }

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

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

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

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

            return true;
        }

        private void PerformBinding(InputAction action, int bindingIndex, bool allCompositeParts = false)
        {
            GameManagers.InputHandler.MOHInputActions.UserInterfaceNavigation.Navigate.Disable();
            if (rebindingOperation != null)
            {
                rebindingOperation.Cancel();
            }

            action.Disable();

            rebindingOperation = PerformBindingByGroup(action, gamepadGroup, bindingIndex, allCompositeParts);
            bindBlocker.SetActive(true);
            var partName = default(string);
            if (bindingText != null)
            {
                var text = !string.IsNullOrEmpty(rebindingOperation.expectedControlType)
                    ? $"{partName}Waiting for {rebindingOperation.expectedControlType} input..."
                    : $"{partName}Waiting for input...";
                bindingText.text = text;
            }

            rebindingOperation.Start();
        }

        private InputActionRebindingExtensions.RebindingOperation PerformBindingByGroup(InputAction action, InputGroupType groupType,
                                                                                        int bindingIndex, bool allCompositeParts)
        {
            if (groupType == InputGroupType.Gamepad)
            {
                return action.PerformInteractiveRebinding(bindingIndex)
                             .OnCancel(operation => RebindCanceled(action))
                             .OnComplete(operation => RebindComplete(action, bindingIndex, allCompositeParts))
                             .OnMatchWaitForAnother(0.3f)
                             .WithBindingGroup("<Gamepad>");
            }

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

        private void RebindCanceled(InputAction action)
        {
            GameManagers.InputHandler.MOHInputActions.UserInterfaceNavigation.Navigate.Enable();
            bindBlocker.SetActive(false);

            CleanupRebinding(action);
        }

        private void RebindComplete(InputAction action, int bindingIndex, bool allCompositeParts)
        {
            GameManagers.InputHandler.MOHInputActions.UserInterfaceNavigation.Navigate.Enable();
            bindBlocker.SetActive(false);

            action.Enable();

            rebindingOperation?.Dispose();
            rebindingOperation = null;

            // If there's more composite parts we should bind, initiate a rebind
            // for the next part.
            if (allCompositeParts)
            {
                int nextBindingIndex = bindingIndex + 1;
                if (nextBindingIndex < action.bindings.Count && action.bindings[nextBindingIndex].isPartOfComposite)
                {
                    PerformBinding(action, nextBindingIndex, true);
                }
            }

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

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

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


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

            if (actionToRebind.bindings[bindingIndex].isPartOfComposite)
            {
                // It's a composite. Remove overrides from part bindings.
                for (var i = bindingIndex + 1; i < actionToRebind.bindings.Count && actionToRebind.bindings[i].isPartOfComposite; ++i)
                {
                    actionToRebind.RemoveBindingOverride(i);
                }
            }
            else
            {
                actionToRebind.RemoveBindingOverride(bindingIndex);
            }
        }

        private void OnDisable()
        {
            rebindButton.onClick.RemoveListener(OnTriggerRebinding);
            resetToDefaultsButton.onClick.RemoveListener(OnResetToDefaults);
        }
    }

And these are the helper methods used in OnEnable()

      public static InputBinding GetInputBindingForInputGroup(InputAction inputAction, InputGroupType inputGroupType)
        {
            int index = inputAction.bindings.IndexOf(x => string.CompareOrdinal(x.groups, inputGroupType.ToString()) == 0);

            return inputAction.bindings[index];
        }

        public static InputAction GetInputReferenceByAction(MOHInputActions actions, string actionName)
        {
            if (actionName == nameof(actions.GeneralUI.EnterMainMenu))
            {
                return actions.GeneralUI.EnterMainMenu;
            }

            if (actionName == nameof(actions.PlayerMovement.PrimaryMovement))
            {
                return actions.PlayerMovement.PrimaryMovement;
            }

            if (actionName == nameof(actions.PlayerMovement.Run))
            {
                return actions.PlayerMovement.Run;
            }

            if (actionName == nameof(actions.PlayerInteraction.Interact))
            {
                return actions.PlayerInteraction.Interact;
            }

            if (actionName == nameof(actions.Dialogue.ProgressDialogue))
            {
                return actions.Dialogue.ProgressDialogue;
            }

            if (actionName == nameof(actions.Dialogue.EnterMainMenu))
            {
                return actions.Dialogue.EnterMainMenu;
            }

            if (actionName == nameof(actions.Cutscene.ProgressCutscene))
            {
                return actions.Cutscene.ProgressCutscene;
            }

            Log.Error($"ActionName: {actionName} does not match any of the InputActions!");
            return null;
        }

Alright, I think I found the issue.

So this code here was problematic after doing some debugging:

  public static InputBinding GetInputBindingForInputGroup(InputAction inputAction, InputGroupType inputGroupType)
        {
            int index = inputAction.bindings.IndexOf(x => string.CompareOrdinal(x.groups, inputGroupType.ToString()) == 0);
            return inputAction.bindings[index];
        }

Since my groupType is a string like this “Gamepad”, it will ofc give me the binding of the first Gamepad-bound binding it finds and this is correctly at index 11.

I adjusted the whole code now and look for the bindings in another way.

Instead of the group type by string I search for the original binding path (or for the composite name I setup in the Action Asset if it’s a composite) like this:

public static InputBinding GetInputBindingByPath(InputAction inputAction, string path)
        {
            int index = inputAction.bindings.IndexOf(x => string.CompareOrdinal(x.path, path) == 0);
            return inputAction.bindings[index];
        }

        public static InputBinding GetInputBindingByCompositeName(InputAction inputAction, string compositeName)
        {
            int index = inputAction.bindings.IndexOf(x => x.isComposite && string.CompareOrdinal(x.name, compositeName) == 0);
            return inputAction.bindings[index];
        }

I wrote a handy method to expose the strings of the path and compositenames and can pass them in here. Works without problems :slight_smile: