Skip to content

Commit e1582e5

Browse files
author
Rene Damm
authored
CHANGE: Enforce started/performed/canceled cycle on button and value actions (#1053).
1 parent a4d953f commit e1582e5

File tree

10 files changed

+306
-144
lines changed

10 files changed

+306
-144
lines changed

Assets/Tests/InputSystem/CoreTests_Actions.cs

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,10 +1507,7 @@ public void Actions_WithMultipleBoundControls_CanHandleInteractionsThatTriggerOn
15071507

15081508
Release(gamepad.buttonNorth);
15091509

1510-
var actions = trace.ToArray();
1511-
Assert.That(actions, Has.Length.EqualTo(1));
1512-
Assert.That(actions[0].phase, Is.EqualTo(InputActionPhase.Performed));
1513-
Assert.That(actions[0].control, Is.SameAs(gamepad.buttonNorth));
1510+
Assert.That(trace, Started(action, gamepad.buttonNorth).AndThen(Performed(action, gamepad.buttonNorth)).AndThen(Canceled(action, gamepad.buttonNorth)));
15141511

15151512
trace.Clear();
15161513

@@ -1522,10 +1519,7 @@ public void Actions_WithMultipleBoundControls_CanHandleInteractionsThatTriggerOn
15221519

15231520
Release(gamepad.buttonNorth);
15241521

1525-
actions = trace.ToArray();
1526-
Assert.That(actions, Has.Length.EqualTo(1));
1527-
Assert.That(actions[0].phase, Is.EqualTo(InputActionPhase.Performed));
1528-
Assert.That(actions[0].control, Is.SameAs(gamepad.buttonNorth));
1522+
Assert.That(trace, Started(action, gamepad.buttonNorth).AndThen(Performed(action, gamepad.buttonNorth)).AndThen(Canceled(action, gamepad.buttonNorth)));
15291523

15301524
trace.Clear();
15311525

@@ -1536,10 +1530,7 @@ public void Actions_WithMultipleBoundControls_CanHandleInteractionsThatTriggerOn
15361530

15371531
Release(keyboard.aKey);
15381532

1539-
actions = trace.ToArray();
1540-
Assert.That(actions, Has.Length.EqualTo(1));
1541-
Assert.That(actions[0].phase, Is.EqualTo(InputActionPhase.Performed));
1542-
Assert.That(actions[0].control, Is.SameAs(keyboard.aKey));
1533+
Assert.That(trace, Started(action, keyboard.aKey).AndThen(Performed(action, keyboard.aKey)).AndThen(Canceled(action, keyboard.aKey)));
15431534

15441535
trace.Clear();
15451536

@@ -1558,19 +1549,13 @@ public void Actions_WithMultipleBoundControls_CanHandleInteractionsThatTriggerOn
15581549

15591550
Release(keyboard.aKey);
15601551

1561-
actions = trace.ToArray();
1562-
Assert.That(actions, Has.Length.EqualTo(1));
1563-
Assert.That(actions[0].phase, Is.EqualTo(InputActionPhase.Performed));
1564-
Assert.That(actions[0].control, Is.SameAs(keyboard.aKey));
1552+
Assert.That(trace, Started(action, keyboard.aKey).AndThen(Performed(action, keyboard.aKey)).AndThen(Canceled(action, keyboard.aKey)));
15651553

15661554
trace.Clear();
15671555

15681556
Release(gamepad.buttonNorth);
15691557

1570-
actions = trace.ToArray();
1571-
Assert.That(actions, Has.Length.EqualTo(1));
1572-
Assert.That(actions[0].phase, Is.EqualTo(InputActionPhase.Performed));
1573-
Assert.That(actions[0].control, Is.SameAs(gamepad.buttonNorth));
1558+
Assert.That(trace, Started(action, gamepad.buttonNorth).AndThen(Performed(action, gamepad.buttonNorth)).AndThen(Canceled(action, gamepad.buttonNorth)));
15741559
}
15751560
}
15761561

Assets/Tests/InputSystem/CoreTests_Actions_Interactions.cs

