Search Unity

MInputState - Emulation of the old GetButtonDown and Up functions in the new InputSystem

Discussion in 'Input System' started by SomeGuy22, Oct 25, 2019.

  1. SomeGuy22

    SomeGuy22

    Joined:
    Jun 3, 2011
    Posts:
    722
    This is not feedback or a question, this is public script for people to use to make their lives easier :)

    A custom wrapper which adds more functionality to the InputSystem. Using it, you are able to replace your references to the InputActionAsset with one of the wrapper classes provided by the script and you're able to do a string search to get your action by name, per InputMap. Once you have the wrapper class, you can call GetAction(), GetActionDown(), and GetActionUp() for replications of the old Input behavior. You also have full access to MInputActionWrapper.action which is the reference to the InputAction it was built from, letting you do use any of the new InputSystem functions such as InputAction.ReadValue().

    The new InputSystem's main way of relaying the input data to your code is through callbacks, which are convenient in many situations since they only run when the input is received. However, there are drawbacks, especially if you already have working code that ran through the old Input system. Take for example if you have a series of reference checks or conditions that must be done whenever you perform an action. If you need those same checks to be done for a second action, you now have to include them again in your callback. Same for a third action, and so on. Maybe we can just turn our conditions into a function and call it for all our actions? Great, if all the conditions are identical. But if you need some inputs to be dependent on certain conditions and others on different ones, you end up repeating a lot of steps that could've been solved with nesting.

    Using nested code in Update(), we are able to front-load logic which is shared by all of our inputs, and simply do the checks there as we check for OnButtonDown() as opposed to having to individually check for every callback. Obviously this will impact some projects more than others, but it is nice to have a convenient solution to this in addition to callbacks. It is also easier to port old OnKeyDown() code using state checking (polling) with some encapsulated emulation as opposed to migrating everything to callbacks.

    Adding a ton of callbacks that also check first-frame bools in your InputScript can get messy quickly. This solution handles all that automatically and lets you check for GetActionDown() using only 2 lines. Also, we avoid the problem of inputs accumulating when our conditions are not met to track the event. Take this script for example:

    Code (CSharp):
    1. public InputAction myAction;
    2.     public bool paused;
    3.     private bool m_MyActionHasBeenPerformed;
    4.     public void Awake()
    5.     {
    6.         myAction.performed += ctx => m_MyActionHasBeenPerformed = true;
    7.     }
    8.     public void Update()
    9.     {
    10.          if (m_MyActionHasBeenPerformed)
    11.             {
    12.                 //...Do stuff
    13.                 m_MyActionHasBeenPerformed = false;
    14.             }
    15.     }
    We are attempting to track the first frame when the action was performed. This is great and will work fine for the frame we want, but suppose we make one minor change like this:

    Code (CSharp):
    1. public InputAction myAction;
    2.     public bool paused;
    3.     private bool m_MyActionHasBeenPerformed;
    4.     public void Awake()
    5.     {
    6.         myAction.performed += ctx => m_MyActionHasBeenPerformed = true;
    7.     }
    8.     public void Update()
    9.     {
    10.         if(!paused) {
    11.             if (m_MyActionHasBeenPerformed)
    12.             {
    13.                 //...Do stuff
    14.                 m_MyActionHasBeenPerformed = false;
    15.             }
    16.         }
    17.     }
    Now we only perform this behavior if we are not paused. Uh oh, if we are paused then our actionBeenPerformed variable will be set to true anyways and //Do stuff will happen as soon as we un-pause, even if we didn't press the button! Yes, this could be solved by moving the paused check to inside the performed if statement, but then we run into a problem of scalability. If I have 20 buttons that all require this pause check, I must track 20 individual bools and also do the pause check on each individual bool check, instead of being able to nest them together. No fun. Also, if you move the paused check to the delegate instead, you again have a problem of scale, since you'd need to do that for every input. And that's also not considering the fact that you may need the input in several places, or could have many more checks with complicated conditions for when each action should happen. This is getting out of hand, and could be solved easily if we could just acquire the frame in which button is pressed or released--something we may end up doing a lot is best served with an automatic system that handles these bools internally, and also ensures "outside conditions" such as our paused check do not impact the result.

    You are always able to emulate GetButton() by taking the action reference and using InputAction.ReadValue() to check if the result is greater than some press point. Also, I believe now we have access to InputAction.triggered, which supposedly is true when the action is first pressed.

    However, there are drawbacks. For one, there is no bool for InputAction.canceled, meaning you cannot get the "up" behavior. Mixing InputAction.triggered with some custom bool tracking solution could also get messy if you combine both uses in your input script, which should just be able to access the data you need cleanly without jumping hoops. In addition, I believe triggered is tied to the Update() loop, getting that behavior to happen in a different frame-time such as FixedUpdate() is impossible. Minor adjustments to MInputState could allow for this. Other alternatives such as InputAction.phase only work for the current state of the action, and do not accumulate events between frames--if you pressed left click really fast (between frames) you would not be able to catch the result. Since this solution uses callbacks internally, it ensures that multiple "down" events between frames are treated as one press the next time you check.

    There is more discussion on these sorts of issues in this thread.

    It takes a peak into your InputActionAsset and creates a dictionary (a data structure with very fast searching, as it is done by the key string - the name of each ActionMap and Action) which is populated with all of your InputActions. Each InputAction is put into an MInputActionWrapper, which has 3 functions for getting your input's state and an "action" variable which is a reference to the original action. When this happens, a callback is set to the MInputState script so that whenever the action is triggered or canceled it sets a bool in the wrapper class.

    Since this script is intended to run first in the Script Execution Order, it runs Update() which checks all entries and ensures they have been seen. This is because we cannot guarantee that an Action happened before our Update() was called. For example, if we are running through the game and we receive the "left mouse" action after our player script has run, then MInputState would just catch the result on the next Update() runthrough and we would never receive OnActionDown(). Running MInputState first ensures that it will mark the input as received for a single frame only while still allowing other scripts to catch the state change. On LateUpdate() it checks if the input happened and simply sets the bool to false. This means that the GetActionDown() can only happen in Update(). The script would require modification to work in other threads.

    The Script


    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.InputSystem;
    5.  
    6. public class MInputState : MonoBehaviour
    7. {
    8.  
    9.    
    10.     [System.Serializable]
    11.     public class MInputActionWrapper
    12.     {
    13.         public InputAction action;
    14.         public bool actionDown = false;
    15.         public bool actionUp = false;
    16.         public bool actionHeld = false;
    17.  
    18.         public bool hadUpdateForDown = false;
    19.         public bool hadUpdateForUp = false;
    20.  
    21.         public bool GetActionDown()
    22.         {
    23.             return (actionDown && hadUpdateForDown);
    24.         }
    25.  
    26.         public bool GetActionUp()
    27.         {
    28.             return (actionUp && hadUpdateForUp);
    29.         }
    30.  
    31.         public bool GetAction()
    32.         {
    33.             return actionHeld;
    34.         }
    35.     }
    36.  
    37.     [System.Serializable]
    38.     public class MInputActionMapWrapper
    39.     {
    40.         [HideInInspector]
    41.         public Dictionary<string, MInputActionWrapper> actionDict;
    42.     }
    43.    
    44.     [System.Serializable]
    45.     public class MInputStats
    46.     {
    47.         public InputActionAsset inputMap;
    48.     }
    49.  
    50.     public MInputStats stats;
    51.  
    52.     [System.Serializable]
    53.     public class MInputReference
    54.     {
    55.         [HideInInspector]
    56.         public Dictionary<string, MInputActionMapWrapper> actionMapDict;
    57.     }
    58.  
    59.     public MInputReference reference;
    60.  
    61.     void Awake()
    62.     {
    63.         EnableAllActions();
    64.         CreateInputWrappers(stats.inputMap);
    65.     }
    66.  
    67.     void EnableAllActions()
    68.     {
    69.         if(stats.inputMap)
    70.         {
    71.             stats.inputMap.Enable();
    72.         }
    73.     }
    74.  
    75.     void CreateInputWrappers(InputActionAsset inMap)
    76.     {
    77.         if(reference.actionMapDict == null)
    78.         {
    79.             reference.actionMapDict = new Dictionary<string, MInputActionMapWrapper>();
    80.         }
    81.         reference.actionMapDict.Clear();
    82.  
    83.         if(inMap)
    84.         {
    85.             for(int i = 0; i < inMap.actionMaps.Count; i++)
    86.             {
    87.                 InputActionMap thisActionMap = inMap.actionMaps[i];
    88.  
    89.                 MInputActionMapWrapper newMapWrapper = new MInputActionMapWrapper();
    90.                 if(newMapWrapper.actionDict == null)
    91.                 {
    92.                     newMapWrapper.actionDict = new Dictionary<string, MInputActionWrapper>();
    93.                 }
    94.                 newMapWrapper.actionDict.Clear();
    95.  
    96.                 for(int j = 0; j < thisActionMap.actions.Count; j++)
    97.                 {
    98.                     InputAction thisAction = thisActionMap.actions[j];
    99.  
    100.                     thisAction.performed += ctx => InputActionPerformed(thisActionMap.name, thisAction.name);
    101.                     thisAction.canceled += ctx2 => InputActionCanceled(thisActionMap.name, thisAction.name);
    102.  
    103.                     MInputActionWrapper newWrapper = new MInputActionWrapper();
    104.                     newWrapper.action = thisAction;
    105.  
    106.                     newMapWrapper.actionDict.Add(thisAction.name, newWrapper);
    107.                 }
    108.  
    109.                 reference.actionMapDict.Add(thisActionMap.name, newMapWrapper);
    110.             }
    111.         }
    112.     }
    113.  
    114.    
    115.  
    116.     public void InputActionPerformed(string mapName, string actionName)
    117.     {
    118.         //Debug.Log(mapName + " ___ " + actionName);
    119.         MInputActionWrapper performedWrapper = GetInputActionWrapper(mapName, actionName);
    120.         if(performedWrapper != null)
    121.         {
    122.             performedWrapper.actionDown = true;
    123.             performedWrapper.actionHeld = true;
    124.         }
    125.     }
    126.  
    127.     public void InputActionCanceled(string mapName, string actionName)
    128.     {
    129.         MInputActionWrapper performedWrapper = GetInputActionWrapper(mapName, actionName);
    130.         if (performedWrapper != null)
    131.         {
    132.             performedWrapper.actionUp = true;
    133.             performedWrapper.actionHeld = false;
    134.         }
    135.     }
    136.  
    137.     private void Update()
    138.     {
    139.         foreach (KeyValuePair<string, MInputActionMapWrapper> mapWrap in reference.actionMapDict)
    140.         {
    141.             foreach (KeyValuePair<string, MInputActionWrapper> actWrap in mapWrap.Value.actionDict)
    142.             {
    143.                 if(actWrap.Value.actionDown)
    144.                 {
    145.                     actWrap.Value.hadUpdateForDown = true;
    146.                 }
    147.                 if(actWrap.Value.actionUp)
    148.                 {
    149.                     actWrap.Value.hadUpdateForUp = true;
    150.                 }
    151.             }
    152.         }
    153.     }
    154.  
    155.     private void LateUpdate()
    156.     {
    157.         foreach(KeyValuePair<string, MInputActionMapWrapper> mapWrap in reference.actionMapDict)
    158.         {
    159.             foreach(KeyValuePair<string, MInputActionWrapper> actWrap in mapWrap.Value.actionDict)
    160.             {
    161.                 if(actWrap.Value.hadUpdateForDown)
    162.                 {
    163.                     actWrap.Value.actionDown = false;
    164.                 }
    165.                 if(actWrap.Value.hadUpdateForUp)
    166.                 {
    167.                     actWrap.Value.actionUp = false;
    168.                 }
    169.             }
    170.         }
    171.     }
    172.  
    173.     public MInputActionWrapper GetInputActionWrapper(string mapName, string actionName)
    174.     {
    175.         MInputActionMapWrapper ret;
    176.         if (reference.actionMapDict.TryGetValue(mapName, out ret))
    177.         {
    178.             MInputActionWrapper act;
    179.             if (ret.actionDict.TryGetValue(actionName, out act))
    180.             {
    181.                 return act;
    182.             }
    183.         }
    184.  
    185.         Debug.Log("InputActionWrapper " + mapName + " __ " + actionName + " was not found!!");
    186.  
    187.         return null;
    188.     }
    189.  
    190. }
    191.  
    Setup

    1. Create a Monobehavior called MInputState.cs
    2. Copy-paste this code into the script
    3. In your project settings, add the MInputState to the top of your Script Execution Order - it must happen BEFORE any of your other scripts that use its functions
    4. Add the component to any "master gameobject" of choice - something that is persistent for the entirety of the scene and can be accessed easily by other scripts. You may want to use a tag to find it on Awake() and acquire the MInputState in your custom code
    5. Add your InputActionAsset to the "inputMap" variable in the component. It only works for one asset, but it supports as many InputActionMaps as you want (the subcategory in the asset UI).
    6. For best results ensure that the action you are polling is of the type "Button". I believe it should work with interaction modifiers, anything that still calls the performed delegate should be fine. However, for my cases I prefer to leave the interaction as None.
    Usage

    Once you have acquired a reference to the MInputState in your code, you can simply call MInputState.GetInputActionWrapper() to acquire a wrapper of your desired InputAction. The first parameter is the InputActionMap, the second is the name of the action itself. For example:



    To get the PrimaryAttack we call
    MInputState.GetInputActionWrapper("Gameplay", "PrimaryAttack")
    . You can do this in Awake() to save operations, though the string search should be pretty fast. Note that due to C# the full Type of the result you get is MInputState.MInputActionWrapper which is a mouthful to type. Just use var myWrapper = stateReference.GetInputActionWrapper() to make life easier.

    Now in Update() we can just call
    MInputActionWrapper.GetAction()
    to check if the action is currently held. We can call
    MInputActionWrapper.GetActionDown()
    to check if the action was performed this frame. We can call
    MInputActionWrapper.GetActionUp()
    to check if the action was released this frame. These happen on the performed and canceled delegates, and they happen on the first available start of the frame loop after the events occur. They are all bool results. Note for beginner programmers that the functions must be called on a reference of the wrapper, the class is not static. Meaning, you take the myWrapper variable we got from the last step and call the GetActionDown() function that it contains. If you need the value of say, a Vector2, you can acquire the InputAction (details on the Unity API) by getting
    MInputActionWrapper.action
    . There you have access to ReadValue() and anything else the action provides, such as the bindings. Here is an example script:

    Code (CSharp):
    1. var myWrapper = myStateReference.GetInputActionWrapper("Gameplay", "PrimaryAttack");
    2. if(myWrapper.GetActionDown())
    3. {
    4.     //Do stuff
    5. }
    Note that right now it only works in Update(). A few modifications are needed to make it work in FixedUpdate() or some other thread. If you are interested in this kind of behavior just let me know and I can recommend additions to the script.

    Conclusion

    I know a lot of people have been talking about making the jump to the new InputSystem so hopefully this helped you in making that transition easier. It's incredible that the InputSystem is versatile enough to allow emulations like this. It certainly won't be the most elegant solution to this problem, but should work in the meantime while the team finds additional ways to bring back the full feature set of the old system. I really appreciate the work done by Unity to overhaul Inputs in a way that is much more dynamic and straightforward, and this script should hopefully show how extendable this new system is. Not everyone that uses Unity are professionals, and neither will everyone want to spend countless hours translating old code into the callback equivalents. Unity is about ease of use and simplicity, as well as community support, which is why polling solutions like these are important to those who are used to the old method or who are new to programming with callbacks. This is my way of giving back to the community which has helped me countless times before.