Skip to content

Commit c52fff9

Browse files
FIX: PointerEventData.pointerId is the same when simultaneously releasing and then pressing with another finger (ISXB-845) (#2033)
* Check if pointer state touchID matches cached TouchControl touchID Covers the edge case of releasing one finger and pressing with another in the same frame. * Add unit test to avoid regression * Add pointerId to the asserts * Update CHANGELOG.md * Apply changes based on review * Get pointer state index based on pointerIds instead of touch controls cache The commit also: - Expands unit test case - Removes m_PointerTouchControls as they were not being used anymore * Fix compiling error * Fix again compile errors * Fix CHANGELOG.md * Removed wrong changelog entry
1 parent 0a39ee0 commit c52fff9

File tree

3 files changed

+203
-35
lines changed

3 files changed

+203
-35
lines changed

Assets/Tests/InputSystem/Plugins/UITests.cs

Lines changed: 195 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1693,6 +1693,201 @@ public IEnumerator UI_CanDriveUIFromMultipleTouches()
16931693
Assert.That(scene.leftChildReceiver.events, Is.Empty);
16941694
}
16951695

1696+
// https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-845
1697+
// This tests that we can release and press touches on the same frame with the expected events and touchIds.
1698+
[UnityTest]
1699+
[Category("UI")]
1700+
public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame()
1701+
{
1702+
var touchScreen = InputSystem.AddDevice<Touchscreen>();
1703+
1704+
// Prevent default selection of left object. This means that we will not have to contend with selections at all
1705+
// in this test as they are driven from UI objects and not by the input module itself.
1706+
var scene = CreateTestUI(noFirstSelected: true);
1707+
1708+
var asset = ScriptableObject.CreateInstance<InputActionAsset>();
1709+
var map = asset.AddActionMap("map");
1710+
var pointAction = map.AddAction("point", type: InputActionType.PassThrough, binding: "<Touchscreen>/touch*/position");
1711+
var leftClickAction = map.AddAction("leftClick", type: InputActionType.PassThrough, binding: "<Touchscreen>/touch*/press");
1712+
1713+
scene.uiModule.point = InputActionReference.Create(pointAction);
1714+
scene.uiModule.leftClick = InputActionReference.Create(leftClickAction);
1715+
1716+
map.Enable();
1717+
1718+
yield return null;
1719+
1720+
scene.leftChildReceiver.events.Clear();
1721+
1722+
Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.False);
1723+
Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.False);
1724+
1725+
// Touch left object.
1726+
var firstPosition = scene.From640x480ToScreen(100, 100);
1727+
BeginTouch(1, firstPosition);
1728+
yield return null;
1729+
1730+
var pointerIdTouch1 = ExtendedPointerEventData.MakePointerIdForTouch(touchScreen.deviceId, 1);
1731+
var pointerIdTouch2 = ExtendedPointerEventData.MakePointerIdForTouch(touchScreen.deviceId, 2);
1732+
var pointerIdTouch3 = ExtendedPointerEventData.MakePointerIdForTouch(touchScreen.deviceId, 3);
1733+
1734+
Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True);
1735+
Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.False);
1736+
1737+
Assert.That(scene.leftChildReceiver.events,
1738+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerEnter).And
1739+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1740+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 1).And
1741+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch1).And
1742+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1743+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition));
1744+
Assert.That(scene.leftChildReceiver.events,
1745+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerDown).And
1746+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1747+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 1).And
1748+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch1).And
1749+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1750+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition));
1751+
1752+
Assert.That(scene.rightChildReceiver.events, Is.Empty);
1753+
Assert.That(scene.uiModule.m_PointerStates.length, Is.EqualTo(1));
1754+
1755+
scene.leftChildReceiver.events.Clear();
1756+
scene.rightChildReceiver.events.Clear();
1757+
1758+
// Release left object and Touch right object on the same frame.
1759+
var secondPosition = scene.From640x480ToScreen(350, 200);
1760+
EndTouch(1, firstPosition);
1761+
BeginTouch(2, secondPosition);
1762+
BeginTouch(3, secondPosition);
1763+
MoveTouch(2, secondPosition);
1764+
yield return null;
1765+
1766+
Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True);
1767+
Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.True);
1768+
Assert.That(scene.eventSystem.IsPointerOverGameObject(3), Is.True);
1769+
1770+
// Pointer 1
1771+
Assert.That(scene.leftChildReceiver.events,
1772+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerUp).And
1773+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1774+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 1).And
1775+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch1).And
1776+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1777+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition));
1778+
1779+
// Pointer 2
1780+
Assert.That(scene.rightChildReceiver.events,
1781+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerEnter).And
1782+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1783+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And
1784+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch2).And
1785+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1786+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition));
1787+
1788+
Assert.That(scene.rightChildReceiver.events,
1789+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerDown).And
1790+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1791+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And
1792+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch2).And
1793+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1794+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition));
1795+
1796+
Assert.That(scene.rightChildReceiver.events,
1797+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerMove).And
1798+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1799+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And
1800+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch2).And
1801+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1802+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition));
1803+
1804+
// Pointer 3
1805+
Assert.That(scene.rightChildReceiver.events,
1806+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerEnter).And
1807+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1808+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 3).And
1809+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch3).And
1810+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1811+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition));
1812+
1813+
Assert.That(scene.rightChildReceiver.events,
1814+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerDown).And
1815+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1816+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 3).And
1817+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch3).And
1818+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1819+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition));
1820+
1821+
Assert.That(scene.uiModule.m_PointerStates.length, Is.EqualTo(3));
1822+
1823+
scene.leftChildReceiver.events.Clear();
1824+
scene.rightChildReceiver.events.Clear();
1825+
1826+
// End second touch.
1827+
EndTouch(2, secondPosition);
1828+
EndTouch(3, secondPosition);
1829+
yield return null;
1830+
1831+
Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True);
1832+
Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.True);
1833+
Assert.That(scene.eventSystem.IsPointerOverGameObject(3), Is.True);
1834+
1835+
Assert.That(scene.leftChildReceiver.events,
1836+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerExit).And
1837+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1838+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 1).And
1839+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch1).And
1840+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1841+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition));
1842+
1843+
Assert.That(scene.rightChildReceiver.events,
1844+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerUp).And
1845+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1846+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And
1847+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch2).And
1848+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1849+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition));
1850+
1851+
Assert.That(scene.rightChildReceiver.events,
1852+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerUp).And
1853+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1854+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 3).And
1855+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch3).And
1856+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1857+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition));
1858+
1859+
Assert.That(scene.uiModule.m_PointerStates.length, Is.EqualTo(2));
1860+
1861+
scene.leftChildReceiver.events.Clear();
1862+
scene.rightChildReceiver.events.Clear();
1863+
1864+
// Next frame
1865+
yield return null;
1866+
1867+
Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.False);
1868+
Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.False);
1869+
Assert.That(scene.eventSystem.IsPointerOverGameObject(3), Is.False);
1870+
1871+
Assert.That(scene.leftChildReceiver.events, Is.Empty);
1872+
Assert.That(scene.rightChildReceiver.events,
1873+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerExit).And
1874+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1875+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And
1876+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch2).And
1877+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1878+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition));
1879+
1880+
Assert.That(scene.rightChildReceiver.events,
1881+
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerExit).And
1882+
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And
1883+
.Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 3).And
1884+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch3).And
1885+
.Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And
1886+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition));
1887+
1888+
Assert.That(scene.uiModule.m_PointerStates.length, Is.Zero);
1889+
}
1890+
16961891
// https://fogbugz.unity3d.com/f/cases/1190150/
16971892
[UnityTest]
16981893
[Category("UI")]
@@ -1724,8 +1919,6 @@ public IEnumerator UI_CanUseTouchSimulationWithUI()
17241919