Lines changed: 113 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,90 @@
44
using UnityEngine.InputSystem.Interactions;
55
using UnityEngine.InputSystem.LowLevel;
66
using UnityEngine.InputSystem.Utilities;
7+
using UnityEngine.InputSystem.XR;
78
using UnityEngine.Scripting;
89

910
internal partial class CoreTests
1011
{
12+
[Preserve]
13+
class InteractionThatOnlyPerforms : IInputInteraction<float>
14+
{
15+
public bool stayPerformed;
16+
17+
public void Process(ref InputInteractionContext context)
18+
{
19+
if (context.ControlIsActuated())
20+
{
21+
if (stayPerformed)
22+
context.PerformedAndStayPerformed();
23+
else
24+
context.Performed();
25+
}
26+
}
27+
28+
public void Reset()
29+
{
30+
}
31+
}
32+
33+
[Test]
34+
[Category("Actions")]
35+
public void Actions_StartedAndCanceledAreEnforcedImplicitly()
36+
{
37+
var gamepad = InputSystem.AddDevice<Gamepad>();
38+
39+
InputSystem.RegisterInteraction<InteractionThatOnlyPerforms>();
40+
41+
var action1 = new InputAction(name: "action1", type: InputActionType.Button, binding: "<Gamepad>/buttonSouth", interactions: "interactionThatOnlyPerforms(stayPerformed=true)");
42+
var action2 = new InputAction(name: "action2", type: InputActionType.Button, binding: "<Gamepad>/buttonSouth", interactions: "interactionThatOnlyPerforms(stayPerformed=false)");
43+
var action3 = new InputAction(name: "action3", type: InputActionType.Button, binding: "<Gamepad>/buttonSouth");
44+
var action4 = new InputAction(name: "action4", type: InputActionType.Value, binding: "<Gamepad>/buttonSouth");
45+
46+
// Pass-Through is special (as always).
47+
var action5 = new InputAction(name: "action5", type: InputActionType.PassThrough, binding: "<Gamepad>/buttonSouth");
48+
var action6 = new InputAction(name: "action6", type: InputActionType.PassThrough, binding: "<Gamepad>/buttonSouth", interactions: "press");
49+
50+
action1.Enable();
51+
action2.Enable();
52+
action3.Enable();
53+
action4.Enable();
54+
action5.Enable();
55+
action6.Enable();
56+
57+
using (var trace1 = new InputActionTrace(action1))
58+
using (var trace2 = new InputActionTrace(action2))
59+
using (var trace3 = new InputActionTrace(action3))
60+
using (var trace4 = new InputActionTrace(action4))
61+
using (var trace5 = new InputActionTrace(action5))
62+
using (var trace6 = new InputActionTrace(action6))
63+
{
64+
Press(gamepad.buttonSouth);
65+
66+
Assert.That(trace1, Started(action1).AndThen(Performed(action1)));
67+
Assert.That(trace2, Started(action2).AndThen(Performed(action2)).AndThen(Canceled(action2)));
68+
Assert.That(trace3, Started(action3).AndThen(Performed(action3)));
69+
Assert.That(trace4, Started(action4).AndThen(Performed(action4)));
70+
Assert.That(trace5, Performed(action5));
71+
Assert.That(trace6, Started(action6).AndThen(Performed(action6)));
72+
73+
trace1.Clear();
74+
trace2.Clear();
75+
trace3.Clear();
76+
trace4.Clear();
77+
trace5.Clear();
78+
trace6.Clear();
79+
80+
Release(gamepad.buttonSouth);
81+
82+
Assert.That(trace1, Is.Empty);
83+
Assert.That(trace2, Is.Empty);
84+
Assert.That(trace3, Canceled(action3));
85+
Assert.That(trace4, Canceled(action4));
86+
Assert.That(trace5, Performed(action5)); // Any value change performs.
87+
Assert.That(trace6, Canceled(action6));
88+
}
89+
}
90+
1191
[Test]
1292
[Category("Actions")]
1393
public void Actions_WhenTransitionFromOneInteractionToNext_GetCallbacks()
@@ -49,7 +129,7 @@ public void Actions_CanPerformPressInteraction()
49129
InputSystem.AddDevice<Keyboard>();
50130

51131
// Test all three press behaviors concurrently.
52-
var pressOnlyAction = new InputAction("PressOnly", binding: "<Gamepad>/buttonSouth", interactions: "press");
132+
var pressOnlyAction = new InputAction("PressOnly", binding: "<Gamepad>/buttonSouth", interactions: "press(behavior=0)");
53133
pressOnlyAction.AddBinding("<Keyboard>/a");
54134
var releaseOnlyAction = new InputAction("ReleaseOnly", binding: "<Gamepad>/buttonSouth", interactions: "press(behavior=1)");
55135
releaseOnlyAction.AddBinding("<Keyboard>/s");
@@ -60,83 +140,50 @@ public void Actions_CanPerformPressInteraction()
60140
releaseOnlyAction.Enable();
61141
pressAndReleaseAction.Enable();
62142

63-
using (var trace = new InputActionTrace())
143+
using (var pressOnly = new InputActionTrace(pressOnlyAction))
144+
using (var releaseOnly = new InputActionTrace(releaseOnlyAction))
145+
using (var pressAndRelease = new InputActionTrace(pressAndReleaseAction))
64146
{
65-
trace.SubscribeToAll();
66-
67147
runtime.currentTime = 1;
68148
Press(gamepad.buttonSouth);
69149

70-
var actions = trace.ToArray();
71-
Assert.That(actions, Has.Length.EqualTo(5));
72-
Assert.That(actions,
73-
Has.Exactly(1).With.Property("action").SameAs(pressOnlyAction).And.With.Property("phase")
74-
.EqualTo(InputActionPhase.Started).And.With.Property("duration")
75-
.EqualTo(0));
76-
Assert.That(actions,
77-
Has.Exactly(1).With.Property("action").SameAs(pressOnlyAction).And.With.Property("phase")
78-
.EqualTo(InputActionPhase.Performed).And.With.Property("duration")
79-
.EqualTo(0));
80-
Assert.That(actions,
81-
Has.Exactly(1).With.Property("action").SameAs(pressAndReleaseAction).And.With.Property("phase")
82-
.EqualTo(InputActionPhase.Started).And.With.Property("duration")
83-
.EqualTo(0));
84-
Assert.That(actions,
85-
Has.Exactly(1).With.Property("action").SameAs(pressAndReleaseAction).And.With.Property("phase")
86-
.EqualTo(InputActionPhase.Performed).And.With.Property("duration")
87-
.EqualTo(0));
88-
Assert.That(actions,
89-
Has.Exactly(1).With.Property("action").SameAs(releaseOnlyAction).And.With.Property("phase")
90-
.EqualTo(InputActionPhase.Started).And.With.Property("duration")
91-
.EqualTo(0));
150+
Assert.That(pressOnly,
151+
Started<PressInteraction>(pressOnlyAction, gamepad.buttonSouth, value: 1.0, time: 1)
152+
.AndThen(Performed<PressInteraction>(pressOnlyAction, gamepad.buttonSouth, time: 1, duration: 0, value: 1.0)));
153+
Assert.That(releaseOnly, Started<PressInteraction>(releaseOnlyAction, gamepad.buttonSouth, time: 1, value: 1.0));
154+
Assert.That(pressAndRelease,
155+
Started<PressInteraction>(pressAndReleaseAction, gamepad.buttonSouth, time: 1, value: 1.0)
156+
.AndThen(Performed<PressInteraction>(pressAndReleaseAction, gamepad.buttonSouth, time: 1, duration: 0, value: 1.0)));
92157

93-
trace.Clear();
158+
pressOnly.Clear();
159+
releaseOnly.Clear();
160+
pressAndRelease.Clear();
94161

95162
runtime.currentTime = 2;
96163
Release(gamepad.buttonSouth);
97164

98-
actions = trace.ToArray();
99-
Assert.That(actions, Has.Length.EqualTo(3));
100-
Assert.That(actions,
101-
Has.Exactly(1).With.Property("action").SameAs(releaseOnlyAction).And.With.Property("phase")
102-
.EqualTo(InputActionPhase.Performed).And.With.Property("duration")
103-
.EqualTo(1));
104-
Assert.That(actions,
105-
Has.Exactly(1).With.Property("action").SameAs(pressAndReleaseAction).And.With.Property("phase")
106-
.EqualTo(InputActionPhase.Started).And.With.Property("duration")
107-
.EqualTo(0));
108-
Assert.That(actions,
109-
Has.Exactly(1).With.Property("action").SameAs(pressAndReleaseAction).And.With.Property("phase")
110-
.EqualTo(InputActionPhase.Performed).And.With.Property("duration")
111-
.EqualTo(0));
165+
Assert.That(pressOnly, Canceled<PressInteraction>(pressOnlyAction, gamepad.buttonSouth, value: 0.0, time: 2, duration: 1));
166+
Assert.That(releaseOnly,
167+
Performed<PressInteraction>(releaseOnlyAction, gamepad.buttonSouth, value: 0.0, time: 2, duration: 1)
168+
.AndThen(Canceled<PressInteraction>(releaseOnlyAction, gamepad.buttonSouth, value: 0.0, time: 2, duration: 1)));
169+
Assert.That(pressAndRelease,
170+
Performed<PressInteraction>(pressAndReleaseAction, gamepad.buttonSouth, value: 0.0, time: 2, duration: 1)
171+
.AndThen(Canceled<PressInteraction>(pressAndReleaseAction, gamepad.buttonSouth, value: 0.0, time: 2, duration: 1)));
112172

113-
trace.Clear();
173+
pressOnly.Clear();
174+
releaseOnly.Clear();
175+
pressAndRelease.Clear();
114176

115177
runtime.currentTime = 5;
116178
Press(gamepad.buttonSouth);
117179

118-
actions = trace.ToArray();
119-
Assert.That(actions, Has.Length.EqualTo(5));
120-
Assert.That(actions,
121-
Has.Exactly(1).With.Property("action").SameAs(pressOnlyAction).And.With.Property("phase")
122-
.EqualTo(InputActionPhase.Started).And.With.Property("duration")
123-
.EqualTo(0));
124-
Assert.That(actions,
125-
Has.Exactly(1).With.Property("action").SameAs(pressOnlyAction).And.With.Property("phase")
126-
.EqualTo(InputActionPhase.Performed).And.With.Property("duration")
127-
.EqualTo(0));
128-
Assert.That(actions,
129-
Has.Exactly(1).With.Property("action").SameAs(pressAndReleaseAction).And.With.Property("phase")
130-
.EqualTo(InputActionPhase.Started).And.With.Property("duration")
131-
.EqualTo(0));
132-
Assert.That(actions,
133-
Has.Exactly(1).With.Property("action").SameAs(pressAndReleaseAction).And.With.Property("phase")
134-
.EqualTo(InputActionPhase.Performed).And.With.Property("duration")
135-
.EqualTo(0));
136-
Assert.That(actions,
137-
Has.Exactly(1).With.Property("action").SameAs(releaseOnlyAction).And.With.Property("phase")
138-
.EqualTo(InputActionPhase.Started).And.With.Property("duration")
139-
.EqualTo(0));
180+
Assert.That(pressOnly,
181+
Started<PressInteraction>(pressOnlyAction, gamepad.buttonSouth, value: 1.0, time: 5)
182+
.AndThen(Performed<PressInteraction>(pressOnlyAction, gamepad.buttonSouth, time: 5, duration: 0, value: 1.0)));
183+
Assert.That(releaseOnly, Started<PressInteraction>(releaseOnlyAction, gamepad.buttonSouth, time: 5, value: 1.0));
184+
Assert.That(pressAndRelease,
185+
Started<PressInteraction>(pressAndReleaseAction, gamepad.buttonSouth, time: 5, value: 1.0)
186+
.AndThen(Performed<PressInteraction>(pressAndReleaseAction, gamepad.buttonSouth, time: 5, duration: 0, value: 1.0)));
140187
}
141188
}
142189

