Search Unity

Question Is it possible to reference InputActionAsset controls, even when inactive, for use in Update?

Discussion in 'Input System' started by BagoDev, Dec 2, 2020.

  1. BagoDev

    BagoDev

    Joined:
    Jan 2, 2017
    Posts:
    21
    This current project has 3rd party controllers/cameras that I am converting over to the new Input system 1.0.1+. I'd like to keep its functionality within the old controllers but gain the benefit of being able to switch action maps and such. Most of the controller code does their work in MonoBehavior Update.

    I am currently struggling to find a solution to converting these:

    if (Input.GetButton())
    if (Input.GetButtonDown())
    if (Input.GetButtonUp())


    I have been referencing the migration documentation to try and find a solution found here: https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/Migration.html , but I seem to be misunderstanding or missing something with the conversion for these specific controls.

    For the button controls, they state that one should use the ButtonControl props, but I am not sure the proper way to get a hold on these controls. Here is an example of what I tried, at the top of the 3rd party controller Update, that was most promising:

    var fire1Btn = _inputManager.PlayerInputs.Player.Fire1.activeControl as ButtonControl;


    *_inputManager is a MonoBehavior object that stores my ref to the PlayerInputs object which is my: IInputActionCollection, IDisposable class which has the InputActionAsset, this is the .cs file generated automatically from the .inputactions file by Unity.

    Then in the 3rd party controller code I replace the respective GetButtonDown GetButton GetButtonUp with this:

    if (fire1Btn != null && fire1Btn.wasPressedThisFrame)
    if (fire1Btn != null && fire1Btn.isPressed)
    if (fire1Btn != null && fire1Btn.wasReleasedThisFrame)


    This causes some issues though. .activeControl is null when actions are not firing and I don't think this behaves in the same way as the legacy controls. For example, when I go into the scene and start moving the player controller around, press forward, then release, the character continues to run forward, as if the value coming in from that control did not go back to 0 before the activeControl was nulled out again. There is also a .controls[] array on the object but this has a long list of controls that I could not easily distinguish which, if any, were the correct control.

    *edit1: after testing it appears as though wasReleasedThisFrame on an activeControl is never true, which I guess makes sense if this control object is only ever active when its being used.

    Is there a proper way to get a hold on these controls? Is this approach naive in some way? Any help or documentation on what to do here would be greatly appreciated.

    **edit2: I have tentatively answered my own question below in a reply on this thread and on the stackoverflow post

    https://stackoverflow.com/questions...actionasset-controls-even-when-inactive-for-u
     
    Last edited: Dec 3, 2020
  2. BagoDev

    BagoDev

    Joined:
    Jan 2, 2017
    Posts:
    21
    I resolved some of the held movement issues as problems with the null conditionals in the controller. Almost have this working with the activeControl now but the checking for null conditionals is still tricky in some spots and still having issues with certain key combo that get held.

    Would really love to know if there is a way to have access to a non-null bound control.
     
  3. SomeGuy22

    SomeGuy22

    Joined:
    Jun 3, 2011
    Posts:
    722
    A while back I posted some code which emulates the functionality of GetButton, GetButtonDown, and GetButtonUp for use in Update(). If it's useful to you, check out this thread. It may be of some use to you.

    Basically, what it does is wrap every Action into a data class called MInputActionWrapper. This new class can be used to access information such as GetAction, GetActionDown, and GetActionUp which are emulations of the old behavior but based on the Action state.

    Once you've completed the setup steps (importing the code, moving it to the top of your execution order, and setting the reference to your InputActionAsset), you can find the MInputActionWrapper you want by doing a string search. I give an example in the thread. Use GetInputActionWrapper("MyMap", "MyAction") and you've retrieved the wrapper associated with a specific action. Then in Update() you are free to check for GetAction, GetActionDown, etc. on that reference. There's more explanation in the thread, I highly recommend you read it as it shows step by step how to acquire the old behavior and the logic behind this approach. If you have more questions let me know, I'd be happy to answer. Good luck!
     
    BagoDev likes this.
  4. BagoDev

    BagoDev

    Joined:
    Jan 2, 2017
    Posts:
    21
    I appreciate the work you've done on the wrapper and for providing the resource. Your resource might well be what we need to get this working, though, half of the controller is using LateUpdate(), and as you mentioned in your post that would require additional work that we may not have time or allowance to implement.

    I already know what my boss will say as well, "if we can't replicate exactly how the off the shelf controllers work with no more than a few edits, we're gonna revert to the old input system for the sake of product updates and support.", even though I know when it comes time to do multi-input support and dynamic binds its gonna be a nightmare.

    Really my original question is more about ascertaining if its even possible to tap these controls in the new input system as the migration documentation makes it sound like there is some level of access to the controls that directly translates to the old system, for the button states specifically*.
     
  5. BagoDev

    BagoDev

    Joined:
    Jan 2, 2017
    Posts:
    21
    Based on my reading, tests, and responses from various individuals, I am going to tentatively assume the answer to my original question is a:

    No, you cannot tap into an input control when its not active

    However, I did manage to create a work around for this issue. Based on my original code from the question and specifically assuming you have the correct entries in your InputActionAsset, generated or created the c# input class from the asset file, instantiated the input class, and have reference to it, something like this should work:

    Code (CSharp):
    1. public class ButtonControlMask
    2. {
    3.     public bool wasPressedThisFrame { get; set; }
    4.     public bool isPressed { get; set; }
    5.     public bool wasReleasedThisFrame { get; set; }
    6. }
    7.  
    8. private bool _fire1BtnWasActiveLastFrame = false;
    9.  
    10. private void LateUpdate() // works in Update as well
    11. {
    12.    // set our controls
    13.     dynamic fire1Btn = _inputManager.PlayerInputs.Player.Fire1.activeControl as ButtonControl;
    14.     if (fire1Btn == null)
    15.     {
    16.        // never let our control be null, use mask and dynamics to prevent having to check for null
    17.         fire1Btn = new ButtonControlMask()
    18.         {
    19.            // _fire1BtnWasActiveLastFrame will check out last frames state
    20.             wasPressedThisFrame = false,
    21.             isPressed = false,
    22.             wasReleasedThisFrame = _fire1BtnWasActiveLastFrame
    23.         };
    24.  
    25.         // if it was active, and is null now, we can now reset our flag
    26.         if (_fire1BtnWasActiveLastFrame) _fire1BtnWasActiveLastFrame = false;
    27.     }
    28.     else
    29.     {
    30.        // we have an active control, set our active last frame flag
    31.         _fire1BtnWasActiveLastFrame = true;
    32.     }
    33.  
    34.     // migrated code below ---
    35.     if (fire1Btn.wasPressedThisFrame)  // This replaces Input.GetButtonDown()
    36.     if (fire1Btn.isPressed) // This replaces Input.GetButton()
    37.     if (fire1Btn.wasReleasedThisFrame) // This replaces Input.GetButtonUp()
    38. }

    Just using the .activeControl we have reliable values for our .wasPressedThisFrame and .isPressed, however, .wasReleasedThisFrame is unknown to us as the control goes null the instant its not being used. To go around this we store the previous frames state in the _fire1BtnWasActiveLastFrame variable based on if the control was null or not. Then in the next frame, if the control is null, _fire1BtnWasActiveLastFrame will inform us that .wasReleasedThisFrame should be true on our masked class for ButtonControl only for this frame.

    It's not pretty but this should allow for clean migration of Input.GetButtonDown, Input.GetButton, Input.GetButtonUp, within your legacy code without having to change or check logic.
     
  6. SomeGuy22

    SomeGuy22

    Joined:
    Jun 3, 2011
    Posts:
    722
    Well, really I would expect adjustments to the wrapper to be much less work than trying to find a specific binding within the action and casting it to a ButtonControl. But I understand since you are unfamiliar with the code the solutions may not seem obvious.

    I've never tried this method before, but it seems like you might have some issues with controls being missed due to not being able to know which one is currently affecting the action. I'm not totally sure, just guessing at this point. That fact that you're losing the control after its released makes it seem like you're jumping hoops when there's probably more direct methods that could be looked at in regards to the action itself. Essentially, what you want to be doing is probably taking a look at what the InputAction can do for you (callbacks, ReadValue) rather than relying on specific bindings. But that's just my take.

    If all you need is LateUpdate support then that's an easy fix. All that needs to be done is to just copy all the components of the Update running system and make them run in the LateUpdate method instead. A simple reversal of the "reset" part will do the trick. It's a naive approach, but it'll work since you say that you need a solution that takes minimal time. Take a look at this updated version of the script:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.InputSystem;
    5. using UnityEngine.EventSystems;
    6. using UnityEngine.Events;
    7. using UnityEngine.UI;
    8.  
    9. public class CInputState : MonoBehaviour
    10. {
    11.     [System.Serializable]
    12.     public class CInputActionWrapper
    13.     {
    14.         public InputAction action;
    15.         public bool actionDown = false;
    16.         public bool actionUp = false;
    17.         public bool actionHeld = false;
    18.  
    19.         public bool hadUpdateForDown = false;
    20.         public bool hadUpdateForUp = false;
    21.  
    22.         public bool GetActionDown()
    23.         {
    24.             return (actionDown && hadUpdateForDown);
    25.         }
    26.  
    27.         public bool GetActionUp()
    28.         {
    29.             return (actionUp && hadUpdateForUp);
    30.         }
    31.  
    32.         public bool GetAction()
    33.         {
    34.             return actionHeld;
    35.         }
    36.  
    37.         public bool lateDown = false;
    38.         public bool lateUp = false;
    39.         public bool lateHeld = false;
    40.  
    41.         public bool hadLateForDown = false;
    42.         public bool hadLateForUp = false;
    43.  
    44.         public bool GetActionDownLate()
    45.         {
    46.             return (lateDown && hadLateForDown);
    47.         }
    48.  
    49.         public bool GetActionUpLate()
    50.         {
    51.             return (lateUp && hadLateForUp);
    52.         }
    53.  
    54.         public bool GetActionLate()
    55.         {
    56.             return lateHeld;
    57.         }
    58.     }
    59.  
    60.     [System.Serializable]
    61.     public class CInputActionMapWrapper
    62.     {
    63.         public InputActionMap actionMap;
    64.         [HideInInspector]
    65.         public Dictionary<string, CInputActionWrapper> actionDict;
    66.     }
    67.  
    68.  
    69.     [System.Serializable]
    70.     public class MInputStats
    71.     {
    72.         public InputActionAsset inputMap;
    73.     }
    74.  
    75.     public MInputStats stats;
    76.  
    77.     [System.Serializable]
    78.     public class MInputReference
    79.     {
    80.         [HideInInspector]
    81.         public Dictionary<string, CInputActionMapWrapper> actionMapDict;
    82.     }
    83.  
    84.     public MInputReference reference;
    85.  
    86.     // Start is called before the first frame update
    87.     void Awake()
    88.     {
    89.         EnableAllActions();
    90.         CreateInputWrappers(stats.inputMap);
    91.     }
    92.  
    93.     void EnableAllActions()
    94.     {
    95.         if (stats.inputMap)
    96.         {
    97.             stats.inputMap.Enable();
    98.         }
    99.     }
    100.  
    101.     //DICTIONARY
    102.  
    103.     void CreateInputWrappers(InputActionAsset inMap)
    104.     {
    105.         if (reference.actionMapDict == null)
    106.         {
    107.             reference.actionMapDict = new Dictionary<string, CInputActionMapWrapper>();
    108.         }
    109.         reference.actionMapDict.Clear();
    110.  
    111.         if (inMap)
    112.         {
    113.             for (int i = 0; i < inMap.actionMaps.Count; i++)
    114.             {
    115.                 InputActionMap thisActionMap = inMap.actionMaps[i];
    116.  
    117.                 CInputActionMapWrapper newMapWrapper = new CInputActionMapWrapper();
    118.                 newMapWrapper.actionMap = thisActionMap;
    119.  
    120.                 if (newMapWrapper.actionDict == null)
    121.                 {
    122.                     newMapWrapper.actionDict = new Dictionary<string, CInputActionWrapper>();
    123.                 }
    124.                 newMapWrapper.actionDict.Clear();
    125.  
    126.                 for (int j = 0; j < thisActionMap.actions.Count; j++)
    127.                 {
    128.                     InputAction thisAction = thisActionMap.actions[j];
    129.  
    130.                     thisAction.performed += ctx => InputActionPerformed(thisActionMap.name, thisAction.name);
    131.                     thisAction.canceled += ctx2 => InputActionCanceled(thisActionMap.name, thisAction.name);
    132.  
    133.                     CInputActionWrapper newWrapper = new CInputActionWrapper();
    134.                     newWrapper.action = thisAction;
    135.  
    136.                     newMapWrapper.actionDict.Add(thisAction.name, newWrapper);
    137.                 }
    138.  
    139.                 reference.actionMapDict.Add(thisActionMap.name, newMapWrapper);
    140.             }
    141.         }
    142.     }
    143.  
    144.  
    145.  
    146.     public void InputActionPerformed(string mapName, string actionName)
    147.     {
    148.         //Debug.Log(mapName + " ___ " + actionName);
    149.         CInputActionWrapper performedWrapper = GetInputActionWrapper(mapName, actionName);
    150.         if (performedWrapper != null)
    151.         {
    152.             performedWrapper.actionDown = true;
    153.             performedWrapper.actionHeld = true;
    154.  
    155.             performedWrapper.lateDown = true;
    156.             performedWrapper.lateHeld = true;
    157.         }
    158.     }
    159.  
    160.     public void InputActionCanceled(string mapName, string actionName)
    161.     {
    162.         CInputActionWrapper performedWrapper = GetInputActionWrapper(mapName, actionName);
    163.         if (performedWrapper != null)
    164.         {
    165.             performedWrapper.actionUp = true;
    166.             performedWrapper.actionHeld = false;
    167.  
    168.             performedWrapper.lateUp = true;
    169.             performedWrapper.lateHeld = false;
    170.         }
    171.     }
    172.  
    173.     //RESETS
    174.  
    175.     private void Update()
    176.     {
    177.  
    178.         foreach (KeyValuePair<string, CInputActionMapWrapper> mapWrap in reference.actionMapDict)
    179.         {
    180.             foreach (KeyValuePair<string, CInputActionWrapper> actWrap in mapWrap.Value.actionDict)
    181.             {
    182.                 if (actWrap.Value.actionDown)
    183.                 {
    184.                     actWrap.Value.hadUpdateForDown = true;
    185.                 }
    186.                 if (actWrap.Value.actionUp)
    187.                 {
    188.                     actWrap.Value.hadUpdateForUp = true;
    189.                 }
    190.  
    191.                 if (actWrap.Value.hadLateForDown)
    192.                 {
    193.                     actWrap.Value.lateDown = false;
    194.                 }
    195.                 if (actWrap.Value.hadLateForUp)
    196.                 {
    197.                     actWrap.Value.lateUp = false;
    198.                 }
    199.             }
    200.         }
    201.     }
    202.  
    203.     private void LateUpdate()
    204.     {
    205.         foreach (KeyValuePair<string, CInputActionMapWrapper> mapWrap in reference.actionMapDict)
    206.         {
    207.             foreach (KeyValuePair<string, CInputActionWrapper> actWrap in mapWrap.Value.actionDict)
    208.             {
    209.                 if (actWrap.Value.hadUpdateForDown)
    210.                 {
    211.                     actWrap.Value.actionDown = false;
    212.                 }
    213.                 if (actWrap.Value.hadUpdateForUp)
    214.                 {
    215.                     actWrap.Value.actionUp = false;
    216.                 }
    217.  
    218.                 if (actWrap.Value.lateDown)
    219.                 {
    220.                     actWrap.Value.hadLateForDown = true;
    221.                 }
    222.                 if (actWrap.Value.lateUp)
    223.                 {
    224.                     actWrap.Value.hadLateForUp = true;
    225.                 }
    226.             }
    227.         }
    228.     }
    229.  
    230.     //INPUT WRAP
    231.  
    232.     public CInputActionWrapper GetInputActionWrapper(string mapName, string actionName)
    233.     {
    234.         CInputActionMapWrapper ret;
    235.         if (reference.actionMapDict.TryGetValue(mapName, out ret))
    236.         {
    237.             CInputActionWrapper act;
    238.             if (ret.actionDict.TryGetValue(actionName, out act))
    239.             {
    240.                 return act;
    241.             }
    242.         }
    243.  
    244.         Debug.Log("InputActionWrapper " + mapName + " __ " + actionName + " was not found!!");
    245.  
    246.         return null;
    247.     }
    248.  
    249.     public CInputActionMapWrapper GetInputActionMapWrapper(string mapName)
    250.     {
    251.         CInputActionMapWrapper ret;
    252.         if (reference.actionMapDict.TryGetValue(mapName, out ret))
    253.         {
    254.             return ret;
    255.         }
    256.  
    257.         Debug.Log("InputActionMapWrapper " + mapName + " was not found!!");
    258.  
    259.         return null;
    260.     }
    261.  
    262.     public Dictionary<string, CInputActionMapWrapper> GetAllInputMaps()
    263.     {
    264.         return reference.actionMapDict;
    265.     }
    266. }
    267.  
    Now, all you need to do is acquire the reference to the wrapper you want as before, and just use GetActionLate(), GetActionDownLate(), and GetActionUpLate() to get the old input behavior in LateUpdate(). You also have the option to use regular GetAction(), GetActionDown(), etc. like before for Update() usage. I even tested it myself to prove that it works:



    If all you need from an action is the value of, let's say, an axis; then you can just retrieve the InputActionWrapper.action and use ReadValue on that. Reading it from LateUpdate would've worked in the original script as well; it's only the new GetActionDown/Up that would have been missed from a LateUpdate timescale.

    You should be able to plug in the script and use it without any extra work. Let me know if that solves your problem or if you have more questions. Hope it helps!

    EDIT: I also changed the name of MInputState to CInputState in case there's any confusion about that. You need a reference to a CInputState instance like before in order to acquire the wrapper for a specific action.
     
  7. BagoDev

    BagoDev

    Joined:
    Jan 2, 2017
    Posts:
    21
    Just wanted to post an update on this issue as I have yet to find something official in solving this problem. Would really like @Rene-Damm to chime in on this if possible as it pertains directly to the documentations information on new input system conversion methods.

    **Update 12/8/2020**

    Based on more recent findings I found that .activeControl will run into issues on composites and I found a better way to tackle this issue. .wasReleasedThisFrame continues to be troublesome though. This newer solution uses an extension method to check press states, with only released state requiring some kind of tracking on previous frame state of a button. Here are the better ways to check state, though the wasReleasedThisFrame method did not work correctly:

    Code (CSharp):
    1. inputAction.ReadValue<float>() > 0f; // for isPressed state
    2. inputAction.triggered && inputAction.ReadValue<float>() > 0f; // for wasPressedThisFrame
    3. inputAction.triggered && inputAction.ReadValue<float>() == 0f; // for wasReleasedThisFrame ** Does not function properly **
    As for .wasReleasedThisFrame you'll have to implement some kind of tracking of previous frame similar to the one I did in my original post. I ended up using a dictionary on my input manager class to track the states, then passed it into the static extension method on checks.

    Code (CSharp):
    1. public static bool WasReleasedThisFrame(this InputAction inputAction, Dictionary<string, ButtonControlMask> maskStates)
    2. {
    3.     return maskStates.ContainsKey(inputAction.name) && maskStates[inputAction.name].wasReleasedThisFrame;
    4. }
    Bear in mind that if you check these values in some other mono-behavior update method (like how I do mine in my manager class), you could run into ScriptOrder issues unless you are checking the values in LateUpdate() like I am.

    I'm still on the hunting for a better solution for the .wasReleasedThisFrame problem.
     
    Last edited: Dec 8, 2020
  8. MUGIK

    MUGIK

    Joined:
    Jul 2, 2015
    Posts:
    481
    Input System v1.4.4 has next methods:

    Code (CSharp):
    1. inputAction.IsPressed()
    2. inputAction.WasPressedThisFrame()
    3. inputAction.WasReleasedThisFrame()

    I don't know in what version they were introduced.