Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Bug Composites + Hold Interaction not working properly

Discussion in 'Input System' started by Galandil, May 27, 2020.

  1. Galandil

    Galandil

    Joined:
    Oct 28, 2016
    Posts:
    13
    I stumbled upon this pretty annoying bug today, when using 1+ more buttons Composites with the Hold interaction.

    Tested it with 1, 2 and even 3 buttons composites (for the latter I wrote a custom class in order to add the option in the Inspector), and it shows up regardless of the devices used (pad/keyboard/mouse/mix of the formers).

    My setup is this -

    First, a simple script to be attached to any game object:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.InputSystem;
    3.  
    4. public class InputTest : MonoBehaviour {
    5.     [SerializeField]
    6.     private InputAction TestAction;
    7.  
    8.     private void Awake() {
    9.         TestAction.started += context => Debug.Log($"Started {context.action.name} at {context.time}");
    10.         TestAction.performed += context => Debug.Log($"Performed {context.action.name} at {context.time}");
    11.         TestAction.canceled += context => Debug.Log($"Canceled {context.action.name} at {context.time}");
    12.     }
    13.  
    14.     private void OnEnable() {
    15.         TestAction.Enable();
    16.     }
    17.  
    18.     private void OnDisable() {
    19.         TestAction.Disable();
    20.     }
    21. }
    Second, the bindings I created on the component:


    Third, the action itself is set to Button and the Hold interaction added, with a Hold Time of 2 seconds.

    With all the setup ready, when you enter play mode, if you press one of the buttons of the composite and then the other one (while holding the first one pressed), everything works fine, the started callback is called first, then the performed one (if you keep them pressed over 2 secs) and finally the canceled one.

    But, if you press both buttons simultaneously (at least, as far as a human can do that) and release them before the hold time is passed, this is what happens:

    The first two callbacks are correct, they get called when I press and then release both buttons simultaneously.
    Then, after an amount of time equal to start_time + hold_time, and without me using any device at all, the started callback is called again, immediately followed by the performed callback, and the last canceled callback is called only if and when I press one of the buttons of the composite.

    I tried a lot of different setups (using an Action Map from an Input Asset, creating the binding via code, setting the Hold interaction in the Bindings instead of the parent Action, etc.), and nothing seemed to fix this.

    Edit: forgot to say that this happens on 2019.3.13f1 and .14f1, with the 1.0.0 package.
     
    Last edited: May 27, 2020
  2. Rene-Damm

    Rene-Damm

    Unity Technologies

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    Hi, could you file a ticket with the Unity bug reporter? (in the editor, it's found under "Help >> Report a Bug..." in the main menu) Would like to have a closer look.
     
  3. Galandil

    Galandil

    Joined:
    Oct 28, 2016
    Posts:
    13
    Done, I used the same title as the forum thread.

    Please let us know if/when you discover anything about it, thank you. :)
     
    Rene-Damm likes this.
  4. Galandil

    Galandil

    Joined:
    Oct 28, 2016
    Posts:
    13
    Any news? A month has passed...
     
  5. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,282
    I'm having a similar issue.

    I have a custom composite that returns 1 if user pressed within a specific region of the screen and 0 otherwise.
    When using a Hold interaction with this composite, the "performed" event fires after the duration of the hold even if the mouse is no longer pressed.

    in other words. I press and release in the desired screen region, then 1 second later "performed" is incorrectly called.

    Issue doesn't happen when not using Composites :(

    here's the composite code:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.InputSystem;
    3. using UnityEngine.InputSystem.Layouts;
    4. using UnityEngine.InputSystem.Utilities;
    5. using UnityEngine.Scripting;
    6.  
    7. namespace MyInput {
    8.  
    9.     [Preserve]
    10.     [DisplayStringFormat("{position}/{button}")]
    11.     public class ScreenInputRegionComposite : InputBindingComposite<float> {
    12.  
    13.         #region Fields
    14.  
    15.         [InputControl(layout = "Axis")]
    16.         public int position;
    17.  
    18.         [InputControl(layout = "Button")]
    19.         public int button;
    20.  
    21.         public TextAnchor region = TextAnchor.LowerLeft;
    22.         public float regionWidth = 100;
    23.         public float regionHeight = 100;
    24.  
    25.         #endregion
    26.  
    27.         #region Public Methods
    28.  
    29.         public override float ReadValue(ref InputBindingCompositeContext context) {
    30.  
    31.             if (context.ReadValueAsButton(button)) {
    32.  
    33.                 var positionValue = context.ReadValue<Vector2, Vector2MagnitudeComparer>(position);
    34.  
    35.                 Rect regionRect = GetScreenRegionRect(region, new Vector2(regionWidth, regionHeight));
    36.  
    37.                 if (regionRect.Contains(positionValue)) {
    38.  
    39.                     return 1;
    40.                 }
    41.             }
    42.  
    43.             return 0;
    44.         }
    45.  
    46.         public override float EvaluateMagnitude(ref InputBindingCompositeContext context) {
    47.  
    48.             return ReadValue(ref context);
    49.         }
    50.  
    51.         #endregion
    52.  
    53.         #region Private Methods
    54.  
    55. #if UNITY_EDITOR
    56.         [UnityEditor.InitializeOnLoadMethod]
    57.         static void RegisterEditor() {
    58.  
    59.             Register();
    60.         }
    61. #endif
    62.  
    63.         [RuntimeInitializeOnLoadMethod]
    64.         private static void Register() {
    65.  
    66.             InputSystem.RegisterBindingComposite<ScreenInputRegionComposite>();
    67.         }
    68.  
    69.         private static Rect GetScreenRegionRect(TextAnchor region, Vector2 size) {
    70.  
    71.             float sw = Screen.width;
    72.             float sh = Screen.height;
    73.  
    74.             float shw = sw / 2;
    75.             float shh = sh / 2;
    76.  
    77.             switch (region) {
    78.  
    79.                 case TextAnchor.UpperLeft:
    80.                     return new Rect(0, sh - size.y, size.x, size.y);
    81.  
    82.                 case TextAnchor.UpperCenter:
    83.                     return new Rect(shw - size.x / 2, sh - size.y, size.x, size.y);
    84.  
    85.                 case TextAnchor.UpperRight:
    86.                     return new Rect(sw - size.x, sh - size.y, size.x, size.y);
    87.  
    88.                 case TextAnchor.MiddleLeft:
    89.                     return new Rect(0, shh - size.y / 2, size.x, size.y);
    90.  
    91.                 case TextAnchor.MiddleCenter:
    92.                     return new Rect(shw - size.x / 2, shh - size.y / 2, size.x, size.y);
    93.  
    94.                 case TextAnchor.MiddleRight:
    95.                     return new Rect(sw - size.x, shh - size.y / 2, size.x, size.y);
    96.  
    97.                 case TextAnchor.LowerLeft:
    98.                     return new Rect(0, 0, size.x, size.y);
    99.  
    100.                 case TextAnchor.LowerCenter:
    101.                     return new Rect(shw - size.x / 2, 0, size.x, size.y);
    102.  
    103.                 case TextAnchor.LowerRight:
    104.                     return new Rect(sw - size.x, 0, size.x, size.y);
    105.             }
    106.  
    107.             return new Rect(0, 0, sw, sh);
    108.         }
    109.  
    110.         #endregion
    111.     }
    112. }
    here's the action map:
    actionmap.png
     
  6. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,282
    So, I copied the default "HoldInteraction" script and modified it a little. Now it works fine :)
    The issue was calling "PerformedAndStayPerformed" when context.timerHasExpired. If I don't do this, works as expected for me.

    Code (CSharp):
    1. public void Process(ref InputInteractionContext context) {
    2.  
    3.     if (context.timerHasExpired) {
    4.  
    5.         Debug.Log("Timer expired! " + context.phase);
    6.  
    7.         //commenting out this fixes the issue!
    8.         //context.PerformedAndStayPerformed();
    9.         //return;
    10.     }
    11.  
    12.     switch (context.phase) {
    13.  
    14.         case InputActionPhase.Waiting:
    15.  
    16.             if (context.ControlIsActuated(pressPointOrDefault)) {
    17.  
    18.                 Debug.Log("Pressed " + context.time);
    19.  
    20.                 timePressed = context.time;
    21.  
    22.                 context.Started();
    23.                 context.SetTimeout(durationOrDefault);
    24.             }
    25.  
    26.             break;
    27.  
    28.         case InputActionPhase.Started:
    29.  
    30.             // If we've reached our hold time threshold, perform the hold.
    31.             // We do this regardless of what state the control changed to.
    32.  
    33.             if (context.time - timePressed >= durationOrDefault) {
    34.  
    35.                 Debug.Log("Perform from start");
    36.  
    37.                 context.PerformedAndStayPerformed();
    38.             }
    39.             else if (!context.ControlIsActuated()) {
    40.  
    41.                 Debug.Log("Cancel from start. No longer pressed!");
    42.  
    43.                 // Control is no longer actuated and we haven't performed a hold yet,
    44.                 // so cancel.
    45.                 context.Canceled();
    46.             }
    47.             break;
    48.  
    49.         case InputActionPhase.Performed:
    50.  
    51.             if (!context.ControlIsActuated(pressPointOrDefault)) {
    52.  
    53.                 Debug.Log("Cancel from perform. No longer pressed!");
    54.  
    55.                 context.Canceled();
    56.             }
    57.  
    58.             break;
    59.     }
    60. }
    61.  
    62. public void Reset() {
    63.  
    64.     timePressed = 0;
    65. }
    Here's the logs I get after pressing and releasing before duration:
    logs.png
    I guess maybe in the "context.timerHasExpired" statement you should probably also check the phase?
    Or maybe the bug is that the phase is still "Waiting" when context.timerHasExpired and using composites?
     
    Last edited: Jul 29, 2020
  7. Khang_Pham

    Khang_Pham

    Joined:
    Feb 28, 2021
    Posts:
    13
    For anyone stumbling across this issue:

    This was fixed in Input System 1.3.0 (available in 2019.4 and above).

    Make sure to update your Input System Version if you encounter this bug.