Skip to content

Commit 4e2f7f6

Browse files
author
Rene Damm
authored
FIX: Actions not triggered correctly with multiple bindings to same control (case 1293808, #1256).
1 parent 65b5226 commit 4e2f7f6

File tree

10 files changed

+257
-31
lines changed

10 files changed

+257
-31
lines changed

Assets/Tests/InputSystem/CoreTests_Actions.cs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,86 @@ public void Actions_CanTargetSameControlWithMultipleActions()
165165
}
166166
}
167167

168+
// We may be looking at a situation of having more than one binding path on the same action
169+
// matches the same control in a system.
170+
// https://fogbugz.unity3d.com/f/cases/1293808/
168171
[Test]
169172
[Category("Actions")]
170-
[Ignore("TODO")]
171-
public void TODO_Actions_WhenSeveralBindingsResolveToSameControl_ThenWhatDoWeDoXXX()
173+
public void Actions_WhenSeveralBindingsResolveToSameControl_ControlIsAssociatedWithFirstActiveBinding()
172174
{
173-
Assert.Fail();
175+
var gamepad = InputSystem.AddDevice<Gamepad>();
176+
177+
var action1 = new InputAction();
178+
var action2 = new InputAction();
179+
180+
var actionMap = new InputActionMap();
181+
var action3 = actionMap.AddAction("action3");
182+
var action4 = actionMap.AddAction("action4");
183+
184+
action1.AddBinding("<Gamepad>/buttonSouth");
185+
action1.AddBinding("<Gamepad>/buttonSouth");
186+
187+
action2.AddBinding("<Gamepad>/buttonSouth");
188+
action2.AddBinding("<Gamepad>/button*");
189+
190+
action3.AddBinding("<Gamepad>/buttonNorth");
191+
action3.AddBinding("<Gamepad>/buttonSouth");
192+
action3.AddBinding("<Gamepad>/buttonSouth");
193+
action4.AddBinding("<Gamepad>/buttonSouth"); // Should not be removed; different action.
194+
195+
Assert.That(action1.controls, Is.EquivalentTo(new[] { gamepad.buttonSouth }));
196+
Assert.That(action2.controls, Has.Exactly(1).SameAs(gamepad.buttonSouth));
197+
Assert.That(action2.controls, Has.Count.EqualTo(4)); // North, south, east, west
198+
Assert.That(action3.controls, Is.EquivalentTo(new[] { gamepad.buttonNorth, gamepad.buttonSouth }));
199+
Assert.That(action4.controls, Is.EquivalentTo(new[] { gamepad.buttonSouth }));
200+
201+
Assert.That(action1.GetBindingIndexForControl(gamepad.buttonSouth), Is.EqualTo(0));
202+
Assert.That(action2.GetBindingIndexForControl(gamepad.buttonSouth), Is.EqualTo(0));
203+
Assert.That(action3.GetBindingIndexForControl(gamepad.buttonSouth), Is.EqualTo(1));
204+
Assert.That(action4.GetBindingIndexForControl(gamepad.buttonSouth), Is.EqualTo(0));
205+
206+
// Go through a bit of pressing and releasing to make sure that the action state
207+
// processing wasn't thrown off its track.
208+
209+
action1.Enable();
210+
action2.Enable();
211+
action3.Enable();
212+
action4.Enable();
213+
214+
Press(gamepad.buttonSouth);
215+
216+
Assert.That(action1.triggered, Is.True);
217+
Assert.That(action2.triggered, Is.True);
218+
Assert.That(action3.triggered, Is.True);
219+
Assert.That(action4.triggered, Is.True);
220+
221+
InputSystem.Update();
222+
223+
Assert.That(action1.triggered, Is.False);
224+
Assert.That(action2.triggered, Is.False);
225+
Assert.That(action3.triggered, Is.False);
226+
Assert.That(action4.triggered, Is.False);
227+
228+
Release(gamepad.buttonSouth);
229+
230+
Assert.That(action1.triggered, Is.False);
231+
Assert.That(action2.triggered, Is.False);
232+
Assert.That(action3.triggered, Is.False);
233+
Assert.That(action4.triggered, Is.False);
234+
235+
Press(gamepad.buttonSouth);
236+
237+
Assert.That(action1.triggered, Is.True);
238+
Assert.That(action2.triggered, Is.True);
239+
Assert.That(action3.triggered, Is.True);
240+
Assert.That(action4.triggered, Is.True);
241+
242+
InputSystem.Update();
243+
244+
Assert.That(action1.triggered, Is.False);
245+
Assert.That(action2.triggered, Is.False);
246+
Assert.That(action3.triggered, Is.False);
247+
Assert.That(action4.triggered, Is.False);
174248
}
175249