@@ -513,15 +560,15 @@ public void Actions_ValueIsDefaultWhenActionIsCanceled()
513560
var gamepad = InputSystem.AddDevice<Gamepad>();
514561

515562
var action = new InputAction("test", binding: "<Gamepad>/leftTrigger", interactions: "CancelingTest");
516-
int performedCount = 0;
563+
var performedCount = 0;
517564
float performedValue = -1;
518565
action.performed += ctx =>
519566
{
520567
performedValue = ctx.ReadValue<float>();
521568
performedCount++;
522569
};
523570

524-
int canceledCount = 0;
571+
var canceledCount = 0;
525572
float canceledValue = -1;
526573
action.canceled += ctx =>
527574
{

Assets/Tests/InputSystem/CoreTests_Devices.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
using Vector2 = UnityEngine.Vector2;
2525
using Vector3 = UnityEngine.Vector3;
2626

27+
////TODO: set display name from product string only for layout builders (and give them control); for other layouts, let the layout dictate the display name
28+
2729
////TODO: test that device re-creation doesn't lose flags and such
2830

2931
partial class CoreTests

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ however, it has to be formatted properly to pass verification tests.
1111

1212
### Changed
1313

14+
- We've changed the rules that govern how action phases have to progress:
15+
* __This is a breaking change!__
16+
- The primary effect is additional callbacks getting triggered.
17+
* __Before__:
18+
- There were no enforced rules about how an action would go through `InputAction.started`, `InputAction.performed`, and `InputAction.canceled`. Which of the callbacks were triggered and in what order depended on a number of factors, the biggest influencer of which were the different interactions that could be applied to actions (like `Press` or `Hold`).
19+
- This made for unpredictable and frequently surprising results. In addition, it led to bugs where, for [example](https://issuetracker.unity3d.com/issues/input-system-ui-becomes-unresponsive-after-the-first-ui-button-press), adding a `Press` interaction to the `Click` action of `InputSystemUIInputModule` would cause the click state to get stuck because the click action would never cancel.
20+
* __Now__:
21+
- The system will now *always* trigger `InputAction.started` first. If this is not done explicitly, it happens implicitly.
22+
- Likewise, the system will now *always* trigger `InputAction.canceled` before going back to waiting state. Like with `InputAction.started`, if this isn't done explicitly, it will happen implicitly. This implies that `InputAction.canceled` no longer signifies an action getting aborted because it stopped after it started but before it performed. It now simply means "the action has ended" whether it actually got performed or not.
23+
- In-between `InputAction.started` and `InputAction.canceled`, `InputAction.performed` may be triggered arbitrary many times (including not at all).
24+
* While late in the cycle for 1.0, we've opted to make this change now in order to fix a range of bugs and problems we've observed that people encountered because of the previous behavior of the system.
25+
- Related to the change above, the behavior of `PressInteraction` has been tweaked and now is the following:
26+
* `Press Only`: Starts and immediately performs when pressed, then stays performed and cancels when button is released.
27+
* `Release Only`: Starts when button is pressed and then performs and immediately cancels when the button is released.
28+
* `Press And Release`: Starts and immediately performs when button is pressed, then stays performed and performs again and immediately cancels when button is released.
1429
- `Vector2Composite` now has a `mode` parameter which can be used to choose between `DigitalNormalized` (the default), `Digital` (same as `DigitalNormalized` but does not normalize the resulting vector), and `Analog` (uses float input values as is).
1530
* `Vector2Composite.normalize` has been deprecated. Note that it will not work together with `Analog`. The parameter will be removed in the future.
1631

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,21 @@ public static string GetDisplayName(string interaction)
314314
if (interactionType == null)
315315
return interaction;
316316

317+
return GetDisplayName(interactionType);
318+
}
319+
320+
public static string GetDisplayName(Type interactionType)
321+
{
322+
if (interactionType == null)
323+
throw new ArgumentNullException(nameof(interactionType));
324+
317325
var displayNameAttribute = interactionType.GetCustomAttribute<DisplayNameAttribute>();
318326
if (displayNameAttribute == null)
319-
return interaction;
327+
{
328+
if (interactionType.Name.EndsWith("Interaction"))
329+
return interactionType.Name.Substring(0, interactionType.Name.Length - "Interaction".Length);
330+
return interactionType.Name;
331+
}
320332

321333
return displayNameAttribute.DisplayName;
322334
}

0 commit comments

Comments
 (0)