Skip to content

Commit b58311b

Browse files
author
Rene Damm
authored
FIX: Device pairing ignoring controls that don't support magnitudes (#764, #781).
1 parent d36333f commit b58311b

File tree

10 files changed

+87
-20
lines changed

10 files changed

+87
-20
lines changed

Assets/Tests/InputSystem/CoreTests_Devices.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -921,20 +921,25 @@ public void Devices_ChangingStateOfDevice_TriggersNotification()
921921
var gamepad = InputSystem.AddDevice<Gamepad>();
922922

923923
var receivedCalls = 0;
924-
InputDevice receivedDevice = null;
924+
InputDevice receivedDevice = default;
925+
InputEventPtr receivedEventPtr = default;
925926

926927
InputState.onChange +=
927-
d =>
928+
(d, e) =>
928929
{
929930
++receivedCalls;
930931
receivedDevice = d;
932+
receivedEventPtr = e;
931933
};
932934

933935
InputSystem.QueueStateEvent(gamepad, new GamepadState { leftStick = new Vector2(0.5f, 0.5f) });
934936
InputSystem.Update();
935937

936938
Assert.That(receivedCalls, Is.EqualTo(1));
937939
Assert.That(receivedDevice, Is.SameAs(gamepad));
940+
Assert.That(receivedEventPtr.valid, Is.True);
941+
Assert.That(receivedEventPtr.deviceId, Is.EqualTo(gamepad.id));
942+
Assert.That(receivedEventPtr.IsA<StateEvent>(), Is.True);
938943
}
939944

940945
private class TestDeviceThatResetsStateInCallback : InputDevice, IInputStateCallbackReceiver
@@ -967,19 +972,22 @@ public void Devices_ChangingStateOfDevice_InStateCallback_TriggersNotification()
967972
var device = InputSystem.AddDevice<TestDeviceThatResetsStateInCallback>();
968973

969974
var receivedCalls = 0;
970-
InputDevice receivedDevice = null;
975+
InputDevice receivedDevice = default;
976+
InputEventPtr receivedEventPtr = default;
971977

972978
InputState.onChange +=
973-
d =>
979+
(d, e) =>
974980
{
975981
++receivedCalls;
976982
receivedDevice = d;
983+
receivedEventPtr = e;
977984
};
978985

979986
InputSystem.Update();
980987

981988
Assert.That(receivedCalls, Is.EqualTo(1));
982989
Assert.That(receivedDevice, Is.SameAs(device));
990+
Assert.That(receivedEventPtr.valid, Is.False);
983991
}
984992

985993
[Test]

Assets/Tests/InputSystem/Plugins/UserTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,23 @@ public void Users_CanFindUserPairedToSpecificDevice()
760760
Assert.That(InputUser.FindUserPairedToDevice(gamepad3), Is.Null);
761761
}
762762

763+
[Test]
764+
[Category("Users")]
765+
public void Users_CanDetectUseOfUnpairedDevice_FromControlThatDoesNotSupportMagnitude()
766+
{
767+
++InputUser.listenForUnpairedDeviceActivity;
768+
769+
var receivedControls = new List<InputControl>();
770+
InputUser.onUnpairedDeviceUsed +=
771+
control => { receivedControls.Add(control); };
772+
773+
var mouse = InputSystem.AddDevice<Mouse>();
774+
775+
Set(mouse.delta, new Vector2(0, 0.234f));
776+
777+
Assert.That(receivedControls, Is.EquivalentTo(new[] { mouse.delta.y }));
778+
}
779+
763780
[Test]
764781
[Category("Users")]
765782
public void Users_CanDetectUseOfUnpairedDevice()

Packages/com.unity.inputsystem/CHANGELOG.md

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

1212
### Fixed
1313

14+
- Fixed `InputUser.onUnpairedDeviceUser` ignoring input on controls that do not support `EvaluateMagnitude`.
15+
* This led to situations, for example, where `PlayerInput` would not initialize a control scheme switch from a `<Mouse>/delta` binding as the delta X and Y axes do not have min&max limits and thus return -1 from `EvaluateMagnitude`.
1416
- Fixed available processor list not updated right away when changing the action type in the Input Action editor window.
1517

1618
#### Actions
@@ -22,6 +24,7 @@ however, it has to be formatted properly to pass verification tests.
2224

2325
- Removed `timesliceEvents` setting - and made this tied to the update mode instead. We now always time slice when using fixed updates, and not when using dynamic updates.
2426
- When adding a composite, only ones compatible with the value type of the current action are shown. This will, for example, no longer display a `2D Vector` composite as an option on a floating-point button action.
27+
- The `InputState.onChange` callback now receives a second argument which is the event (if any) that triggered the state change on the device.
2528

