Skip to content

Commit e109744

Browse files
jamesmcgillDmytro Ivanov
andauthored
FIX: Composite touchscreen controls not firing action after enabling (ISXB-98) (#1540)
* Investigation test * FIX: Composite touchscreen controls not firing action after enabling (ISXB-98) * Formatting changes * Review changes. Keep bit packing code together in same class * Review changes. Cleanup test comments * Update changelog * Review change: update doc regarding sorting behaviour * Review change: fix comment * Review change: fix comments and access modifier Co-authored-by: Dmytro Ivanov <dmytro@unity3d.com>
1 parent 457ccf4 commit e109744

5 files changed

Lines changed: 163 additions & 12 deletions

File tree

Assets/Tests/InputSystem/CoreTests_Actions.cs

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public void Actions_CanConsumeInput(bool legacyComposites)
5353

5454
// Enable some actions individually to make sure the code that deals
5555
// with re-resolution of already enabled bindings handles the enabling
56-
// of just individual actions out of the whole set correctlyuk.
56+
// of just individual actions out of the whole set correctly.
5757
action1.Enable();
5858
action2.Enable();
5959

@@ -2077,10 +2077,10 @@ public void Actions_CanCreateActionAssetWithMultipleActionMaps()
20772077
.AndThen(Performed(action4,
20782078
value: new StickDeadzoneProcessor().Process(new Vector2(0.123f, 0.234f)) * new Vector2(1, -1),
20792079
control: gamepad.leftStick, time: startTime + 0.234))
2080-
// map3/action5 should have been started.
2081-
.AndThen(Started<TapInteraction>(action5, value: 1f, control: gamepad.buttonSouth, time: startTime + 0.345))
20822080
// map2/action3 should have been started.
20832081
.AndThen(Started<TapInteraction>(action3, value: 1f, control: gamepad.buttonSouth, time: startTime + 0.345))
2082+
// map3/action5 should have been started.
2083+
.AndThen(Started<TapInteraction>(action5, value: 1f, control: gamepad.buttonSouth, time: startTime + 0.345))
20842084
// map3/action4 should have been performed as the stick has been moved
20852085
// beyond where it had already moved.
20862086
.AndThen(Performed(action4,
@@ -7463,7 +7463,10 @@ public void Actions_CanCreateAxisComposite()
74637463
InputSystem.QueueStateEvent(gamepad, new GamepadState {rightTrigger = 0.456f});
74647464
InputSystem.Update();
74657465

7466-
Assert.That(trace, Performed(action, control: gamepad.rightTrigger, value: 0.456f));
7466+
// Bit of an odd case. leftTrigger and rightTrigger have both changed state here so
7467+
// in a way, it's up to the system which one to pick. Might be useful if it was deliberately
7468+
// picking the control with the highest magnitude but not sure it's worth the effort.
7469+
Assert.That(trace, Performed(action, control: gamepad.leftTrigger, value: 0.456f));
74677470

74687471
trace.Clear();
74697472

@@ -8177,7 +8180,7 @@ public void Actions_CompositesReportControlThatTriggeredTheCompositeInCallback()
81778180
InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.A, Key.S));
81788181
InputSystem.Update();
81798182

8180-
Assert.That(performedControl, Is.EqualTo(keyboard.aKey));
8183+
Assert.That(performedControl, Is.EqualTo(keyboard.sKey));
81818184

81828185
LogAssert.NoUnexpectedReceived();
81838186
}
@@ -10045,6 +10048,116 @@ public void Actions_RebindingCandidatesShouldBeSorted_IfAddingNewCandidate()
1004510048
}
1004610049
}
1004710050

