Search Unity

Resolved How to use hold/press interaction on the same button to trigger 2 different actions?

Discussion in 'Input System' started by TwiiK, Sep 24, 2020.

  1. TwiiK

    TwiiK

    Joined:
    Oct 23, 2007
    Posts:
    1,729
    There is actually 2 different problems I want to solve, and I'm currently unable to solve either one, but the one I described in the title is the most important one.

    First of all how do I trigger 2 different actions from the same button depending on the interaction (hold/press)? Think Dark Souls. In Dark Souls you sprint by holding down the B-button and you roll by pressing the B-button.

    I'm currently not able to get this work. I know how I would make it work by just coding the logic myself with timers etc., but this has to be the sort of thing the new input system is meant to solve for you, right?

    I came across this thread https://forum.unity.com/threads/hol...-performing-both-actions-when-holding.729887/, but that deals with 2 different interactions for the same action, not 2 different actions like I have. And I wasn't able to get that to work either way. Not to mention the image he posted doesn't exist anymore, which I feel is embarrassing for Unity's sake. That post is just over a year old and it's not usable as support due to essential information missing from it.

    And then there's the second problem: In Dark Souls both actions are hardcoded as 1 bindable action with hold/press behavior. So you're forced to bind both actions to the same key even when playing on keyboard. I want these actions to be freely bindable by the player. I was thinking I could allow my player to choose both the button/key for the action and also choose the interaction (hold/press) and that way let them freely configure it exactly how they want it and not have the limitation that Dark Souls has. But the actual rebinding is secondary, just getting this to work is more important. And like I said solving the problem in the topic title is the most important.

    This is my input actions at the moment:
    input-actions.png

    There you can see that I want to use Left Shift for Sprint and Space for Roll on keyboard, while I want to use the B-button for both actions on gamepad.

    I've set the B-Button to Hold interaction for the Sprint action as shown and I've added a Press interaction for the Roll action. I've done the same for the keyboard keys.

    So far in code I have this:
    Code (CSharp):
    1. _inputActions.ActionMap.Sprint.performed += context => {
    2.     if (context.interaction is HoldInteraction) {
    3.         SprintInput(true);
    4.     }
    5. };
    6. _inputActions.ActionMap.Sprint.canceled += context => {
    7.     if (context.interaction is HoldInteraction) {
    8.         SprintInput(false);
    9.     }
    10. };
    11. _inputActions.ActionMap.Roll.performed += context => {
    12.     if (context.interaction is PressInteraction) {
    13.         RollInput();
    14.     }
    15. };
    But this does not work at all:
    • If I tap the B-button then both RollInput() and SprintInput(false) are called. This is fine.
    • If I hold the B-Button then both SprintInput(true) and RollInput() are called. RollInput() should no be called here.
    • If I release the B-Button after a hold then nothing happens. After SprintInput(true) has been called I am unable to stop sprinting. SprintInput(false) is never called.
    • On Keyboard I obviously don't have the problem that both sprint and roll are called at the same time as they are on different keys, but I'm still unable to stop sprinting.
    I've looked at the samples included with the package and none of them are helpful from what I could tell.
     
  2. TwiiK

    TwiiK

    Joined:
    Oct 23, 2007
    Posts:
    1,729
    Thanks to information in this thread https://forum.unity.com/threads/new-input-system-how-to-use-the-hold-interaction.605587/ I was able to solve this.

    First of all I removed all the interactions from my sprint and roll actions in the input system because they didn't work and then I changed my code to be like this:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.InputSystem;
    3.  
    4. namespace DarkSouls {
    5.  
    6.     public class Player : MonoBehaviour {
    7.  
    8.         public CharacterController controller;
    9.  
    10.         private InputActions _inputActions;
    11.  
    12.         [Tooltip("The time in seconds before sprint is started if the sprint button is held down. This is if both sprint and roll is bound to the same button.")]
    13.         public float holdTimeBeforeSprintIsStarted = 0.2f;
    14.         private bool _rollButtonPressed;
    15.         private bool _sprintButtonHeld;
    16.         private float _sprintButtonHoldTimer;
    17.  
    18.         private void OnEnable() {
    19.             _inputActions.Enable();
    20.         }
    21.  
    22.         private void OnDisable() {
    23.             _inputActions.Disable();
    24.         }
    25.  
    26.         private void Awake() {
    27.             _inputActions = new InputActions();
    28.             _inputActions.ActionMap.Sprint.started += SprintOrRollInput;
    29.             _inputActions.ActionMap.Sprint.canceled += SprintOrRollInput;
    30.             _inputActions.ActionMap.Roll.started += SprintOrRollInput;
    31.             _inputActions.ActionMap.Roll.canceled += SprintOrRollInput;
    32.         }
    33.  
    34.         private void Update() {
    35.             // Continously read the hold state for the sprint action.
    36.             // It returns 1 when held and 0 when not held so we convert that to a boolean for easier usability.
    37.             _sprintButtonHeld = System.Convert.ToBoolean(_inputActions.ActionMap.Sprint.ReadValue<float>());
    38.          
    39.             if (_sprintButtonHeld) {
    40.                 _sprintButtonHoldTimer += Time.deltaTime;
    41.                 if (_sprintButtonHoldTimer > holdTimeBeforeSprintIsStarted) {
    42.                     controller.SprintingStart();
    43.                 }
    44.             }
    45.         }
    46.  
    47.         private void SprintOrRollInput(InputAction.CallbackContext context) {
    48.             if (context.started == true) {
    49.                 if (context.action == _inputActions.ActionMap.Sprint) {
    50.                     _sprintButtonHoldTimer = 0;
    51.                 }
    52.                 if (context.action == _inputActions.ActionMap.Roll) {
    53.                     _rollButtonPressed = true;
    54.                     // If we're holding down the sprint button when we press the roll button
    55.                     // then just roll immediately because I couldn't get it to work otherwise.
    56.                     if (_sprintButtonHeld) {
    57.                         controller.Roll();
    58.                         _rollButtonPressed = false;
    59.                     }
    60.                 }
    61.             }
    62.  
    63.             if (context.canceled == true) {
    64.                 if (_sprintButtonHeld && _rollButtonPressed) {
    65.                     if (_sprintButtonHoldTimer > holdTimeBeforeSprintIsStarted) {
    66.                         controller.SprintingStop();
    67.                     }
    68.                     else {
    69.                         controller.Roll();
    70.                     }
    71.                     _rollButtonPressed = false;
    72.                 }
    73.                 else if (_rollButtonPressed) {
    74.                     controller.Roll();
    75.                     _rollButtonPressed = false;
    76.                 }
    77.                 else {
    78.                     controller.SprintingStop();
    79.                 }
    80.             }
    81.         }
    82.     }
    83. }
    The code is convoluted, but it works, and it works better than Dark Souls.

    I learned quite a bit from that other thread. For example I learned the syntax I used above for passing the context object into the method*. I also learned that the Hold interaction doesn't work how I expected it to at all. The method you put in the callback is still only called once with Hold, same as a Press, not continuously as I thought. So you seemingly still have to implement the logic for detecting if the button is pressed, held or released yourself, same as with the old input system.

    What I have now works better than Dark Souls because I'm able to have Roll and Sprint on different keys on keyboard while still having them on the same button on the gamepad. And of course they could be on the same key on keyboard or on different buttons on gamepad as I've done nothing to not make it device agnostic. It's still only checking the actions, and not the bindings.

    There's still one thing that bothers me though and that is the fact that on keyboard where there's currently separate keys for the actions you still only roll when you release the spacebar and it still takes 0.2 seconds for the sprint to initiate as if it was checking to see if it was a roll even though that's impossible as they are on separate keys. It's not a huge problem to begin with, and I'm sure it can be solved. I'm just happy to get this far. I know it could be solved by checking to see which binding triggered the action, but then it wouldn't be device agnostic anymore.

    * I have to say that the syntax for interacting with the new input system is completely foreign to me. I have no idea if this is native C# or some Unity gibberish, but what was wrong with Input.GetButton()? :p I'm guessing this is some sort of event/delegate ordeal, but does doing it this way actually have any benefits other than to look advanced and feel convoluted?

    Edit: Gah. Just found an obvious issue. Rolling with the gamepad button tied to both actions initiates the sprint after you roll. I'll fix that later and update the code here.

    Edit 2: While fixing the above issue I discovered two more issues with my initial approach. You would stop sprinting if you pressed the spacebar on the keyboard while still holding the shift key. And if you quickly tapped the left shift key and the spacebar together you wouldn't roll, only if you held the shift key longer than 0.2 seconds would you roll. This code is no longer visible here because I edited the post so you won't know and probably don't care what it was, but at least so far my new code has no known issues. However it's even more convoluted and I had to dive into the other thread to learn one more thing to get this to work, namely how to poll the input action directly from the Update method so that I could see if the sprint button was currently being held or not.

    This can surely (hopefully) be simplified a lot, but it at least works now and does everything I want it to. For now at least. If I wanted to support rebindable controls then this would surely need to be expanded upon, but by then I would hopefully know how to write it simpler as well.

    One more thing to note is that this new version of the code continously calls controller.SprintingStart() while the sprint button is held which looks and feels wrong, but all that method does at the moment is set an isSprinting bool to true if a couple of flags like isGrounded etc. are true so it doesn't really matter.

    Edit 3: I forgot the Awake method in the last code and I quickly refactored it a bit for my own future sake so I may as well post those changes here as well. I posted the entire script so you have the full context for how it's used.
     
    Last edited: Sep 26, 2020
    Ribeiro3k likes this.