176250
[Test]

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ however, it has to be formatted properly to pass verification tests.
2727
- Fixed action with multiple bindings getting stuck in `Performed` state when two or more controls are pressed at the same time ([case 1295535](https://issuetracker.unity3d.com/issues/input-system-not-registering-multiple-inputs)).
2828
* Regression introduced in 1.1-preview.2.
2929

30+
#### Actions
31+
32+
- 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/)).
33+
* 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.
34+
```CSharp
35+
var action = new InputAction();
36+
action.AddBinding("<Gamepad>/buttonSouth");
37+
action.AddBinding("<Gamepad>/buttonSouth"); // Will be ignored.
38+
action.AddBinding("<Gamepad>/button*"); // Will only receive buttonWest, buttonEast, and buttonNorth.
39+
```
40+
* This also means that `InputAction.controls` will now only contain any control at most once.
41+
3042
### Added
3143

3244
- Added a new high-performance way to iterate over changed controls in an event.

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,23 @@ 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+
```
679+
663680
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.
664681

665682
#### Choosing which Devices to use

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* [Embedding Actions in MonoBehaviours](#embedding-actions-in-monobehaviours)
77
* [Loading Actions from JSON](#loading-actions-from-json)
88
* [Creating Actions in code](#creating-actions-in-code)
9+
* [Default Actions](#default-actions)
910
* [Using Actions](#using-actions)
1011
* [Responding to Actions](#responding-to-actions)
1112
* [Action callbacks](#action-callbacks)
@@ -183,6 +184,21 @@ asset.AddActionMap(gameplayMap);
183184
var lookAction = gameplayMap.AddAction("look", "<Gamepad>/leftStick");
184185
```
185186

187+
#### Default Actions
188+
189+
An [asset](./ActionAssets.md) called `DefaultInputActions.inputactions` containing a default setup of Actions comes with the Input System Package. You can reference this asset directly in your projects like any other Unity asset. However, the asset is also available in code form through the [`DefaultInputActions`](../api/UnityEngine.InputSystem.DefaultInputActions.html) class.
190+
191+
```CSharp
192+
void Start()
193+
{
194+
// Create an instance of the default actions.
195+
var actions = new DefaultInputActions();
196+
actions.Player.Look.performed += OnLook;
197+
actions.Player.Move.performed += OnMove;
198+
actions.Enable();
199+
}
200+
```
201+
186202
## Using Actions
187203

188204
For an Action to do something, you must first enable it. You can do this either by individually enabling Actions, or by enabling them in bulk through Action Maps. The second method is more efficient in all scenarios.

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,48 @@ public InputBinding? bindingMask
427427
/// will first resolve controls on the action (and for all actions in the map and/or
428428
/// the asset). See <a href="../manual/ActionBindings.html#binding-resolution">Binding Resolution</a>
429429
/// in the manual for details.
430+
///
431+
/// To map a control in this array to an index into <see cref="bindings"/>, use
432+
/// <see cref="InputActionRebindingExtensions.GetBindingIndexForControl"/>.
433+
///
434+
/// <example>
435+
/// <code>
436+
/// // Map control list to binding indices.
437+
/// var bindingIndices = myAction.controls.Select(c => myAction.GetBindingIndexForControl(c));
438+
/// </code>
439+
/// </example>
440+
///
441+
/// Note that this array will not contain the same control multiple times even if more than
442+
/// one binding on an action references the same control. Instead, the first binding on
443+
/// an action that resolves to a particular control will essentially "own" the control
444+
/// and subsequent bindings for the action will be blocked from resolving to the same control.
445+
///
446+
/// <example>
447+
/// <code>
448+
/// var action1 = new InputAction();
449+
/// action1.AddBinding("&lt;Gamepad&gt;/buttonSouth");
450+
/// action1.AddBinding("&lt;Gamepad&gt;/buttonSouth"); // This binding will be ignored.
451+
///
452+
/// // Contains only one instance of buttonSouth which is associated
453+
/// // with the first binding (at index #0).
454+
/// var action1Controls = action1.controls;
455+
///
456+
/// var action2 = new InputAction();
457+
/// action2.AddBinding("&lt;Gamepad&gt;/buttonSouth");
458+
/// // Add a binding that implicitly matches the first binding, too. When binding resolution
459+
/// // happens, this binding will only receive buttonNorth, buttonWest, and buttonEast, but not
460+
/// // buttonSouth as the first binding already received that control.
461+
/// action2.AddBinding("&lt;Gamepad&gt;/button*");
462+
///
463+
/// // Contains only all four face buttons (buttonSouth, buttonNorth, buttonEast, buttonWest)
464+
/// // but buttonSouth is associated with the first button and only buttonNorth, buttonEast,
465+
/// // and buttonWest are associated with the second binding.
466+
/// var action2Controls = action2.controls;
467+
/// </code>
468+
/// </example>
430469
/// </remarks>
470+
/// <seealso cref="InputActionRebindingExtensions.GetBindingIndexForControl"/>
471+
/// <seealso cref="bindings"/>
431472
public ReadOnlyArray<InputControl> controls
432473
{
433474
get

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,6 +1672,7 @@ public RebindingOperation WithControlsHavingToMatchPath(string path)
16721672
return this;
16731673
}
16741674

