Skip to content

Commit b8d5716

Browse files
author
Rene Damm
authored
FIX: Regression from suppressing same control bound repeatedly (#1394).
1 parent 86ff0df commit b8d5716

File tree

8 files changed

+106
-68
lines changed

8 files changed

+106
-68
lines changed

Assets/Tests/InputSystem/CoreTests_Actions.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public void Actions_CanTargetSameControlWithMultipleActions()
170170
// https://fogbugz.unity3d.com/f/cases/1293808/
171171
[Test]
172172
[Category("Actions")]
173-
public void Actions_WhenSeveralBindingsResolveToSameControl_ControlIsAssociatedWithFirstActiveBinding()
173+
public void Actions_WhenSeveralBindingsResolveToSameControl_SameControlFeedsIntoActionMultipleTimes_ButIsListedInControlsOnlyOnce()
174174
{
175175
var gamepad = InputSystem.AddDevice<Gamepad>();
176176

@@ -180,6 +180,8 @@ public void Actions_WhenSeveralBindingsResolveToSameControl_ControlIsAssociatedW
180180
var actionMap = new InputActionMap();
181181
var action3 = actionMap.AddAction("action3");
182182
var action4 = actionMap.AddAction("action4");
183+
var action5 = actionMap.AddAction("action5");
184+
var action6 = actionMap.AddAction("action6");
183185

184186
action1.AddBinding("<Gamepad>/buttonSouth");
185187
action1.AddBinding("<Gamepad>/buttonSouth");
@@ -192,16 +194,30 @@ public void Actions_WhenSeveralBindingsResolveToSameControl_ControlIsAssociatedW
192194
action3.AddBinding("<Gamepad>/buttonSouth");
193195
action4.AddBinding("<Gamepad>/buttonSouth"); // Should not be removed; different action.
194196

197+
action5.AddBinding("<Gamepad>/buttonSouth", interactions: "press(behavior=0)");
198+
action5.AddBinding("<Gamepad>/buttonSouth", interactions: "press(behavior=1)");
199+
action5.AddBinding("<Gamepad>/buttonSouth", processors: "invert");
200+
201+
action6.AddCompositeBinding("Dpad")
202+
.With("Left", "<Gamepad>/leftStick/y", processors: "clamp(min=0,max=1)")
203+
.With("Right", "<Gamepad>/leftStick/y", processors: "clamp(min=-1,max=0),invert");
204+
205+
var action6Performed = 0;
206+
action6.performed += ctx => action6Performed += ctx.performed ? 1 : 0;
207+
195208
Assert.That(action1.controls, Is.EquivalentTo(new[] { gamepad.buttonSouth }));
196209
Assert.That(action2.controls, Has.Exactly(1).SameAs(gamepad.buttonSouth));
197-
Assert.That(action2.controls, Has.Count.EqualTo(4)); // North, south, east, west
210+
Assert.That(action2.controls, Is.EquivalentTo(new[] { gamepad.buttonNorth, gamepad.buttonSouth, gamepad.buttonEast, gamepad.buttonWest }));
198211
Assert.That(action3.controls, Is.EquivalentTo(new[] { gamepad.buttonNorth, gamepad.buttonSouth }));
199212
Assert.That(action4.controls, Is.EquivalentTo(new[] { gamepad.buttonSouth }));
213+
Assert.That(action5.controls, Is.EquivalentTo(new[] { gamepad.buttonSouth }));
214+
Assert.That(action6.controls, Is.EquivalentTo(new[] { gamepad.leftStick.y })); // Only mentioned once.
200215

201216
Assert.That(action1.GetBindingIndexForControl(gamepad.buttonSouth), Is.EqualTo(0));
202217
Assert.That(action2.GetBindingIndexForControl(gamepad.buttonSouth), Is.EqualTo(0));
203218
Assert.That(action3.GetBindingIndexForControl(gamepad.buttonSouth), Is.EqualTo(1));
204219
Assert.That(action4.GetBindingIndexForControl(gamepad.buttonSouth), Is.EqualTo(0));
220+
Assert.That(action5.GetBindingIndexForControl(gamepad.buttonSouth), Is.EqualTo(0));
205221

206222
// Go through a bit of pressing and releasing to make sure that the action state
207223
// processing wasn't thrown off its track.
@@ -210,6 +226,8 @@ public void Actions_WhenSeveralBindingsResolveToSameControl_ControlIsAssociatedW
210226
action2.Enable();
211227
action3.Enable();
212228
action4.Enable();
229+
action5.Enable();
230+
action6.Enable();
213231

214232
Press(gamepad.buttonSouth);
215233

@@ -245,6 +263,11 @@ public void Actions_WhenSeveralBindingsResolveToSameControl_ControlIsAssociatedW
245263
Assert.That(action2.triggered, Is.False);
246264
Assert.That(action3.triggered, Is.False);
247265
Assert.That(action4.triggered, Is.False);
266+
267+
Set(gamepad.leftStick, new Vector2(0, -1));
268+
269+
Assert.That(action6Performed, Is.EqualTo(1));
270+
Assert.That(action6.ReadValue<Vector2>(), Is.EqualTo(new Vector2(1, 0)));
248271
}
249272

250273
[Test]

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
88
Due to package verification, the latest version below is the unpublished version and the date is meaningless.
99
however, it has to be formatted properly to pass verification tests.
1010

11+
## [Unreleased]
12+
13+
### Changed
14+
15+
- Modified the fix that landed in `1.1-preview.3` for [any given control being added to an action only once](#same_control_multiple_times_fix).
16+
* This caused a regression with some setups that, for example, bound the same control multiple times in a composite using processors to alter the value of the control.
17+
* Internally, a control is now again allowed to feed into the same action through more than one binding.
18+
* However, externally the control will be mentioned on the action's `InputAction.controls` list only once.
19+
1120
## [1.1.0-pre.6] - 2021-08-23
1221

1322
### Fixed
@@ -266,7 +275,7 @@ however, it has to be formatted properly to pass verification tests.
266275

267276
#### Actions
268277

269-
- Fixed actions not triggering correctly when multiple bindings on the same action were referencing the same control ([case 1293808](https://issuetracker.unity3d.com/product/unity/issues/guid/1293808/)).
278+
- <a name="same_control_multiple_times_fix"></a>Fixed actions not triggering correctly when multiple bindings on the same action were referencing the same control ([case 1293808](https://issuetracker.unity3d.com/product/unity/issues/guid/1293808/)).
270279
* Bindings will now "claim" controls during resolution. If several bindings __on the same action__ resolve to the same control, only the first such binding will successfully resolve to the control. Subsequent bindings will only resolve to controls not already referenced by other bindings on the action.
271280
```CSharp
272281
var action = new InputAction();

Packages/com.unity.inputsystem/Documentation~/ActionBindings.md

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -660,22 +660,7 @@ Note that a single [Binding path](Controls.md#control-paths) can match multiple
660660

661661
* A Binding path can also contain wildcards, such as `<Gamepad>/button*`. This matches any Control on any gamepad with a name starting with "button", which matches all the four action buttons on any connected gamepad. A different example: `*/{Submit}` matches any Control tagged with the "Submit" [usage](Controls.md#control-usages) on any Device.
662662

663-
If there are multiple Bindings on the same Action that all reference the same Control(s), only the first such Binding will successfully bind to the control.
664-
665-
```CSharp
666-
var action1 = new InputAction();
667-
668-
action1.AddBinding("<Gamepad>/buttonSouth");
669-
action1.AddBinding("<Gamepad>/buttonSouth"); // This binding will be ignored.
670-
671-
var action2 = new InputAction();
672-
673-
action2.AddBinding("<Gamepad>/buttonSouth");
674-
// Add a binding that implicitly matches the first binding, too. When binding resolution
675-
// happens, this binding will only receive buttonNorth, buttonWest, and buttonEast, but not
676-
// buttonSouth as the first binding already received that control.
677-
action2.AddBinding("<Gamepad>/button*");
678-
```
663+
If there are multiple Bindings on the same Action that all reference the same Control(s), the Control will effectively feed into the Action multiple times. This is to allow, for example, a single Control to produce different input on the same Action by virtue of being bound in a different fashion (composites, processors, interactions, etc). However, regardless of how many times a Control is bound on any given action, it will only be mentioned once in the Action's [array of `controls`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_controls).
679664

680665
To query the Controls that an Action resolves to, you can use [`InputAction.controls`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_controls). You can also run this query if the Action is disabled.
681666

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -441,9 +441,7 @@ public InputBinding? bindingMask
441441
/// </example>
442442
///
443443
/// Note that this array will not contain the same control multiple times even if more than
444-
/// one binding on an action references the same control. Instead, the first binding on
445-
/// an action that resolves to a particular control will essentially "own" the control
446-
/// and subsequent bindings for the action will be blocked from resolving to the same control.
444+
/// one binding on an action references the same control.
447445
///
448446
/// <example>
449447
/// <code>

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

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,25 @@ private unsafe void SetUpPerActionCachedBindingData()
836836
m_SingletonAction.m_BindingsCount = m_Bindings.Length;
837837
m_SingletonAction.m_ControlStartIndex = 0;
838838
m_SingletonAction.m_ControlCount = m_State?.totalControlCount ?? 0;
839+
840+
// Only complication, InputActionState allows a control to appear multiple times
841+
// on the same action and InputAction.controls[] doesn't.
842+
if (m_ControlsForEachAction.HaveDuplicateReferences(0, m_SingletonAction.m_ControlCount))
843+
{
844+
var numControls = 0;
845+
var controls = new InputControl[m_SingletonAction.m_ControlCount];
846+
for (var i = 0; i < m_SingletonAction.m_ControlCount; ++i)
847+
{
848+
if (!controls.ContainsReference(m_ControlsForEachAction[i]))
849+
{
850+
controls[numControls] = m_ControlsForEachAction[i];
851+
++numControls;
852+
}
853+
}
854+
855+
m_ControlsForEachAction = controls;
856+
m_SingletonAction.m_ControlCount = numControls;
857+
}
839858
}
840859
else
841860
{
@@ -943,17 +962,27 @@ private unsafe void SetUpPerActionCachedBindingData()
943962
// but do not really resolve to controls themselves).
944963
if (m_State != null && !m_Bindings[sourceBindingToCopy].isComposite)
945964
{
946-
var controlCountForBinding = m_State
947-
.bindingStates[mapIndices.bindingStartIndex + sourceBindingToCopy].controlCount;
965+
ref var bindingState = ref m_State.bindingStates[mapIndices.bindingStartIndex + sourceBindingToCopy];
966+
967+
var controlCountForBinding = bindingState.controlCount;
948968
if (controlCountForBinding > 0)
949969
{
950-
Array.Copy(m_State.controls,
951-
m_State.bindingStates[mapIndices.bindingStartIndex + sourceBindingToCopy]
952-
.controlStartIndex,
953-
m_ControlsForEachAction, currentControlIndex, controlCountForBinding);
954-
955-
currentControlIndex += controlCountForBinding;
956-
currentAction.m_ControlCount += controlCountForBinding;
970+
// Internally, we allow several bindings on a given action to resolve to the same control.
971+
// Externally, however, InputAction.controls[] is a set and thus should not contain duplicates.
972+
// So, instead of just doing a straight copy here, we copy controls one by one.
973+
974+
var controlStartIndexForBinding = bindingState.controlStartIndex;
975+
for (var n = 0; n < controlCountForBinding; ++n)
976+
{
977+
var control = m_State.controls[controlStartIndexForBinding + n];
978+
if (!m_ControlsForEachAction.ContainsReference(currentAction.m_ControlStartIndex,
979+
currentAction.m_ControlCount, control))
980+
{
981+
m_ControlsForEachAction[currentControlIndex] = control;
982+
++currentControlIndex;
983+
++currentAction.m_ControlCount;
984+
}
985+
}
957986
}
958987
}
959988

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,6 +1205,7 @@ private bool ShouldIgnoreControlStateChange(ref TriggerState trigger, int action
12051205
// anything.
12061206
if (actionState->controlIndex == kInvalidIndex)
12071207
{
1208+
actionState->magnitude = trigger.magnitude;
12081209
Profiler.EndSample();
12091210
return false;
12101211
}
@@ -1219,7 +1220,7 @@ private bool ShouldIgnoreControlStateChange(ref TriggerState trigger, int action
12191220
if (trigger.magnitude > actionState->magnitude)
12201221
{
12211222
// If this is not the control that is currently driving the action, we know
1222-
// there are multiple controls that are concurrently actuated on the control.
1223+
// there are multiple controls that are concurrently actuated on the action.
12231224
// Remember that so that when the controls are released again, we can more
12241225
// efficiently determine whether we need to take multiple bound controls into
12251226
// account or not.
@@ -1378,7 +1379,12 @@ private bool ShouldIgnoreControlStateChange(ref TriggerState trigger, int action
13781379
// it drive the action - this is like a direction change on the same control.
13791380
if (bindingStates[trigger.bindingIndex].isPartOfComposite && triggerControlIndex == actionStateControlIndex)
13801381
return false;
1381-
if (trigger.magnitude > 0 && triggerControlIndex != actionState->controlIndex)
1382+
// If we do have an actuation on a control that isn't currently driving the action, flag the action has
1383+
// having multiple concurrent inputs ATM.
1384+
// NOTE: We explicitly check for whether it is in fact not the same control even if the control indices are different.
1385+
// The reason is that we allow the same control, on the same action to be bound more than once on the same
1386+
// action.
1387+
if (trigger.magnitude > 0 && triggerControlIndex != actionState->controlIndex && controls[triggerControlIndex] != controls[actionState->controlIndex])
13821388
actionState->hasMultipleConcurrentActuations = true;
13831389
return true;
13841390
}

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

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -281,40 +281,6 @@ public unsafe void AddActionMap(InputActionMap map)
281281
numControls = InputSystem.FindControls(path, ref resolvedControls);
282282
}
283283

284-
// Check for controls that are already bound to the action through other
285-
// bindings. The first binding that grabs a specific control gets to "own" it.
286-
if (numControls > 0)
287-
{
288-
for (var i = 0; i < n; ++i)
289-
{
290-
ref var otherBindingState = ref bindingStatesPtr[bindingStartIndex + i];
291-
292-
// Skip if binding has no controls.
293-
if (otherBindingState.controlCount == 0)
294-
continue;
295-
296-
// Skip if binding isn't from same action.
297-
if (otherBindingState.actionIndex != actionStartIndex + actionIndexInMap)
298-
continue;
299-
300-
// Check for controls in the set that we just resolved that are also on the other
301-
// binding. Each such control we find, we kick out of the list.
302-
for (var k = 0; k < numControls; ++k)
303-
{
304-
var controlOnCurrentBinding = resolvedControls[firstControlIndex + k - memory.controlCount];
305-
var controlIndexOnOtherBinding = resolvedControls.IndexOf(controlOnCurrentBinding,
306-
otherBindingState.controlStartIndex - memory.controlCount, otherBindingState.controlCount);
307-
if (controlIndexOnOtherBinding != -1)
308-
{
309-
// Control is bound to a previous binding. Remove it from the current binding.
310-
resolvedControls.RemoveAt(firstControlIndex + k - memory.controlCount);
311-
--numControls;
312-
--k;
313-
}
314-
}
315-
}
316-
}
317-
318284
// Disable binding if it doesn't resolve to any controls.
319285
// NOTE: This also happens to bindings that got all their resolved controls removed because other bindings from the same
320286
// action already grabbed them.
@@ -408,7 +374,8 @@ public unsafe void AddActionMap(InputActionMap map)
408374
throw new InvalidOperationException(
409375
$"Binding '{unresolvedBinding}' that is part of composite '{composites[currentCompositeIndex]}' is missing a name");
410376

411-
// Give a part index for the
377+
// Assign an index to the current part of the composite which
378+
// can be used by the composite to read input from this part.
412379
partIndex = AssignCompositePartIndex(composites[currentCompositeIndex], unresolvedBinding.name,
413380
ref currentCompositePartCount);
414381

Packages/com.unity.inputsystem/InputSystem/Utilities/ArrayHelpers.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,27 @@ public static bool ContainsReference<TFirst, TSecond>(this TFirst[] array, int c
107107
return IndexOfReference(array, value, count) != -1;
108108
}
109109

110+
public static bool ContainsReference<TFirst, TSecond>(this TFirst[] array, int startIndex, int count, TSecond value)
111+
where TSecond : class
112+
where TFirst : TSecond
113+
{
114+
return IndexOfReference(array, value, startIndex, count) != -1;
115+
}
116+
117+
public static bool HaveDuplicateReferences<TFirst>(this TFirst[] first, int index, int count)
118+
{
119+
for (var i = 0; i < count; ++i)
120+
{
121+
var element = first[i];
122+
for (var n = i + 1; n < count - i; ++n)
123+
{
124+
if (ReferenceEquals(element, first[n]))
125+
return true;
126+
}
127+
}
128+
return false;
129+
}
130+
110131
public static bool HaveEqualElements<TValue>(TValue[] first, TValue[] second, int count = int.MaxValue)
111132
{
112133
if (first == null || second == null)

0 commit comments

Comments
 (0)