10051+
// Straight from demo project
10052+
public struct PointerInput
10053+
{
10054+
public bool Contact;
10055+
public int InputId;
10056+
public Vector2 Position;
10057+
public Vector2? Tilt;
10058+
public float? Pressure;
10059+
public Vector2? Radius;
10060+
public float? Twist;
10061+
}
10062+
10063+
public class PointerInputComposite : InputBindingComposite<PointerInput>
10064+
{
10065+
[InputControl(layout = "Button")]
10066+
public int contact;
10067+
10068+
[InputControl(layout = "Vector2")]
10069+
public int position;
10070+
10071+
[InputControl(layout = "Vector2")]
10072+
public int tilt;
10073+
10074+
[InputControl(layout = "Vector2")]
10075+
public int radius;
10076+
10077+
[InputControl(layout = "Axis")]
10078+
public int pressure;
10079+
10080+
[InputControl(layout = "Axis")]
10081+
public int twist;
10082+
10083+
[InputControl(layout = "Integer")]
10084+
public int inputId;
10085+
10086+
public override PointerInput ReadValue(ref InputBindingCompositeContext context)
10087+
{
10088+
var contact = context.ReadValueAsButton(this.contact);
10089+
var pointerId = context.ReadValue<int>(inputId);
10090+
var pressure = context.ReadValue<float>(this.pressure);
10091+
var radius = context.ReadValue<Vector2, Vector2MagnitudeComparer>(this.radius);
10092+
var tilt = context.ReadValue<Vector2, Vector2MagnitudeComparer>(this.tilt);
10093+
var position = context.ReadValue<Vector2, Vector2MagnitudeComparer>(this.position);
10094+
var twist = context.ReadValue<float>(this.twist);
10095+
10096+
return new PointerInput
10097+
{
10098+
Contact = contact,
10099+
InputId = pointerId,
10100+
Position = position,
10101+
Tilt = tilt != default ? tilt : (Vector2?)null,
10102+
Pressure = pressure > 0 ? pressure : (float?)null,
10103+
Radius = radius.sqrMagnitude > 0 ? radius : (Vector2?)null,
10104+
Twist = twist > 0 ? twist : (float?)null,
10105+
};
10106+
}
10107+
}
10108+
10109+
// https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-98
10110+
[Test]
10111+
[Category("Actions")]
10112+
[TestCase(true)]
10113+
[TestCase(false)]
10114+
public void Actions_WithMultipleCompositeBindings_WithoutEvaluateMagnitude_Works(bool prepopulateTouchesBeforeEnablingAction)
10115+
{
10116+
InputSystem.RegisterBindingComposite<PointerInputComposite>();
10117+
10118+
InputSystem.AddDevice<Touchscreen>();
10119+
10120+
var actionMap = new InputActionMap("test");
10121+
var action = actionMap.AddAction("point", InputActionType.Value);
10122+
for (var i = 0; i < 5; ++i)
10123+
action.AddCompositeBinding("PointerInput")
10124+
.With("contact", $"<Touchscreen>/touch{i}/press")
10125+
.With("position", $"<Touchscreen>/touch{i}/position")
10126+
.With("radius", $"<Touchscreen>/touch{i}/radius")
10127+
.With("pressure", $"<Touchscreen>/touch{i}/pressure")
10128+
.With("inputId", $"<Touchscreen>/touch{i}/touchId");
10129+
10130+
var values = new List<PointerInput>();
10131+
action.started += ctx => values.Add(ctx.ReadValue<PointerInput>());
10132+
action.performed += ctx => values.Add(ctx.ReadValue<PointerInput>());
10133+
action.canceled += ctx => values.Add(ctx.ReadValue<PointerInput>());
10134+
10135+
if (!prepopulateTouchesBeforeEnablingAction) // normally actions are enabled before any control actuations happen
10136+
actionMap.Enable();
10137+
10138+
// Start 5 touches, so we fill all slots [touch0, touch4] in Touchscreen with some valid touchId
10139+
for (var i = 0; i < 2; ++i)
10140+
BeginTouch(100 + i, new Vector2(100 * (i + 1), 100 * (i + 1)));
10141+
for (var i = 0; i < 2; ++i)
10142+
EndTouch(100 + i, new Vector2(100 * (i + 1), 100 * (i + 1)));
10143+
Assert.That(values.Count, Is.EqualTo(prepopulateTouchesBeforeEnablingAction ? 0 : 3));
10144+
values.Clear();
10145+
10146+
// Now when enabling actionMap ..
10147+
actionMap.Enable();
10148+
// On the following update we will trigger OnBeforeUpdate which will rise started/performed
10149+
// from InputActionState.OnBeforeInitialUpdate as controls are "actuated"
10150+
InputSystem.Update();
10151+
Assert.That(values.Count, Is.EqualTo(prepopulateTouchesBeforeEnablingAction ? 2 : 0)); // started+performed arrive from OnBeforeUpdate
10152+
values.Clear();
10153+
10154+
// Now subsequent touches should not be ignored
10155+
BeginTouch(200, new Vector2(1, 1));
10156+
Assert.That(values.Count, Is.EqualTo(1));
10157+
Assert.That(values[0].InputId, Is.EqualTo(200));
10158+
Assert.That(values[0].Position, Is.EqualTo(new Vector2(1, 1)));
10159+
}
10160+
1004810161
[Test]
1004910162
[Category("Actions")]
1005010163
[Ignore("TODO")]

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ however, it has to be formatted properly to pass verification tests.
1010

1111
## [Unreleased]
1212