2629
### Added
2730

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,9 @@ public static unsafe bool HasValueChangeInEvent(this InputControl control, Input
505505
// We need to unsubtract those offsets here.
506506
stateOffset += device.m_StateBlock.byteOffset;
507507

508-
if (control.m_StateBlock.byteOffset - stateOffset + control.m_StateBlock.alignedSizeInBytes > stateSizeInBytes)
508+
// Return null if state is out of range.
509+
var controlOffset = (int)control.m_StateBlock.byteOffset - stateOffset;
510+
if (controlOffset < 0 || controlOffset + control.m_StateBlock.alignedSizeInBytes > stateSizeInBytes)
509511
return null;
510512

511513
return (byte*)statePtr - (int)stateOffset;

Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ private void OnDeviceChange(InputDevice device, InputDeviceChange change)
305305
}
306306
}
307307

308-
private void OnDeviceStateChange(InputDevice device)
308+
private void OnDeviceStateChange(InputDevice device, InputEventPtr eventPtr)
309309
{
310310
////REVIEW: Ideally we would defer the refresh until we repaint. That way, we would not refresh on every single
311311
//// state change but rather only once for a repaint. However, for some reason, if we move the refresh

Packages/com.unity.inputsystem/InputSystem/Events/InputEventPtr.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ internal double internalTime
121121

122122
public InputEvent* data => m_EventPtr;
123123

124+
// The stateFormat, stateSizeInBytes, and stateOffset properties are very
125+
// useful for debugging.
126+
124127
internal FourCC stateFormat
125128
{
126129
get
@@ -145,6 +148,16 @@ internal uint stateSizeInBytes
145148
}
146149
}
147150

