Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice

Question [New Input System] Mouse double tap doesn't work properly

Discussion in 'Input System' started by MikitaRyzhkouski, Feb 12, 2024.

  1. MikitaRyzhkouski

    MikitaRyzhkouski

    Joined:
    Jun 16, 2022
    Posts:
    8
    Hi. I'm using the new Input System (Unity 2021.3.33) and have a problem with mouse double click.My action looks like:
    Screenshot 2024-02-12 105200.png

    And the problem is that the double click applies even if I click two different elements, despite the different mouse position between the clicks. Press point param dosn't affect at all.
    mouseDoubleClickProblem.gif

    Is it a bug or correct behaviour? How can I limit my mouse double click to a radius of a few pixels?
     
  2. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 5, 2024
    Posts:
    459
    You should change the Max Tap Spacing value. That should limit the double tap distance between two taps if they considered double tap or two separate taps.
    Also always should go for
    .performed
    event if you are using events, because those fire once the parameters satisfy the interaction. The
    .started
    and
    .canceled
    may be called independently in order to keep tab on clicks and potential interactions.
    If you share code we can help with that too.
     
  3. MikitaRyzhkouski

    MikitaRyzhkouski

    Joined:
    Jun 16, 2022
    Posts:
    8
    According to the hint, this is the delay between clicks. If I reduce it, double-clicking won't be convenient either.
    upload_2024-2-12_14-7-48.png

    The code is simple:

    Code (CSharp):
    1. if (_input.Inventory.TakeOrUse.WasPerformedThisFrame())
    2. {
    3.     TryToUseFocusedItem();
    4. }
     
  4. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 5, 2024
    Posts:
    459
    Ouch, checked the source code, you're right, someone on the InputSystem team felt funny and renamed the tapDelay to tapSpacing. I'm afraid in this case you will need to code the distance restriction yourself. Probably a good idea if you create your own, working multitap interaction. And maybe submit a bug report about this craziness.
     
  5. MikitaRyzhkouski

    MikitaRyzhkouski

    Joined:
    Jun 16, 2022
    Posts:
    8
    Thank you for explanation! So now I know that it wasn't my mistake, the problem is in the Engine). Will try to make custome one.
     
  6. MikitaRyzhkouski

    MikitaRyzhkouski

    Joined:
    Jun 16, 2022
    Posts:
    8
    According to Unity source code, there is no any distance check at all...

    Code (CSharp):
    1.                 case TapPhase.None:
    2.                     if (context.ControlIsActuated(pressPointOrDefault))
    3.                     {
    4.                         m_CurrentTapPhase = TapPhase.WaitingForNextRelease;
    5.                         m_CurrentTapStartTime = context.time;
    6.                         context.Started();
    7.  
    8.                         var maxTapTime = tapTimeOrDefault;
    9.                         var maxDelayInBetween = tapDelayOrDefault;
    10.                         context.SetTimeout(maxTapTime);
    11.  
    12.                         // We'll be using multiple timeouts so set a total completion time that
    13.                         // effects the result of InputAction.GetTimeoutCompletionPercentage()
    14.                         // such that it accounts for the total time we allocate for the interaction
    15.                         // rather than only the time of one single timeout.
    16.                         context.SetTotalTimeoutCompletionTime(maxTapTime * tapCount + (tapCount - 1) * maxDelayInBetween);
    17.                     }
    18.                     break;
    19.  
    20.                 case TapPhase.WaitingForNextRelease:
    21.                     if (!context.ControlIsActuated(releasePointOrDefault))
    22.                     {
    23.                         if (context.time - m_CurrentTapStartTime <= tapTimeOrDefault)
    24.                         {
    25.                             ++m_CurrentTapCount;
    26.                             if (m_CurrentTapCount >= tapCount)
    27.                             {
    28.                                 context.Performed();
    29.                             }
    30.                             else
    31.                             {
    32.                                 m_CurrentTapPhase = TapPhase.WaitingForNextPress;
    33.                                 m_LastTapReleaseTime = context.time;
    34.                                 context.SetTimeout(tapDelayOrDefault);
    35.                             }
    36.                         }
    37.                         else
    38.                         {
    39.                             context.Canceled();
    40.                         }
    41.                     }
    42.                     break;
    43.  
    44.                 case TapPhase.WaitingForNextPress:
    45.                     if (context.ControlIsActuated(pressPointOrDefault))
    46.                     {
    47.                         if (context.time - m_LastTapReleaseTime <= tapDelayOrDefault)
    48.                         {
    49.                             m_CurrentTapPhase = TapPhase.WaitingForNextRelease;
    50.                             m_CurrentTapStartTime = context.time;
    51.                             context.SetTimeout(tapTimeOrDefault);
    52.                         }
    53.                         else
    54.                         {
    55.                             context.Canceled();
    56.                         }
    57.                     }
    58.                     break;
     
  7. AcidArrow

    AcidArrow

    Joined:
    May 20, 2010
    Posts:
    11,707
    Yep, that's why you were told to create your own that is based on that file but add a distance check (for our stuff, I believe that instead of distance we check if both taps / raycasts we do hit the same collider, and if they do then we consider that a double click / tap, worked well for us, but depends on how your game is set up).

    Why Unity's doesn't have a distance check by default is anyone's guess...
     
    Last edited: Feb 12, 2024
    Lurking-Ninja likes this.
  8. MikitaRyzhkouski

    MikitaRyzhkouski

    Joined:
    Jun 16, 2022
    Posts:
    8
    Sure, I already did it based on Unity sources. But I had to use
    Code (CSharp):
    1. Mouse.current.position.value
    to store a click position.
    The next code just doesn't work, throws an exception:
    Code (CSharp):
    1. context.action.ReadValue<Vector2>();
    2. context.ReadValue<Vector2>();
    Maybe because it's a button action type. But I don't need Value/Vector2 type for double click checking...
     
  9. MikitaRyzhkouski

    MikitaRyzhkouski

    Joined:
    Jun 16, 2022
    Posts:
    8
    The final code is tested in the editor and the Win build. I hope this will be useful to someone. And I will be grateful if anyone can tell me how to get rid of Input.mousePosition.

    Code (CSharp):
    1. namespace UnityEngine.InputSystem.Interactions
    2. {
    3. #if UNITY_EDITOR
    4.     using UnityEditor;
    5.     //Allow for the interaction to be utilized outside of Play Mode and so that it will actually show up as an option in the Input Manager
    6.     [InitializeOnLoad]
    7. #endif
    8.     [Scripting.Preserve, System.ComponentModel.DisplayName("StableMultiTap"), System.Serializable]
    9.     public class StableMultiTapInteraction : IInputInteraction<float>
    10.     {
    11. /// <summary>
    12.         /// The time in seconds within which the control needs to be pressed and released to perform the interaction.
    13.         /// </summary>
    14.         /// <remarks>
    15.         /// If this value is equal to or smaller than zero, the input system will use (<see cref="InputSettings.defaultTapTime"/>) instead.
    16.         /// </remarks>
    17.         [Tooltip("The maximum time (in seconds) allowed to elapse between pressing and releasing a control for it to register as a tap.")]
    18.         public float tapTime;
    19.  
    20.         /// <summary>
    21.         /// The time in seconds which is allowed to pass between taps.
    22.         /// </summary>
    23.         /// <remarks>
    24.         /// If this time is exceeded, the multi-tap interaction is canceled.
    25.         /// If this value is equal to or smaller than zero, the input system will use the duplicate value of <see cref="tapTime"/> instead.
    26.         /// </remarks>
    27.         [Tooltip("The maximum delay (in seconds) allowed between each tap. If this time is exceeded, the multi-tap is canceled.")]
    28.         public float tapDelay;
    29.  
    30.         /// <summary>
    31.         /// The number of taps required to perform the interaction.
    32.         /// </summary>
    33.         /// <remarks>
    34.         /// How many taps need to be performed in succession. Two means double-tap, three means triple-tap, and so on.
    35.         /// </remarks>
    36.         [Tooltip("How many taps need to be performed in succession. Two means double-tap, three means triple-tap, and so on.")]
    37.         public int tapCount = 2;
    38.  
    39.         /// <summary>
    40.         /// Magnitude threshold that must be crossed by an actuated control for the control to
    41.         /// be considered pressed.
    42.         /// </summary>
    43.         /// <seealso cref="InputControl.EvaluateMagnitude()"/>
    44.         public float pressPoint;
    45.        
    46.         public float maxTapDistance = 5;
    47.  
    48.         private float tapTimeOrDefault => tapTime > 0.0 ? tapTime : InputSystem.settings.defaultTapTime;
    49.         internal float tapDelayOrDefault => tapDelay > 0.0 ? tapDelay : InputSystem.settings.multiTapDelayTime;
    50.         private float pressPointOrDefault => pressPoint;
    51.         private float releasePointOrDefault => pressPointOrDefault;
    52.  
    53.         /// <inheritdoc />
    54.         public void Process(ref InputInteractionContext context)
    55.         {
    56.             if (context.timerHasExpired)
    57.             {
    58.                 // We use timers multiple times but no matter what, if they expire it means
    59.                 // that we didn't get input in time.
    60.                 context.Canceled();
    61.                 return;
    62.             }
    63.  
    64.             switch (m_CurrentTapPhase)
    65.             {
    66.                 case TapPhase.None:
    67.                     if (context.ControlIsActuated(pressPointOrDefault))
    68.                     {
    69.                         m_CurrentTapPhase = TapPhase.WaitingForNextRelease;
    70.                         m_CurrentTapStartTime = context.time;
    71.                         context.Started();
    72.  
    73.                         var maxTapTime = tapTimeOrDefault;
    74.                         var maxDelayInBetween = tapDelayOrDefault;
    75.                         context.SetTimeout(maxTapTime);
    76.  
    77.                         // We'll be using multiple timeouts so set a total completion time that
    78.                         // effects the result of InputAction.GetTimeoutCompletionPercentage()
    79.                         // such that it accounts for the total time we allocate for the interaction
    80.                         // rather than only the time of one single timeout.
    81.                         context.SetTotalTimeoutCompletionTime(maxTapTime * tapCount + (tapCount - 1) * maxDelayInBetween);
    82.                     }
    83.                     break;
    84.  
    85.                 case TapPhase.WaitingForNextRelease:
    86.                     if (!context.ControlIsActuated(releasePointOrDefault))
    87.                     {
    88.                         if (context.time - m_CurrentTapStartTime <= tapTimeOrDefault)
    89.                         {
    90.                             ++m_CurrentTapCount;
    91.                             if (m_CurrentTapCount >= tapCount && (Input.mousePosition - m_LastTapPosition).sqrMagnitude < maxTapDistance * maxTapDistance)
    92.                             {
    93.                                 context.Performed();
    94.                             }
    95.                             else
    96.                             {
    97.                                 m_CurrentTapPhase = TapPhase.WaitingForNextPress;
    98.                                 m_LastTapReleaseTime = context.time;
    99.                                 context.SetTimeout(tapDelayOrDefault);
    100.                                 m_LastTapPosition = Input.mousePosition;
    101.                             }
    102.                         }
    103.                         else
    104.                         {
    105.                             context.Canceled();
    106.                         }
    107.                     }
    108.                     break;
    109.  
    110.                 case TapPhase.WaitingForNextPress:
    111.                     if (context.ControlIsActuated(pressPointOrDefault))
    112.                     {
    113.                         if (context.time - m_LastTapReleaseTime <= tapDelayOrDefault)
    114.                         {
    115.                             m_CurrentTapPhase = TapPhase.WaitingForNextRelease;
    116.                             m_CurrentTapStartTime = context.time;
    117.                             context.SetTimeout(tapTimeOrDefault);
    118.                         }
    119.                         else
    120.                         {
    121.                             context.Canceled();
    122.                         }
    123.                     }
    124.                     break;
    125.             }
    126.         }
    127.        
    128.         /// <inheritdoc />
    129.         public void Reset()
    130.         {
    131.             m_CurrentTapPhase = TapPhase.None;
    132.             m_CurrentTapCount = 0;
    133.             m_CurrentTapStartTime = 0;
    134.             m_LastTapReleaseTime = 0;
    135.             m_LastTapPosition = Vector3.zero;
    136.         }
    137.  
    138.         private TapPhase m_CurrentTapPhase;
    139.         private int m_CurrentTapCount;
    140.         private double m_CurrentTapStartTime;
    141.         private double m_LastTapReleaseTime;
    142.         private Vector3 m_LastTapPosition = Vector3.zero;
    143.  
    144.         private enum TapPhase
    145.         {
    146.             None,
    147.             WaitingForNextRelease,
    148.             WaitingForNextPress,
    149.         }
    150.        
    151.         [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
    152.         private static void RegisterInteraction()
    153.         {
    154.             if (InputSystem.TryGetInteraction("StableMultiTap") == null)
    155.             {
    156.                 //For some reason if this is called again when it already exists, it permanently removees it from the drop-down options... So have to check first
    157.                 InputSystem.RegisterInteraction<StableMultiTapInteraction>("StableMultiTap");
    158.             }
    159.         }
    160.  
    161.         //Constructor will be called by our Editor [InitializeOnLoad] attribute when outside Play Mode
    162.         static StableMultiTapInteraction() => RegisterInteraction();
    163.     }
    164. }