13+
### Changed
14+
15+
### Fixed
16+
- Fixed composite touchscreen controls were not firing an action if screen was touched before enabling the action ([case ISXB-98](https://jira.unity3d.com/browse/ISXB-98)).
17+
18+
### Added
19+
1320
## [1.4.0] - 2022-04-10
1421

1522
### Changed

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

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,11 +1333,19 @@ void IInputStateChangeMonitor.NotifyTimerExpired(InputControl control, double ti
13331333
ProcessTimeout(time, mapIndex, controlIndex, bindingIndex, interactionIndex);
13341334
}
13351335

1336-
// We mangle the various indices we use into a single long for association with state change
1337-
// monitors. While we could look up map and binding indices from control indices, keeping
1338-
// all the information together avoids having to unnecessarily jump around in memory to grab
1339-
// the various pieces of data.
1340-
1336+
/// <summary>
1337+
/// Bit pack the mapIndex, controlIndex, bindingIndex and complexity components into a single long monitor index value.
1338+
/// </summary>
1339+
/// <param name="mapIndex">The mapIndex value to pack.</param>
1340+
/// <param name="controlIndex">The controlIndex value to pack.</param>
1341+
/// <param name="bindingIndex">The bindingIndex value to pack..</param>
1342+
/// <remarks>
1343+
/// We mangle the various indices we use into a single long for association with state change
1344+
/// monitors. While we could look up map and binding indices from control indices, keeping
1345+
/// all the information together avoids having to unnecessarily jump around in memory to grab
1346+
/// the various pieces of data.
1347+
/// The complexity component is implicitly derived and does not need to be passed as an argument.
1348+
/// </remarks>
13411349
private long ToCombinedMapAndControlAndBindingIndex(int mapIndex, int controlIndex, int bindingIndex)
13421350
{
13431351
// We have limits on the numbers of maps, controls, and bindings we allow in any single
@@ -1350,6 +1358,13 @@ private long ToCombinedMapAndControlAndBindingIndex(int mapIndex, int controlInd
13501358
return result;
13511359
}
13521360

1361+
/// <summary>
1362+
/// Extract the mapIndex, controlIndex and bindingIndex components from the provided bit packed argument (monitor index).
1363+
/// </summary>
1364+
/// <param name="mapControlAndBindingIndex">Represents a monitor index, which is a bit packed field containing multiple components.</param>
1365+
/// <param name="mapIndex">Will hold the extracted mapIndex value after the function completes.</param>
1366+
/// <param name="controlIndex">Will hold the extracted controlIndex value after the function completes.</param>
1367+
/// <param name="bindingIndex">Will hold the extracted bindingIndex value after the function completes.</param>
13531368
private void SplitUpMapAndControlAndBindingIndex(long mapControlAndBindingIndex, out int mapIndex,
13541369
out int controlIndex, out int bindingIndex)
13551370
{
@@ -1358,6 +1373,15 @@ private void SplitUpMapAndControlAndBindingIndex(long mapControlAndBindingIndex,
13581373
mapIndex = (int)((mapControlAndBindingIndex >> 40) & 0xff);
13591374
}
13601375

1376+
/// <summary>
1377+
/// Extract the 'complexity' component from the provided bit packed argument (monitor index).
1378+
/// </summary>
1379+
/// <param name="mapControlAndBindingIndex">Represents a monitor index, which is a bit packed field containing multiple components.</param>
1380+
internal static int GetComplexityFromMonitorIndex(long mapControlAndBindingIndex)
1381+
{
1382+
return (int)((mapControlAndBindingIndex >> 48) & 0xff);
1383+
}
1384+
13611385
/// <summary>
13621386
/// Process a state change that has happened in one of the controls attached
13631387
/// to this action map state.

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,15 @@ public void SortMonitorsByIndex()
261261
// Insertion sort.
262262
for (var i = 1; i < signalled.length; ++i)
263263
{
264-
for (var j = i; j > 0 && listeners[j - 1].monitorIndex < listeners[j].monitorIndex; --j)
264+
for (var j = i; j > 0; --j)
265265
{
266+
// Sort by complexities only to keep the sort stable
267+
// i.e. don't reverse the order of controls which have the same complexity
268+
var firstComplexity = InputActionState.GetComplexityFromMonitorIndex(listeners[j - 1].monitorIndex);
269+
var secondComplexity = InputActionState.GetComplexityFromMonitorIndex(listeners[j].monitorIndex);
270+
if (firstComplexity >= secondComplexity)
271+
break;
272+
266273
listeners.SwapElements(j, j - 1);
267274
memoryRegions.SwapElements(j, j - 1);
268275

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public static bool IsIntegerFormat(this FourCC format)
155155
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c> -or- <paramref name="monitor"/> is <c>null</c>.</exception>
156156
/// <exception cref="ArgumentException">The <see cref="InputDevice"/> of <paramref name="control"/> has not been <see cref="InputDevice.added"/>.</exception>
157157
/// <remarks>
158-
/// All monitors on an <see cref="InputDevice"/> are sorted by their <paramref name="monitorIndex"/> (in decreasing order) and invoked
158+
/// All monitors on an <see cref="InputDevice"/> are sorted by the complexity specified in their <paramref name="monitorIndex"/> (in decreasing order) and invoked
159159
/// in that order.
160160
///
161161
/// Every handler gets an opportunity to set <see cref="InputEventPtr.handled"/> to <c>true</c>. When doing so, all remaining pending monitors

0 commit comments

Comments
 (0)