151+
internal uint stateOffset
152+
{
153+
get
154+
{
155+
if (IsA<DeltaStateEvent>())
156+
return DeltaStateEvent.From(this)->stateOffset;
157+
throw new InvalidOperationException("Event must be a DeltaStateEvent but is " + this);
158+
}
159+
}
160+
148161
public bool IsA<TOtherEvent>()
149162
where TOtherEvent : struct, IInputEventTypeInfo
150163
{

Packages/com.unity.inputsystem/InputSystem/InputManager.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
namespace UnityEngine.InputSystem
4040
{
4141
using DeviceChangeListener = Action<InputDevice, InputDeviceChange>;
42-
using DeviceStateChangeListener = Action<InputDevice>;
42+
using DeviceStateChangeListener = Action<InputDevice, InputEventPtr>;
4343
using LayoutChangeListener = Action<string, InputControlLayoutChange>;
4444
using EventListener = Action<InputEventPtr, InputDevice>;
4545
using UpdateListener = Action;
@@ -2844,7 +2844,7 @@ internal unsafe bool UpdateState(InputDevice device, InputUpdateType updateType,
28442844

28452845
// Notify listeners.
28462846
for (var i = 0; i < m_DeviceStateChangeListeners.length; ++i)
2847-
m_DeviceStateChangeListeners[i](device);
2847+
m_DeviceStateChangeListeners[i](device, eventPtr);
28482848

28492849
// Now that we've committed the new state to memory, if any of the change
28502850
// monitors fired, let the associated actions know.

Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInput.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1225,7 +1225,7 @@ private static void OnUnpairedDeviceUsed(InputControl control)
12251225

12261226
// Go through all control schemes and see if there is one usable with the device.
12271227
// If so, switch to it.
1228-
var controlScheme = InputControlScheme.FindControlSchemeForDevice<ReadOnlyArray<InputControlScheme>>(control.device, player.m_Actions.controlSchemes);
1228+
var controlScheme = InputControlScheme.FindControlSchemeForDevice(control.device, player.m_Actions.controlSchemes);
12291229
if (controlScheme != null)
12301230
{
12311231
// First remove the currently paired devices, then pair the device that was used,

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

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -338,14 +338,16 @@ public static event Action<InputUser, InputUserChange, InputDevice> onChange
338338
/// To enable detection of the use of unpaired devices, set <see cref="listenForUnpairedDeviceActivity"/> to true.
339339
/// It is disabled by default.
340340
///
341-
/// The control passed to the callback is the first control that activity was detected on. The device can be accessed
342-
/// through <see cref="InputControl.device"/>. If multiple controls on a device have been actuated, this will simply
343-
/// be the first control in <see cref="InputDevice.allControls"/> that is actuated. The system does not spend time
344-
/// trying to find the control that has been actuated the strongest. If desired, this can be done manually in the callback.
341+
/// The callback is invoked for each non-leaf, non-synthetic, non-noisy control that has been actuated on the device.
342+
/// It being restricted to non-leaf controls means that if, say, the stick on a gamepad is actuated in both X and Y
343+
/// direction, you will see two calls: one with stick/x and one with stick/y.
344+
///
345+
/// The reason that the callback is invoked for each individual control is that pairing often relies on checking
346+
/// for specific kinds of interactions. For example, a pairing callback may listen exclusively for button presses.
345347
///
346348
/// Note that whether the use of unpaired devices leads to them getting paired is under the control of the application.
347-
/// If the device should be paired, invoke <see cref="PerformPairingWithDevice"/> from the callback. This can also be used
348-
/// to further filter the activity, e.g. to only join users when pressing a button.
349+
/// If the device should be paired, invoke <see cref="PerformPairingWithDevice"/> from the callback. If you do so,
350+
/// no further callbacks will get triggered for other controls that may have been actuated in the same event.
349351
///
350352
/// <example>
351353
/// <code>
@@ -1613,8 +1615,13 @@ private static void OnDeviceChange(InputDevice device, InputDeviceChange change)
16131615
}
16141616
}
16151617

1616-
internal static void OnDeviceStateChange(InputDevice device)
1618+
private static unsafe void OnDeviceStateChange(InputDevice device, InputEventPtr eventPtr)
16171619
{
1620+
// Ignore any state change that was triggered internally. This immediately filters out
1621+
// things such as Pointers resetting deltas, for example.
1622+
if (!eventPtr.valid)
1623+
return;
1624+
16181625
Debug.Assert(s_ListenForUnpairedDeviceActivity != 0,
16191626
"This should only be called while listening for unpaired device activity");
16201627
if (s_ListenForUnpairedDeviceActivity == 0)
@@ -1629,6 +1636,10 @@ internal static void OnDeviceStateChange(InputDevice device)
16291636

16301637
Profiler.BeginSample("InputCheckForUnpairedDeviceActivity");
16311638

1639+
// Ignore any state change not triggered from a state event.
1640+
if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
1641+
return;
1642+
16321643
////TODO: allow filtering (e.g. by device requirements on user actions)
16331644

16341645
// Yes, it is so let's find out whether there was actual user activity
@@ -1647,20 +1658,28 @@ internal static void OnDeviceStateChange(InputDevice device)
16471658
if (control.noisy || control.synthetic)
16481659
continue;
16491660

1650-
////REVIEW: is this safe?
16511661
// Ignore non-leaf controls.
16521662
if (control.children.Count > 0)
16531663
continue;
16541664

1665+
// Ignore controls that aren't part of the event.
1666+
var statePtr = control.GetStatePtrFromStateEvent(eventPtr);
1667+
if (statePtr == null)
1668+
continue;
1669+
16551670
// Check for default state. Cheaper check than magnitude evaluation
16561671
// which may involve several virtual method calls.
16571672
if (control.CheckStateIsAtDefault())
16581673
continue;
16591674

16601675
// Ending up here is costly. We now do per-control work that may involve
16611676
// walking all over the place in the InputControl machinery.
1677+
//
1678+
// NOTE: We already know the control has moved away from its default state
1679+
// so in case it does not support magnitudes, we assume that the
1680+
// control has changed value, too.
16621681
var magnitude = control.EvaluateMagnitude();
1663-
if (magnitude > 0)
1682+
if (magnitude > 0 || magnitude == -1)
16641683
{
16651684
// Yes, something was actuated on the device.
16661685
var deviceHasBeenPaired = false;
@@ -1839,7 +1858,7 @@ private struct OngoingAccountSelection
18391858
private static InlinedArray<Action<InputUser, InputUserChange, InputDevice>> s_OnChange;
18401859
private static InlinedArray<Action<InputControl>> s_OnUnpairedDeviceUsed;
18411860
private static Action<InputDevice, InputDeviceChange> s_OnDeviceChangeDelegate;
1842-
private static Action<InputDevice> s_OnDeviceStateChangeDelegate;
1861+
private static Action<InputDevice, InputEventPtr> s_OnDeviceStateChangeDelegate;
18431862
private static bool s_OnDeviceChangeHooked;
18441863
private static bool s_OnDeviceStateChangeHooked;
18451864
private static int s_ListenForUnpairedDeviceActivity;

Packages/com.unity.inputsystem/InputSystem/State/InputState.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ public static class InputState
3535
/// <summary>
3636
/// Callback that is triggered when the state of an input device changes.
3737
/// </summary>
38-
public static event Action<InputDevice> onChange
38+
/// <remarks>
39+
/// The first parameter is the device whose state was changed the second parameter is the event
40+
/// that triggered the change in state. Note that the latter may be <c>null</c> in case the
41+
/// change was performed directly through <see cref="Change"/> rather than through an event.
42+
/// </remarks>
43+
public static event Action<InputDevice, InputEventPtr> onChange
3944
{
4045
add => InputSystem.s_Manager.onDeviceStateChange += value;
4146
remove => InputSystem.s_Manager.onDeviceStateChange -= value;

0 commit comments

Comments
 (0)