diff --git a/Assets/Tests/InputSystem/CoreTests_Actions_Interactions.cs b/Assets/Tests/InputSystem/CoreTests_Actions_Interactions.cs index 1712efcbfe..fb421b3e81 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions_Interactions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions_Interactions.cs @@ -669,6 +669,78 @@ public void Actions_CanPerformTapInteraction() Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); } + [Test] + [Category("Actions")] + public void Actions_CanPerformTapInteractionWithAnalogControls() + { + ResetTime(); + + var gamepad = InputSystem.AddDevice(); + + var action = new InputAction(binding: "/leftTrigger", type: InputActionType.Button, + interactions: "tap(duration=0.2)"); + + // This is the default value, which makes the release point to be 0.75 * 0.5 = 0.375. + InputSystem.settings.defaultButtonPressPoint = 0.5f; + + action.Enable(); + + currentTime = 0f; + + using (var trace = new InputActionTrace()) + { + trace.SubscribeTo(action); + + currentTime = 0.1f; + Set(gamepad.leftTrigger, 0.3f); + currentTime = 0.2f; + Set(gamepad.leftTrigger, 0.54f); + + Assert.That(trace, + Started(action, value: 0.54f, time: 0.2f)); + trace.Clear(); + + // Assert that a timeout will ocurr and a canceled event will be triggered. + currentTime = 0.5f; + Set(gamepad.leftTrigger, 0.9f); + Assert.That(trace, + Canceled(action)); + trace.Clear(); + + // Maintain a value above the press point for a while to assess that a start event is not triggered. + // This was the case where the tap interaction was previously re-starting. + currentTime = 1.2f; + Set(gamepad.leftTrigger, 0.52f); + + Assert.That(trace, Is.Empty); + + // Go below the release point so check that no cancel event is triggered, since it didn't start. + // This was the case where the tap interaction was previously re-starting and then would cancel after + // timeout. + currentTime = 1.5f; + Set(gamepad.leftTrigger, 0.2f); + + Assert.That(trace, Is.Empty); + + // Go above the press point again and check that a start event is triggered. + currentTime = 2.0f; + Set(gamepad.leftTrigger, 0.6f); + + Assert.That(trace, + Started(action)); + trace.Clear(); + + currentTime = 2.10f; + Set(gamepad.leftTrigger, 0.4f); + + // Check that the tap is performed. + currentTime = 2.15f; + Set(gamepad.leftTrigger, 0.2f); + Assert.That(trace, + Performed(action)); + } + } + [Test] [Category("Actions")] public void Actions_CanPerformDoubleTapInteraction() diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 269ed3455a..bf42c7171d 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -24,6 +24,7 @@ however, it has to be formatted properly to pass verification tests. - Fixed Gamepad stick up/down inputs that were not recognized in WebGL. [ISXB-1090](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1090) - Fixed PlayerInput component automatically switching away from the default ActionMap set to 'None'. - Fixed a console error being shown when targeting visionOS builds in 2022.3. +- Fixed a Tap Interaction issue with analog controls. The Tap interaction would keep re-starting after timeout. [ISXB-627](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-627) ## [1.14.0] - 2025-03-20 diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/Interactions/TapInteraction.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/Interactions/TapInteraction.cs index b76a78073f..510b1caef2 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/Interactions/TapInteraction.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/Interactions/TapInteraction.cs @@ -41,6 +41,7 @@ public class TapInteraction : IInputInteraction private float releasePointOrDefault => pressPointOrDefault * ButtonControl.s_GlobalDefaultButtonReleaseThreshold; private double m_TapStartTime; + bool canceledFromTimerExpired; ////TODO: make sure 2d doesn't move too far @@ -49,10 +50,15 @@ public void Process(ref InputInteractionContext context) if (context.timerHasExpired) { context.Canceled(); + // Cache the fact that we canceled the interaction due to a timer expiration. + canceledFromTimerExpired = true; return; } - if (context.isWaiting && context.ControlIsActuated(pressPointOrDefault)) + // Check if the control is actuated but avoid starting the interaction if it was canceled due to a timeout. + // Otherwise, we would start the interaction again immediately after it is canceled due to timeout, + // particularly in analog controls such as Gamepad stick or triggers. (ISXB-627) + if (context.isWaiting && context.ControlIsActuated(pressPointOrDefault) && !canceledFromTimerExpired) { m_TapStartTime = context.time; // Set timeout slightly after duration so that if tap comes in exactly at the expiration @@ -74,6 +80,12 @@ public void Process(ref InputInteractionContext context) context.Canceled(); } } + + // Once the control is released, we allow the interaction to be started again. + if (!context.ControlIsActuated(releasePointOrDefault)) + { + canceledFromTimerExpired = false; + } } public void Reset()