Skip to content

Commit cca1e3b

Browse files
author
Rene Damm
authored
FIX: Timers on interactions sometimes triggering erroneously (case 1251231, #1474).
1 parent eeec99e commit cca1e3b

3 files changed

Lines changed: 81 additions & 18 deletions

File tree

Assets/Tests/InputSystem/CoreTests_Actions_Interactions.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,64 @@ public void Actions_HoldInteraction_DoesNotGetStuck_WhenHeldAndReleasedInSameEve
462462
}
463463
}
464464

465+
// https://fogbugz.unity3d.com/f/cases/1251231/
466+
[Test]
467+
[Category("Actions")]
468+
public void Actions_HoldInteraction_CanBePerformedWhenInvolvingMoreThanOneControl()
469+
{
470+
var keyboard = InputSystem.AddDevice<Keyboard>();
471+
InputSystem.AddDevice<Mouse>();
472+
473+
// Add several bindings just to ensure that if conflict resolution is in the mix,
474+
// things don't go sideways.
475+
476+
var action = new InputAction(interactions: "hold(duration=2)");
477+
action.AddCompositeBinding("ButtonWithOneModifier")
478+
.With("Modifier", "<Keyboard>/a")
479+
.With("Button", "<Keyboard>/s");
480+
action.AddCompositeBinding("ButtonWithOneModifier")
481+
.With("Modifier", "<Mouse>/leftButton")
482+
.With("Button", "<Mouse>/rightButton");
483+
action.AddCompositeBinding("ButtonWithOneModifier")
484+
.With("Modifier", "<Keyboard>/shift")
485+
.With("Button", "<Mouse>/rightButton");
486+
487+
action.Enable();
488+
489+
var startedCount = 0;
490+
var performedCount = 0;
491+
var canceledCount = 0;
492+
493+
action.started += _ => ++ startedCount;
494+
action.performed += _ => ++ performedCount;
495+
action.canceled += _ => ++ canceledCount;
496+
497+
InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.A));
498+
InputSystem.Update();
499+
InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.A, Key.S));
500+
InputSystem.Update();
501+
502+
Assert.That(startedCount, Is.EqualTo(1));
503+
Assert.That(performedCount, Is.Zero);
504+
Assert.That(canceledCount, Is.Zero);
505+
506+
// Release before hold time.
507+
InputSystem.QueueStateEvent(keyboard, default(KeyboardState));
508+
InputSystem.Update();
509+
510+
Assert.That(startedCount, Is.EqualTo(1));
511+
Assert.That(performedCount, Is.Zero);
512+
Assert.That(canceledCount, Is.EqualTo(1));
513+
514+
currentTime += 3;
515+
516+
InputSystem.Update();
517+
518+
Assert.That(startedCount, Is.EqualTo(1));
519+
Assert.That(performedCount, Is.Zero);
520+
Assert.That(canceledCount, Is.EqualTo(1));
521+
}
522+
465523
[Test]
466524
[Category("Actions")]
467525
public void Actions_ReleasedHoldInteractionIsCancelled_WithMultipleBindings()

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ however, it has to be formatted properly to pass verification tests.
1313
### Changed
1414