17251920
Assert.That(scene.uiModule.m_CurrentPointerType, Is.EqualTo(UIPointerType.Touch));
17261921
Assert.That(scene.uiModule.m_PointerIds.length, Is.EqualTo(1));
1727-
Assert.That(scene.uiModule.m_PointerTouchControls.length, Is.EqualTo(1));
1728-
Assert.That(scene.uiModule.m_PointerTouchControls[0], Is.SameAs(Touchscreen.current.touches[0]));
17291922
Assert.That(scene.leftChildReceiver.events,
17301923
EventSequence(
17311924
AllEvents("pointerType", UIPointerType.Touch),
@@ -1749,8 +1942,6 @@ public IEnumerator UI_CanUseTouchSimulationWithUI()
17491942

17501943
Assert.That(scene.uiModule.m_CurrentPointerType, Is.EqualTo(UIPointerType.Touch));
17511944
Assert.That(scene.uiModule.m_PointerIds.length, Is.EqualTo(1));
1752-
Assert.That(scene.uiModule.m_PointerTouchControls.length, Is.EqualTo(1));
1753-
Assert.That(scene.uiModule.m_PointerTouchControls[0], Is.SameAs(Touchscreen.current.touches[0]));
17541945
Assert.That(scene.leftChildReceiver.events,
17551946
EventSequence(
17561947
AllEvents("pointerType", UIPointerType.Touch),
@@ -1767,7 +1958,6 @@ public IEnumerator UI_CanUseTouchSimulationWithUI()
17671958

17681959
Assert.That(scene.uiModule.m_CurrentPointerType, Is.EqualTo(UIPointerType.None));
17691960
Assert.That(scene.uiModule.m_PointerIds.length, Is.Zero);
1770-
Assert.That(scene.uiModule.m_PointerTouchControls.length, Is.Zero);
17711961
Assert.That(scene.leftChildReceiver.events,
17721962
EventSequence(
17731963
AllEvents("pointerType", UIPointerType.Touch),

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ however, it has to be formatted properly to pass verification tests.
1313
### Fixed
1414
- Fixed an issue causing the Action context menu to not show on right click when right clicking an action in the Input Action Editor [ISXB-1134](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1134).
1515
- Fixed `ArgumentNullException: Value cannot be null.` during the migration of Project-wide Input Actions from `InputManager.asset` to `InputSystem_Actions.inputactions` asset which lead do the lost of the configuration [ISXB-1105](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1105)
16+
- Fixed pointerId staying the same when simultaneously releasing and then pressing in the same frame on mobile using touch. [ISXB-1006](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-845)
1617

1718
### Changed
1819
- Added back the InputManager to InputSystem project-wide asset migration code with performance improvement (ISX-2086)

Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1721,26 +1721,9 @@ private int GetPointerStateIndexFor(InputControl control, bool createIfNotExists
17211721
////REVIEW: Any way we can cut down on the hops all over memory that we're doing here?
17221722
var device = control.device;
17231723

1724-
////TODO: We're repeatedly inspecting the control setup here. Do this once and only redo it if the control setup changes.
1725-
1726-
////REVIEW: It seems wrong that we are picking up an input here that is *NOT* reflected in our actions. We just end
1727-
//// up reading a touchId control implicitly instead of allowing actions to deliver IDs to us. On the other hand,
1728-
//// making that setup explicit in actions may be quite awkward and not nearly as robust.
17291724
// Determine the pointer (and touch) ID. We default the pointer ID to the device
17301725
// ID of the InputDevice.
17311726
var controlParent = control.parent;
1732-
var touchControlIndex = m_PointerTouchControls.IndexOfReference(controlParent);
1733-
if (touchControlIndex != -1)
1734-
{
1735-
// For touches, we cache a reference to the control of a pointer so that we don't
1736-
// have to continuously do ReadValue() on the touch ID control.
1737-
m_CurrentPointerId = m_PointerIds[touchControlIndex];
1738-
m_CurrentPointerIndex = touchControlIndex;
1739-
m_CurrentPointerType = UIPointerType.Touch;
1740-
1741-
return touchControlIndex;
1742-
}
1743-
17441727
var pointerId = device.deviceId;
17451728
var touchId = 0;
17461729
var touchPosition = Vector2.zero;
@@ -1773,18 +1756,15 @@ private int GetPointerStateIndexFor(InputControl control, bool createIfNotExists
17731756
// NOTE: This is a linear search but m_PointerIds is only IDs and the number of concurrent pointers
17741757
// should be very low at any one point (in fact, we don't generally expect to have more than one
17751758
// which is why we are using InlinedArrays).
1776-
if (touchId == 0) // Not necessary for touches; see above.
1759+
for (var i = 0; i < m_PointerIds.length; i++)
17771760
{
1778-
for (var i = 0; i < m_PointerIds.length; i++)
1761+
if (m_PointerIds[i] == pointerId)
17791762
{
1780-
if (m_PointerIds[i] == pointerId)
1781-
{
1782-
// Existing entry found. Make it the current pointer.
1783-
m_CurrentPointerId = pointerId;
1784-
m_CurrentPointerIndex = i;
1785-
m_CurrentPointerType = m_PointerStates[i].pointerType;
1786-
return i;
1787-
}
1763+
// Existing entry found. Make it the current pointer.
1764+
m_CurrentPointerId = pointerId;
1765+
m_CurrentPointerIndex = i;
1766+
m_CurrentPointerType = m_PointerStates[i].pointerType;
1767+
return i;
17881768
}
17891769
}
17901770

@@ -1930,7 +1910,6 @@ private int AllocatePointer(int pointerId, int displayIndex, int touchId, UIPoin
19301910

19311911
// Allocate state.
19321912
m_PointerIds.AppendWithCapacity(pointerId);
1933-
m_PointerTouchControls.AppendWithCapacity(touchControl);
19341913
return m_PointerStates.AppendWithCapacity(new PointerModel(eventData));
19351914
}
19361915

@@ -1975,7 +1954,6 @@ private void RemovePointerAtIndex(int index)
19751954
// Remove. Note that we may change the order of pointers here. This can save us needless copying
19761955
// and m_CurrentPointerIndex should be the only index we get around for longer.
19771956
m_PointerIds.RemoveAtByMovingTailWithCapacity(index);
1978-
m_PointerTouchControls.RemoveAtByMovingTailWithCapacity(index);
19791957
m_PointerStates.RemoveAtByMovingTailWithCapacity(index);
19801958
Debug.Assert(m_PointerIds.length == m_PointerStates.length, "Pointer ID array should match state array in length");
19811959

@@ -2475,7 +2453,6 @@ private struct InputActionReferenceState
24752453
[NonSerialized] private int m_CurrentPointerIndex = -1;
24762454
[NonSerialized] internal UIPointerType m_CurrentPointerType = UIPointerType.None;
24772455
internal InlinedArray<int> m_PointerIds; // Index in this array maps to index in m_PointerStates. Separated out to make searching more efficient (we do a linear search).
2478-
internal InlinedArray<InputControl> m_PointerTouchControls;
24792456
internal InlinedArray<PointerModel> m_PointerStates;
24802457

24812458
// Navigation-type input.

0 commit comments

Comments
 (0)