1675+
////REVIEW: This API has been confusing for users who usually will do something like WithControlsExcluding("Mouse"); find a more intuitive way to do this
16751676
/// <summary>
16761677
/// Prevent specific controls from being considered as candidate controls.
16771678
/// </summary>

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

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,74 @@ public unsafe void AddActionMap(InputActionMap map)
252252
!action.m_BindingMask.Value.Matches(ref unresolvedBinding,
253253
InputBinding.MatchOptions.EmptyGroupMatchesAny));
254254

255+
// If the binding isn't disabled, look up controls now. We do this first as we may still disable the
256+
// binding if it doesn't resolve to any controls or resolves only to controls already bound to by
257+
// other bindings.
258+
//
259+
// NOTE: We continuously add controls here to `resolvedControls`. Once we've completed our
260+
// pass over the bindings in the map, `resolvedControls` will have all the controls for
261+
// the current map.
262+
if (!bindingIsDisabled && !isComposite)
263+
{
264+
firstControlIndex = memory.controlCount + resolvedControls.Count;
265+
if (devicesForThisMap != null)
266+
{
267+
// Search in devices for only this map.
268+
var list = devicesForThisMap.Value;
269+
for (var i = 0; i < list.Count; ++i)
270+
{
271+
var device = list[i];
272+
if (!device.added)
273+
continue; // Skip devices that have been removed.
274+
numControls += InputControlPath.TryFindControls(device, path, 0, ref resolvedControls);
275+
}
276+
}
277+
else
278+
{
279+
// Search globally.
280+
numControls = InputSystem.FindControls(path, ref resolvedControls);
281+
}
282+
283+
// Check for controls that are already bound to the action through other
284+
// bindings. The first binding that grabs a specific control gets to "own" it.
285+
if (numControls > 0)
286+
{
287+
for (var i = 0; i < n; ++i)
288+
{
289+
ref var otherBindingState = ref bindingStatesPtr[bindingStartIndex + i];
290+
291+
// Skip if binding has no controls.
292+
if (otherBindingState.controlCount == 0)
293+
continue;
294+
295+
// Skip if binding isn't from same action.
296+
if (otherBindingState.actionIndex != actionStartIndex + actionIndexInMap)
297+
continue;
298+
299+
// Check for controls in the set that we just resolved that are also on the other
300+
// binding. Each such control we find, we kick out of the list.
301+
for (var k = 0; k < numControls; ++k)
302+
{
303+
var controlOnCurrentBinding = resolvedControls[firstControlIndex + k - memory.controlCount];
304+
var controlIndexOnOtherBinding = resolvedControls.IndexOf(controlOnCurrentBinding,
305+
otherBindingState.controlStartIndex - memory.controlCount, otherBindingState.controlCount);
306+
if (controlIndexOnOtherBinding != -1)
307+
{
308+
// Control is bound to a previous binding. Remove it from the current binding.
309+
resolvedControls.RemoveAt(firstControlIndex + k - memory.controlCount);
310+
--numControls;
311+
--k;
312+
}
313+
}
314+
}
315+
}
316+
317+
// Disable binding if it doesn't resolve to any controls.
318+
// NOTE: This also happens to bindings that got all their resolved controls removed because other bindings from the same
319+
// action already grabbed them.
320+
if (numControls == 0)
321+
bindingIsDisabled = true;
322+
}
255323