1515
- The artificial `ctrl`, `shift`, and `alt` controls (which combine the left and right controls into one) on the keyboard can now be written to and no longer throw `NotSupportedException` when trying to do so ([case 1340793](https://issuetracker.unity3d.com/issues/on-screen-button-errors-on-mouse-down-slash-up-when-its-control-path-is-set-to-control-keyboard)).
16-
- All devices are now resynced/reset in next update after entering play mode, this is needed to read current state of devices before any intentional input is provided ([case 1231907](https://issuetracker.unity3d.com/issues/mouse-coordinates-reported-as-00-until-the-first-move)).
16+
- All devices are now re-synced/reset in next update after entering play mode, this is needed to read current state of devices before any intentional input is provided ([case 1231907](https://issuetracker.unity3d.com/issues/mouse-coordinates-reported-as-00-until-the-first-move)).
1717
- Replaced `UnityLinkerBuildPipelineData.inputDirectory` with hardcoded `Temp` folder because `inputDirectory` is deprecated.
1818
- Deprecated `InputSettings.filterNoiseOnCurrent`. Now noise filtering is always enabled. Device only will become `.current` if any non-noise control have changed state.
1919

@@ -40,6 +40,8 @@ however, it has to be formatted properly to pass verification tests.
4040
- Fixed interactions such as `Press` not getting processed correctly when having multiple of them on different bindings of the same action and receiving simultaneous input on all of them ([case 1364667](https://issuetracker.unity3d.com/issues/new-input-system-stops-working-after-pressing-2-keyboard-buttons-at-the-same-time)).
4141
* If, for example, you bind the A and S key on the same action, put a `Press` interaction on both, and then press both keys, interactions would get missed or got stuck.
4242
- Fixed `InputAction.IsPressed`/`WasPressed`/`WasReleased` returning incorrect results when binding multiple buttons on the same action and pressing/releasing them simultaneously.
43+
- Fixed interactions involving timeouts (such as `HoldInteraction`) performing erroneous delayed triggers on actions when input is composed of multiple controls ([1251231](https://issuetracker.unity3d.com/issues/input-system-composites-hold-interaction-can-be-performed-when-no-keys-are-hold)).
44+
* For example, if you bind `Shift+B` using a `OneModifierComposite` and put a `HoldInteraction` on the binding, then depending on the order in which the keys are pressed, you would sometimes see the action spuriously getting triggered when in fact no input was received.
4345

4446
## [1.2.0] - 2021-10-22
4547

Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,9 @@ private bool IsConflictingInput(ref TriggerState trigger, int actionIndex)
13971397
if (actionState->bindingIndex != bindingWithHighestActuation)
13981398
{
13991399
// If there's an interaction currently driving the action, reset it.
1400+
// NOTE: This will also cancel an ongoing timer. So, say we're currently 0.5 seconds into
1401+
// a 1 second "Hold" when the user shifts to a different control, then this code here
1402+
// will *cancel* the current "Hold" and restart from scratch.
14001403
if (actionState->interactionIndex != kInvalidIndex)
14011404
ResetInteractionState(trigger.mapIndex, actionState->bindingIndex, actionState->interactionIndex);
14021405

@@ -1668,8 +1671,7 @@ internal void StartTimeout(float seconds, ref TriggerState trigger)
16681671
// If there's already a timeout running, cancel it first.
16691672
ref var interactionState = ref interactionStates[interactionIndex];
16701673
if (interactionState.isTimerRunning)
1671-
StopTimeout(trigger.mapIndex, interactionState.triggerControlIndex, trigger.bindingIndex,
1672-
interactionIndex);
1674+
StopTimeout(interactionIndex);
16731675

16741676
// Add new timeout.
16751677
manager.AddStateChangeMonitorTimeout(control, this, currentTime + seconds, monitorIndex,
@@ -1679,28 +1681,26 @@ internal void StartTimeout(float seconds, ref TriggerState trigger)
16791681
interactionState.isTimerRunning = true;
16801682
interactionState.timerStartTime = currentTime;
16811683
interactionState.timerDuration = seconds;
1684+
interactionState.timerMonitorIndex = monitorIndex;
16821685
}
16831686

1684-
private void StopTimeout(int mapIndex, int controlIndex, int bindingIndex, int interactionIndex)
1687+
private void StopTimeout(int interactionIndex)
16851688
{
1686-
Debug.Assert(mapIndex >= 0 && mapIndex < totalMapCount, "Map index out of range");
1687-
Debug.Assert(controlIndex >= 0 && controlIndex < totalControlCount, "Control index out of range");
16881689
Debug.Assert(interactionIndex >= 0 && interactionIndex < totalInteractionCount, "Interaction index out of range");
16891690

1690-
var manager = InputSystem.s_Manager;
1691-
var monitorIndex =
1692-
ToCombinedMapAndControlAndBindingIndex(mapIndex, controlIndex, bindingIndex);
1691+
ref var interactionState = ref interactionStates[interactionIndex];
16931692

1694-
manager.RemoveStateChangeMonitorTimeout(this, monitorIndex, interactionIndex);
1693+
var manager = InputSystem.s_Manager;
1694+
manager.RemoveStateChangeMonitorTimeout(this, interactionState.timerMonitorIndex, interactionIndex);
16951695

16961696
// Update state.
1697-
ref var interactionState = ref interactionStates[interactionIndex];
16981697
interactionState.isTimerRunning = false;
16991698
interactionState.totalTimeoutCompletionDone += interactionState.timerDuration;
17001699
interactionState.totalTimeoutCompletionTimeRemaining =
17011700
Mathf.Max(interactionState.totalTimeoutCompletionTimeRemaining - interactionState.timerDuration, 0);
17021701
interactionState.timerDuration = default;
17031702
interactionState.timerStartTime = default;
1703+
interactionState.timerMonitorIndex = default;
17041704
}
17051705

17061706
/// <summary>
@@ -1750,8 +1750,7 @@ internal void ChangePhaseOfInteraction(InputActionPhase newPhase, ref TriggerSta
17501750
// Any time an interaction changes phase, we cancel all pending timeouts.
17511751
ref var interactionState = ref interactionStates[interactionIndex];
17521752
if (interactionState.isTimerRunning)
1753-
StopTimeout(trigger.mapIndex, interactionState.triggerControlIndex, trigger.bindingIndex,
1754-
trigger.interactionIndex);
1753+
StopTimeout(trigger.interactionIndex);
17551754

17561755
// Update interaction state.
17571756
interactionState.phase = newPhase;
@@ -2201,10 +2200,7 @@ private void ResetInteractionState(int mapIndex, int bindingIndex, int interacti
22012200

22022201
// Clean up timer.
22032202
if (interactionStates[interactionIndex].isTimerRunning)
2204-
{
2205-
var controlIndex = interactionStates[interactionIndex].triggerControlIndex;
2206-
StopTimeout(mapIndex, controlIndex, bindingIndex, interactionIndex);
2207-
}
2203+
StopTimeout(interactionIndex);
22082204

22092205
// Reset state record.
22102206
interactionStates[interactionIndex] =
@@ -2696,7 +2692,7 @@ internal bool ReadValueAsButton(int bindingIndex, int controlIndex)
26962692
/// Records the current state of a single interaction attached to a binding.
26972693
/// Each interaction keeps track of its own trigger control and phase progression.
26982694
/// </summary>
2699-
[StructLayout(LayoutKind.Explicit, Size = 40)]
2695+
[StructLayout(LayoutKind.Explicit, Size = 48)]
27002696
internal struct InteractionState
27012697
{
27022698
[FieldOffset(0)] private ushort m_TriggerControlIndex;
@@ -2708,6 +2704,7 @@ internal struct InteractionState
27082704
[FieldOffset(24)] private double m_PerformedTime;
27092705
[FieldOffset(32)] private float m_TotalTimeoutCompletionTimeDone;
27102706
[FieldOffset(36)] private float m_TotalTimeoutCompletionTimeRemaining;
2707+
[FieldOffset(40)] private long m_TimerMonitorIndex;
27112708

27122709
public int triggerControlIndex
27132710
{
@@ -2757,6 +2754,12 @@ public float totalTimeoutCompletionTimeRemaining
27572754
set => m_TotalTimeoutCompletionTimeRemaining = value;
27582755
}
27592756

2757+
public long timerMonitorIndex
2758+
{
2759+
get => m_TimerMonitorIndex;
2760+
set => m_TimerMonitorIndex = value;
2761+
}
2762+
27602763
public bool isTimerRunning
27612764
{
27622765
get => ((Flags)m_Flags & Flags.TimerRunning) == Flags.TimerRunning;

0 commit comments

Comments
 (0)