Search Unity

Question Confusion with rebinding input with generated C# InputActions at runtime

Discussion in 'Input System' started by BTStone, Apr 5, 2023.

  1. BTStone

    BTStone

    Joined:
    Mar 10, 2012
    Posts:
    1,422
    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:

    Code (CSharp):
    1.  /// <summary>
    2.     /// The idea with this script is to attach it to each UI Element in the GameOptions where we want to allow
    3.     /// the player to rebind their input mappings
    4.     /// Each on screen input command in the options should have their own instance of this component attached
    5.     /// </summary>
    6.     public class RebindingInputAction : MonoBehaviour
    7.     {
    8.         [SerializeField]
    9.         private InputActionReference actionReference;
    10.  
    11.         [SerializeField]
    12.         private InputBinding.DisplayStringOptions displayStringOptions;
    13.  
    14.         [SerializeField]
    15.         private InputGroupType inputGroupType;
    16.  
    17.         [SerializeField]
    18.         private GameObject bindBlocker;
    19.  
    20.         [SerializeField]
    21.         private TextMeshProUGUI bindingText;
    22.  
    23.         private InputActionRebindingExtensions.RebindingOperation rebindingOperation;
    24.         private Button                                            buttonToTriggerRebinding;
    25.         private InputAction                                       currentAction;
    26.         private string                                            defaultBindingId;
    27.         private string                                            currentBindingId;
    28.  
    29.  
    30.         private void OnEnable()
    31.         {
    32.             buttonToTriggerRebinding = GetComponentInChildren<Button>();
    33.             buttonToTriggerRebinding.onClick.AddListener(OnButtonToTriggerRebinding);
    34.  
    35.             currentAction = GameManagers.InputHandler.MOHInputActions.FindAction(actionReference.action.name);
    36.  
    37.             InputBinding binding = GetInputBindingForGamepad(currentAction);
    38.             defaultBindingId = binding.id.ToString();
    39.             currentBindingId = defaultBindingId;
    40.         }
    41.  
    42.         private InputBinding GetInputBindingForGamepad(InputAction inputAction)
    43.         {
    44.             int index = inputAction.bindings.IndexOf(x => string.CompareOrdinal(x.groups, inputGroupType.ToString()) == 0);
    45.  
    46.             return inputAction.bindings[index];
    47.         }
    48.  
    49.         private void OnButtonToTriggerRebinding()
    50.         {
    51.             StartRebindOperation();
    52.         }
    53.  
    54.         private void StartRebindOperation()
    55.         {
    56.             if (!ResolveActionAndBinding(out int bindingIndex))
    57.             {
    58.                 return;
    59.             }
    60.  
    61.             PerformBinding(currentAction, bindingIndex);
    62.         }
    63.  
    64.         public bool ResolveActionAndBinding(out int bindingIndex)
    65.         {
    66.             bindingIndex = -1;
    67.  
    68.             if (currentAction == null)
    69.             {
    70.                 return false;
    71.             }
    72.  
    73.             if (string.IsNullOrEmpty(currentBindingId))
    74.             {
    75.                 return false;
    76.             }
    77.  
    78.             // Look up binding index.
    79.             Guid bindingId = new(currentBindingId);
    80.             bindingIndex = currentAction.bindings.IndexOf(x => x.id == bindingId);
    81.             if (bindingIndex == -1)
    82.             {
    83.                 Log.Error($"Cannot find binding with ID '{bindingId}' on '{currentAction}'", this);
    84.                 return false;
    85.             }
    86.  
    87.             return true;
    88.         }
    89.  
    90.         private void PerformBinding(InputAction action, int bindingIndex)
    91.         {
    92.             if (rebindingOperation != null)
    93.             {
    94.                 rebindingOperation.Cancel();
    95.             }
    96.  
    97.             action.Disable();
    98.  
    99.             rebindingOperation = PerformBindingByGroup(action, inputGroupType, bindingIndex);
    100.  
    101.             var partName = default(string);
    102.             if (action.bindings[bindingIndex].isPartOfComposite)
    103.                 partName = $"Binding '{action.bindings[bindingIndex].name}'. ";
    104.             // Bring up rebind overlay, if we have one.
    105.             bindBlocker?.SetActive(true);
    106.             if (bindingText != null)
    107.             {
    108.                 var text = !string.IsNullOrEmpty(rebindingOperation.expectedControlType)
    109.                     ? $"{partName}Waiting for {rebindingOperation.expectedControlType} input..."
    110.                     : $"{partName}Waiting for input...";
    111.                 bindingText.text = text;
    112.             }
    113.  
    114.             // If we have no rebind overlay and no callback but we have a binding text label,
    115.             // temporarily set the binding text label to "<Waiting>".
    116.             if (bindBlocker == null && bindingText == null && bindingText != null)
    117.                 bindingText.text = "<Waiting...>";
    118.             rebindingOperation.Start();
    119.         }
    120.  
    121.         private InputActionRebindingExtensions.RebindingOperation PerformBindingByGroup(InputAction action, InputGroupType groupType,
    122.                                                                                         int bindingIndex)
    123.         {
    124.             if (groupType == InputGroupType.Gamepad)
    125.             {
    126.                 return action.PerformInteractiveRebinding(bindingIndex)
    127.                              .OnCancel(operation => RebindCanceled(action))
    128.                              .OnComplete(operation => RebindComplete(action));
    129.             }
    130.  
    131.             if (groupType == InputGroupType.Keyboard)
    132.             {
    133.                 return action.PerformInteractiveRebinding(bindingIndex)
    134.                              .OnCancel(operation => RebindCanceled(action))
    135.                              .OnComplete(operation => RebindComplete(action));
    136.             }
    137.  
    138.             Log.Error("No Gamepad or Keyboard groupType specified, RebindingOperation returned null");
    139.             return null;
    140.         }
    141.  
    142.         private void RebindCanceled(InputAction action)
    143.         {
    144.             bindBlocker.SetActive(false);
    145.  
    146.             CleanupRebinding(action);
    147.         }
    148.  
    149.         private void RebindComplete(InputAction action)
    150.         {
    151.             bindBlocker.SetActive(false);
    152.  
    153.             action.Enable();
    154.  
    155.             rebindingOperation?.Dispose();
    156.             rebindingOperation = null;
    157.  
    158.             Message.Raise(new RebindInputActionCompleted(GameManagers.InputHandler.MOHInputActions.SaveBindingOverridesAsJson()));
    159.         }
    160.  
    161.         private void CleanupRebinding(InputAction action)
    162.         {
    163.             action.Enable();
    164.  
    165.             rebindingOperation?.Dispose();
    166.             rebindingOperation = null;
    167.         }
    168.  
    169.         private void OnDisable()
    170.         {
    171.             buttonToTriggerRebinding.onClick.RemoveListener(OnButtonToTriggerRebinding);
    172.         }
    173.     }
    174. }




    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:

    Code (CSharp):
    1. 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:

    https://forum.unity.com/threads/rebind-not-working.882127/

    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:
    https://forum.unity.com/threads/saving-loading-input-actions-with-generated-class.1152083/

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

    Code (CSharp):
    1. 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:

    Code (CSharp):
    1. public static InputAction GetInputActionByName(MOHInputActions actions, string actionName)
    2.         {
    3.             if (actionName == nameof(actions.PlayerInteraction.Interact))
    4.             {
    5.                 return actions.PlayerInteraction.Interact;
    6.             }
    7.  
    8.             return null;
    9.         }

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

    Code (CSharp):
    1. currentAction = InputActionReference.Create(GameManagers.InputHandler.MOHInputActions.FindAction(inputActionName));
    And this did not work.


    So here's a summary:

    Code (CSharp):
    1.  
    2.             // Version 1 - This works
    3.             currentAction = InputActionReference.Create(InputUtils.GetInputReferenceByAction(
    4.                                                             GameManagers.InputHandler.MOHInputActions, inputActionName));
    5.  
    6.             // Version 2 - This does not work
    7.             currentAction = GameManagers.InputHandler.MOHInputActions.FindAction(inputActionName);
    8.  
    9.             // Version 3 - This does not work
    10.             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 :)
     
  2. BTStone

    BTStone

    Joined:
    Mar 10, 2012
    Posts:
    1,422
  3. BTStone

    BTStone

    Joined:
    Mar 10, 2012
    Posts:
    1,422
  4. BTStone

    BTStone

    Joined:
    Mar 10, 2012
    Posts:
    1,422
    Bump, maybe @Schubkraft could chime in and give some insight? :)
     
  5. BTStone

    BTStone

    Joined:
    Mar 10, 2012
    Posts:
    1,422
    Aaand another bump, would really like to understand this :)
     
  6. BTStone

    BTStone

    Joined:
    Mar 10, 2012
    Posts:
    1,422
  7. visca_c

    visca_c

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

    Code (CSharp):
    1. string json = _inputActionAsset.SaveBindingOverridesAsJson();
    2. _myInputActions.asset.LoadBindingOverridesFromJson(json);
     
  8. BTStone

    BTStone

    Joined:
    Mar 10, 2012
    Posts:
    1,422
    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:

    Code (CSharp):
    1.  
    2.             // Version 1 - This works
    3.             currentAction = InputActionReference.Create(InputUtils.GetInputReferenceByAction(
    4.                                                             GameManagers.InputHandler.MOHInputActions, inputActionName));
    5.             // Version 2 - This does not work
    6.             currentAction = GameManagers.InputHandler.MOHInputActions.FindAction(inputActionName);
    7.             // Version 3 - This does not work
    8.             currentAction = InputActionReference.Create(GameManagers.InputHandler.MOHInputActions.FindAction(inputActionName));
     
  9. visca_c

    visca_c

    Joined:
    Apr 7, 2014
    Posts:
    30
    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.
     
  10. BTStone

    BTStone

    Joined:
    Mar 10, 2012
    Posts:
    1,422
    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 :)
     
  11. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 5, 2024
    Posts:
    467
    They are busy duplicating their UI in the project settings...
     
    BTStone likes this.
  12. BTStone

    BTStone

    Joined:
    Mar 10, 2012
    Posts:
    1,422
    bump
    Pinging @Schubkraft once again to get some insight on the difference on these calls :D