256324
// If the binding isn't disabled, resolve its controls, processors, and interactions.
257325
if (!bindingIsDisabled)
@@ -326,30 +394,6 @@ public unsafe void AddActionMap(InputActionMap map)
326394
currentCompositeAction = null;
327395
currentCompositeActionIndexInMap = InputActionState.kInvalidIndex;
328396
}
329-
330-
// Look up controls.
331-
//
332-
// NOTE: We continuously add controls here to `resolvedControls`. Once we've completed our
333-
// pass over the bindings in the map, `resolvedControls` will have all the controls for
334-
// the current map.
335-
firstControlIndex = memory.controlCount + resolvedControls.Count;
336-
if (devicesForThisMap != null)
337-
{
338-
// Search in devices for only this map.
339-
var list = devicesForThisMap.Value;
340-
for (var i = 0; i < list.Count; ++i)
341-
{
342-
var device = list[i];
343-
if (!device.added)
344-
continue; // Skip devices that have been removed.
345-
numControls += InputControlPath.TryFindControls(device, path, 0, ref resolvedControls);
346-
}
347-
}
348-
else
349-
{
350-
// Search globally.
351-
numControls = InputSystem.FindControls(path, ref resolvedControls);
352-
}
353397
}
354398
}
355399

Packages/com.unity.inputsystem/InputSystem/Controls/InputControlList.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,15 +348,29 @@ public void CopyTo(TControl[] array, int arrayIndex)
348348

349349
public int IndexOf(TControl item)
350350
{
351+
return IndexOf(item, 0);
352+
}
353+
354+
public int IndexOf(TControl item, int startIndex, int count = -1)
355+
{
356+
if (startIndex < 0)
357+
throw new ArgumentOutOfRangeException(nameof(startIndex), "startIndex cannot be negative");
358+
351359
if (m_Count == 0)
352360
return -1;
353361

362+
if (count < 0)
363+
count = Mathf.Max(m_Count - startIndex, 0);
364+
365+
if (startIndex + count > m_Count)
366+
throw new ArgumentOutOfRangeException(nameof(count));
367+
354368
var index = ToIndex(item);
355369
var indices = (ulong*)m_Indices.GetUnsafeReadOnlyPtr();
356370

357-
for (var i = 0; i < m_Count; ++i)
358-
if (indices[i] == index)
359-
return i;
371+
for (var i = 0; i < count; ++i)
372+
if (indices[startIndex + i] == index)
373+
return startIndex + i;
360374

361375
return -1;
362376
}
@@ -376,6 +390,11 @@ public bool Contains(TControl item)
376390
return IndexOf(item) != -1;
377391
}
378392

393+
public bool Contains(TControl item, int startIndex, int count = -1)
394+
{
395+
return IndexOf(item, startIndex, count) != -1;
396+
}
397+
379398
public void SwapElements(int index1, int index2)
380399
{
381400
if (index1 < 0 || index1 >= m_Count)

Packages/com.unity.inputsystem/InputSystem/Plugins/Users/InputUser.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using UnityEngine.InputSystem.Utilities;
66
using UnityEngine.Profiling;
77

8+
////REVIEW: remove users automatically when exiting play mode?
9+
810
////REVIEW: do we need to handle the case where devices are added to a user that are each associated with a different user account
911

1012
////REVIEW: how should we handle pairings of devices *not* called for by a control scheme? should that result in a failed match?

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ public PrimitiveValue ConvertTo(TypeCode type)
218218
case TypeCode.Int64: return ToInt64();
219219
case TypeCode.UInt16: return ToInt16();
220220
case TypeCode.UInt32: return ToInt32();
221-
case TypeCode.UInt64: return ToInt64();
221+
case TypeCode.UInt64: return ToUInt64();
222222
case TypeCode.Single: return ToSingle();
223223
case TypeCode.Double: return ToDouble();
224224
case TypeCode.Empty: return new PrimitiveValue();

0 commit comments

Comments
 (0)