From 57b190ad0dd5636c206d89e2096f7def7d858915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freire?= Date: Mon, 21 Oct 2024 10:32:38 +0200 Subject: [PATCH 01/10] 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. --- .../Plugins/UI/InputSystemUIInputModule.cs | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs index c65c6a6700..5d7a46c5b1 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs @@ -1729,16 +1729,33 @@ private int GetPointerStateIndexFor(InputControl control, bool createIfNotExists // Determine the pointer (and touch) ID. We default the pointer ID to the device // ID of the InputDevice. var controlParent = control.parent; + // NOTE: m_PointerTouchControls stores references to TouchControl instances. e.g. this means that "touch0" + // in the same frame can have different touchId values. var touchControlIndex = m_PointerTouchControls.IndexOfReference(controlParent); if (touchControlIndex != -1) { - // For touches, we cache a reference to the control of a pointer so that we don't - // have to continuously do ReadValue() on the touch ID control. - m_CurrentPointerId = m_PointerIds[touchControlIndex]; - m_CurrentPointerIndex = touchControlIndex; - m_CurrentPointerType = UIPointerType.Touch; - - return touchControlIndex; + // Pointers are deallocated in the frame after their release (unpressed), which means the pointer state + // and the m_PointerTouchControls entry is only removed in the next frame + // (see UI_TouchPointersAreKeptForOneFrameAfterRelease). + // To accomodate for cases when touch is released and pressed in the same frame, the pointer + // state touchId needs to be checked against the cached touch control touchId (in m_PointerTouchControls) + // This is because the pointer state of the released touch (unpressed) still exists. + // If touchIDs are different, a new pointer should be allocated. Otherwise, a cached reference will + // be used. + var pointerTouchControl = m_PointerTouchControls[touchControlIndex]; + ref var pointerState = ref GetPointerStateForIndex(touchControlIndex); + + if (!(pointerTouchControl is TouchControl) || + (pointerTouchControl is TouchControl && + pointerState.eventData.touchId == ((TouchControl)pointerTouchControl).touchId.value)) + { + // For touches, we cache a reference to the control of a pointer so that we don't + // have to continuously do ReadValue() on the touch ID control. + m_CurrentPointerId = m_PointerIds[touchControlIndex]; + m_CurrentPointerIndex = touchControlIndex; + m_CurrentPointerType = UIPointerType.Touch; + return touchControlIndex; + } } var pointerId = device.deviceId; From 4715d3facf6d8be09ef6c671318c949cc99ae7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freire?= Date: Mon, 21 Oct 2024 12:06:01 +0200 Subject: [PATCH 02/10] Add unit test to avoid regression --- Assets/Tests/InputSystem/Plugins/UITests.cs | 136 ++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/Assets/Tests/InputSystem/Plugins/UITests.cs b/Assets/Tests/InputSystem/Plugins/UITests.cs index 6ca04144bd..1b6ca73bad 100644 --- a/Assets/Tests/InputSystem/Plugins/UITests.cs +++ b/Assets/Tests/InputSystem/Plugins/UITests.cs @@ -1693,6 +1693,142 @@ public IEnumerator UI_CanDriveUIFromMultipleTouches() Assert.That(scene.leftChildReceiver.events, Is.Empty); } + // https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-845 + // This tests that we can release and press touches on the same frame with the expected events and touchIds. + [UnityTest] + [Category("UI")] + public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() + { + var touchScreen = InputSystem.AddDevice(); + + // Prevent default selection of left object. This means that we will not have to contend with selections at all + // in this test as they are driven from UI objects and not by the input module itself. + var scene = CreateTestUI(noFirstSelected: true); + + var asset = ScriptableObject.CreateInstance(); + var map = asset.AddActionMap("map"); + var pointAction = map.AddAction("point", type: InputActionType.PassThrough, binding: "/touch*/position"); + var leftClickAction = map.AddAction("leftClick", type: InputActionType.PassThrough, binding: "/touch*/press"); + + scene.uiModule.point = InputActionReference.Create(pointAction); + scene.uiModule.leftClick = InputActionReference.Create(leftClickAction); + + map.Enable(); + + yield return null; + + scene.leftChildReceiver.events.Clear(); + + Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.False); + Assert.That(scene.eventSystem.IsPointerOverGameObject(touchScreen.deviceId), Is.False); + Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.False); + Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.False); + + // Touch left object. + var firstPosition = scene.From640x480ToScreen(100, 100); + BeginTouch(1, firstPosition); + yield return null; + + Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.True); + Assert.That(scene.eventSystem.IsPointerOverGameObject(touchScreen.deviceId), Is.True); + Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True); + Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.False); + + Assert.That(scene.leftChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerEnter).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 1).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition)); + Assert.That(scene.leftChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerDown).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 1).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition)); + Assert.That(scene.rightChildReceiver.events, Is.Empty); + + scene.leftChildReceiver.events.Clear(); + scene.rightChildReceiver.events.Clear(); + + // Release left object and Touch right object on the same frame. + var secondPosition = scene.From640x480ToScreen(350, 200); + EndTouch(1, firstPosition); + BeginTouch(2, secondPosition); + yield return null; + + Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.True); + Assert.That(scene.eventSystem.IsPointerOverGameObject(touchScreen.deviceId), Is.True); + Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True); + Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.True); + + Assert.That(scene.leftChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerUp).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 1).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition)); + + Assert.That(scene.rightChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerEnter).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); + + Assert.That(scene.rightChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerDown).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); + + scene.leftChildReceiver.events.Clear(); + scene.rightChildReceiver.events.Clear(); + + // End second touch. + EndTouch(2, secondPosition); + yield return null; + + Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.True); + Assert.That(scene.eventSystem.IsPointerOverGameObject(touchScreen.deviceId), Is.True); + Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True); + Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.True); + + Assert.That(scene.leftChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerExit).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 1).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition)); + + Assert.That(scene.rightChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerUp).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); + + scene.leftChildReceiver.events.Clear(); + scene.rightChildReceiver.events.Clear(); + + // Next frame + yield return null; + + Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.False); + Assert.That(scene.eventSystem.IsPointerOverGameObject(touchScreen.deviceId), Is.False); + Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.False); + Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.False); + + Assert.That(scene.leftChildReceiver.events, Is.Empty); + Assert.That(scene.rightChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerExit).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); + } + // https://fogbugz.unity3d.com/f/cases/1190150/ [UnityTest] [Category("UI")] From 82b1d4f7d1e3eb6c284dc0fa0c7b3c909d62fc42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freire?= Date: Mon, 21 Oct 2024 13:07:03 +0200 Subject: [PATCH 03/10] Add pointerId to the asserts --- Assets/Tests/InputSystem/Plugins/UITests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Assets/Tests/InputSystem/Plugins/UITests.cs b/Assets/Tests/InputSystem/Plugins/UITests.cs index 1b6ca73bad..6dc598efd5 100644 --- a/Assets/Tests/InputSystem/Plugins/UITests.cs +++ b/Assets/Tests/InputSystem/Plugins/UITests.cs @@ -1729,6 +1729,9 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() BeginTouch(1, firstPosition); yield return null; + var pointerIdTouch1 = ExtendedPointerEventData.MakePointerIdForTouch(touchScreen.deviceId, 1); + var pointerIdTouch2 = ExtendedPointerEventData.MakePointerIdForTouch(touchScreen.deviceId, 2); + Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(touchScreen.deviceId), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True); @@ -1738,12 +1741,14 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerEnter).And .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 1).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch1).And .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition)); Assert.That(scene.leftChildReceiver.events, Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerDown).And .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 1).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch1).And .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition)); Assert.That(scene.rightChildReceiver.events, Is.Empty); @@ -1766,6 +1771,7 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerUp).And .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 1).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch1).And .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition)); @@ -1773,6 +1779,7 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerEnter).And .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch2).And .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); @@ -1780,6 +1787,7 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerDown).And .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch2).And .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); @@ -1799,6 +1807,7 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerExit).And .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 1).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch1).And .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition)); @@ -1806,6 +1815,7 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerUp).And .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch2).And .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); @@ -1825,6 +1835,7 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerExit).And .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch2).And .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); } From 9e33ac849f8f4963df50fa3b498310dee676bd42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freire?= Date: Mon, 21 Oct 2024 13:51:45 +0200 Subject: [PATCH 04/10] Update CHANGELOG.md --- Packages/com.unity.inputsystem/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 6a9a909a41..8e93ce4d30 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -26,6 +26,7 @@ however, it has to be formatted properly to pass verification tests. - Fixed "AnalyticsResult" errors on consoles [ISXB-1107] - Fixed wrong `Display Index` value for touchscreen events.[ISXB-1101](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1101) - Fixed event handling when using Fixed Update processing where WasPressedThisFrame could appear to true for consecutive frames [ISXB-1006](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1006) +- 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) ### Added - Added the display of the device flag `CanRunInBackground` in device debug view. From 7d7ac39532cd6d57de5741a9c42455bf0322386d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freire?= Date: Tue, 22 Oct 2024 13:59:25 +0200 Subject: [PATCH 05/10] Apply changes based on review --- Assets/Tests/InputSystem/Plugins/UITests.cs | 5 ----- Packages/com.unity.inputsystem/CHANGELOG.md | 3 ++- .../InputSystem/Plugins/UI/InputSystemUIInputModule.cs | 3 +-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Assets/Tests/InputSystem/Plugins/UITests.cs b/Assets/Tests/InputSystem/Plugins/UITests.cs index 6dc598efd5..89a7c79fc0 100644 --- a/Assets/Tests/InputSystem/Plugins/UITests.cs +++ b/Assets/Tests/InputSystem/Plugins/UITests.cs @@ -1720,7 +1720,6 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() scene.leftChildReceiver.events.Clear(); Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.False); - Assert.That(scene.eventSystem.IsPointerOverGameObject(touchScreen.deviceId), Is.False); Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.False); Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.False); @@ -1733,7 +1732,6 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() var pointerIdTouch2 = ExtendedPointerEventData.MakePointerIdForTouch(touchScreen.deviceId, 2); Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.True); - Assert.That(scene.eventSystem.IsPointerOverGameObject(touchScreen.deviceId), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.False); @@ -1763,7 +1761,6 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() yield return null; Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.True); - Assert.That(scene.eventSystem.IsPointerOverGameObject(touchScreen.deviceId), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.True); @@ -1799,7 +1796,6 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() yield return null; Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.True); - Assert.That(scene.eventSystem.IsPointerOverGameObject(touchScreen.deviceId), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.True); @@ -1826,7 +1822,6 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() yield return null; Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.False); - Assert.That(scene.eventSystem.IsPointerOverGameObject(touchScreen.deviceId), Is.False); Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.False); Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.False); diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 8e93ce4d30..694a68f108 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -12,7 +12,8 @@ however, it has to be formatted properly to pass verification tests. ### Fixed - 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). -- 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) +- Fixed `ArgumentN- 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) +- 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) ### Changed - Added back the InputManager to InputSystem project-wide asset migration code with performance improvement (ISX-2086) diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs index 5d7a46c5b1..991a898a16 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs @@ -1746,8 +1746,7 @@ private int GetPointerStateIndexFor(InputControl control, bool createIfNotExists ref var pointerState = ref GetPointerStateForIndex(touchControlIndex); if (!(pointerTouchControl is TouchControl) || - (pointerTouchControl is TouchControl && - pointerState.eventData.touchId == ((TouchControl)pointerTouchControl).touchId.value)) + (pointerState.eventData.touchId == ((TouchControl)pointerTouchControl).touchId.value)) { // For touches, we cache a reference to the control of a pointer so that we don't // have to continuously do ReadValue() on the touch ID control. From ebbf612587cdcaf1c629dc9b5da3f0d629f09838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freire?= Date: Thu, 24 Oct 2024 15:22:09 +0200 Subject: [PATCH 06/10] 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 --- Assets/Tests/InputSystem/Plugins/UITests.cs | 68 ++++++++++++++++--- .../Plugins/UI/InputSystemUIInputModule.cs | 20 ++---- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/Assets/Tests/InputSystem/Plugins/UITests.cs b/Assets/Tests/InputSystem/Plugins/UITests.cs index 89a7c79fc0..856517326c 100644 --- a/Assets/Tests/InputSystem/Plugins/UITests.cs +++ b/Assets/Tests/InputSystem/Plugins/UITests.cs @@ -1719,7 +1719,6 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() scene.leftChildReceiver.events.Clear(); - Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.False); Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.False); Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.False); @@ -1730,8 +1729,8 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() var pointerIdTouch1 = ExtendedPointerEventData.MakePointerIdForTouch(touchScreen.deviceId, 1); var pointerIdTouch2 = ExtendedPointerEventData.MakePointerIdForTouch(touchScreen.deviceId, 2); + var pointerIdTouch3 = ExtendedPointerEventData.MakePointerIdForTouch(touchScreen.deviceId, 3); - Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.False); @@ -1749,7 +1748,9 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch1).And .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition)); + Assert.That(scene.rightChildReceiver.events, Is.Empty); + Assert.That(scene.uiModule.m_PointerStates.length, Is.EqualTo(1)); scene.leftChildReceiver.events.Clear(); scene.rightChildReceiver.events.Clear(); @@ -1758,12 +1759,15 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() var secondPosition = scene.From640x480ToScreen(350, 200); EndTouch(1, firstPosition); BeginTouch(2, secondPosition); + BeginTouch(3, secondPosition); + MoveTouch(2, secondPosition); yield return null; - Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.True); + Assert.That(scene.eventSystem.IsPointerOverGameObject(3), Is.True); + // Pointer 1 Assert.That(scene.leftChildReceiver.events, Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerUp).And .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And @@ -1772,6 +1776,7 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition)); + // Pointer 2 Assert.That(scene.rightChildReceiver.events, Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerEnter).And .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And @@ -1788,16 +1793,44 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); + Assert.That(scene.rightChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerMove).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 2).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch2).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); + + // Pointer 3 + Assert.That(scene.rightChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerEnter).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 3).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch3).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); + + Assert.That(scene.rightChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerDown).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 3).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch3).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); + + Assert.That(scene.uiModule.m_PointerStates.length, Is.EqualTo(3)); + scene.leftChildReceiver.events.Clear(); scene.rightChildReceiver.events.Clear(); // End second touch. EndTouch(2, secondPosition); + EndTouch(3, secondPosition); yield return null; - Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.True); Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.True); + Assert.That(scene.eventSystem.IsPointerOverGameObject(3), Is.True); Assert.That(scene.leftChildReceiver.events, Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerExit).And @@ -1815,15 +1848,25 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); + Assert.That(scene.rightChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerUp).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 3).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch3).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); + + Assert.That(scene.uiModule.m_PointerStates.length, Is.EqualTo(2)); + scene.leftChildReceiver.events.Clear(); scene.rightChildReceiver.events.Clear(); // Next frame yield return null; - Assert.That(scene.eventSystem.IsPointerOverGameObject(), Is.False); Assert.That(scene.eventSystem.IsPointerOverGameObject(1), Is.False); Assert.That(scene.eventSystem.IsPointerOverGameObject(2), Is.False); + Assert.That(scene.eventSystem.IsPointerOverGameObject(3), Is.False); Assert.That(scene.leftChildReceiver.events, Is.Empty); Assert.That(scene.rightChildReceiver.events, @@ -1833,6 +1876,16 @@ public IEnumerator UI_CanReleaseAndPressTouchesOnSameFrame() .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch2).And .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); + + Assert.That(scene.rightChildReceiver.events, + Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerExit).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.device == touchScreen).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.touchId == 3).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerId == pointerIdTouch3).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.pointerType == UIPointerType.Touch).And + .Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition)); + + Assert.That(scene.uiModule.m_PointerStates.length, Is.Zero); } // https://fogbugz.unity3d.com/f/cases/1190150/ @@ -1866,8 +1919,6 @@ public IEnumerator UI_CanUseTouchSimulationWithUI() Assert.That(scene.uiModule.m_CurrentPointerType, Is.EqualTo(UIPointerType.Touch)); Assert.That(scene.uiModule.m_PointerIds.length, Is.EqualTo(1)); - Assert.That(scene.uiModule.m_PointerTouchControls.length, Is.EqualTo(1)); - Assert.That(scene.uiModule.m_PointerTouchControls[0], Is.SameAs(Touchscreen.current.touches[0])); Assert.That(scene.leftChildReceiver.events, EventSequence( AllEvents("pointerType", UIPointerType.Touch), @@ -1891,8 +1942,6 @@ public IEnumerator UI_CanUseTouchSimulationWithUI() Assert.That(scene.uiModule.m_CurrentPointerType, Is.EqualTo(UIPointerType.Touch)); Assert.That(scene.uiModule.m_PointerIds.length, Is.EqualTo(1)); - Assert.That(scene.uiModule.m_PointerTouchControls.length, Is.EqualTo(1)); - Assert.That(scene.uiModule.m_PointerTouchControls[0], Is.SameAs(Touchscreen.current.touches[0])); Assert.That(scene.leftChildReceiver.events, EventSequence( AllEvents("pointerType", UIPointerType.Touch), @@ -1909,7 +1958,6 @@ public IEnumerator UI_CanUseTouchSimulationWithUI() Assert.That(scene.uiModule.m_CurrentPointerType, Is.EqualTo(UIPointerType.None)); Assert.That(scene.uiModule.m_PointerIds.length, Is.Zero); - Assert.That(scene.uiModule.m_PointerTouchControls.length, Is.Zero); Assert.That(scene.leftChildReceiver.events, EventSequence( AllEvents("pointerType", UIPointerType.Touch), diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs index 991a898a16..17b11e245e 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs @@ -1789,18 +1789,15 @@ private int GetPointerStateIndexFor(InputControl control, bool createIfNotExists // NOTE: This is a linear search but m_PointerIds is only IDs and the number of concurrent pointers // should be very low at any one point (in fact, we don't generally expect to have more than one // which is why we are using InlinedArrays). - if (touchId == 0) // Not necessary for touches; see above. + for (var i = 0; i < m_PointerIds.length; i++) { - for (var i = 0; i < m_PointerIds.length; i++) + if (m_PointerIds[i] == pointerId) { - if (m_PointerIds[i] == pointerId) - { - // Existing entry found. Make it the current pointer. - m_CurrentPointerId = pointerId; - m_CurrentPointerIndex = i; - m_CurrentPointerType = m_PointerStates[i].pointerType; - return i; - } + // Existing entry found. Make it the current pointer. + m_CurrentPointerId = pointerId; + m_CurrentPointerIndex = i; + m_CurrentPointerType = m_PointerStates[i].pointerType; + return i; } } @@ -1946,7 +1943,6 @@ private int AllocatePointer(int pointerId, int displayIndex, int touchId, UIPoin // Allocate state. m_PointerIds.AppendWithCapacity(pointerId); - m_PointerTouchControls.AppendWithCapacity(touchControl); return m_PointerStates.AppendWithCapacity(new PointerModel(eventData)); } @@ -1991,7 +1987,6 @@ private void RemovePointerAtIndex(int index) // Remove. Note that we may change the order of pointers here. This can save us needless copying // and m_CurrentPointerIndex should be the only index we get around for longer. m_PointerIds.RemoveAtByMovingTailWithCapacity(index); - m_PointerTouchControls.RemoveAtByMovingTailWithCapacity(index); m_PointerStates.RemoveAtByMovingTailWithCapacity(index); Debug.Assert(m_PointerIds.length == m_PointerStates.length, "Pointer ID array should match state array in length"); @@ -2491,7 +2486,6 @@ private struct InputActionReferenceState [NonSerialized] private int m_CurrentPointerIndex = -1; [NonSerialized] internal UIPointerType m_CurrentPointerType = UIPointerType.None; internal InlinedArray m_PointerIds; // Index in this array maps to index in m_PointerStates. Separated out to make searching more efficient (we do a linear search). - internal InlinedArray m_PointerTouchControls; internal InlinedArray m_PointerStates; // Navigation-type input. From f4c8e157e4711e1657581d6583cdb89319716005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freire?= Date: Thu, 24 Oct 2024 15:49:48 +0200 Subject: [PATCH 07/10] Fix compiling error --- .../Plugins/UI/InputSystemUIInputModule.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs index 17b11e245e..fbbbb3d6cc 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs @@ -1729,21 +1729,6 @@ private int GetPointerStateIndexFor(InputControl control, bool createIfNotExists // Determine the pointer (and touch) ID. We default the pointer ID to the device // ID of the InputDevice. var controlParent = control.parent; - // NOTE: m_PointerTouchControls stores references to TouchControl instances. e.g. this means that "touch0" - // in the same frame can have different touchId values. - var touchControlIndex = m_PointerTouchControls.IndexOfReference(controlParent); - if (touchControlIndex != -1) - { - // Pointers are deallocated in the frame after their release (unpressed), which means the pointer state - // and the m_PointerTouchControls entry is only removed in the next frame - // (see UI_TouchPointersAreKeptForOneFrameAfterRelease). - // To accomodate for cases when touch is released and pressed in the same frame, the pointer - // state touchId needs to be checked against the cached touch control touchId (in m_PointerTouchControls) - // This is because the pointer state of the released touch (unpressed) still exists. - // If touchIDs are different, a new pointer should be allocated. Otherwise, a cached reference will - // be used. - var pointerTouchControl = m_PointerTouchControls[touchControlIndex]; - ref var pointerState = ref GetPointerStateForIndex(touchControlIndex); if (!(pointerTouchControl is TouchControl) || (pointerState.eventData.touchId == ((TouchControl)pointerTouchControl).touchId.value)) From dd3127ae04292ff6bc3ec678bb5e408169de8449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freire?= Date: Thu, 24 Oct 2024 15:51:44 +0200 Subject: [PATCH 08/10] Fix again compile errors --- .../Plugins/UI/InputSystemUIInputModule.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs index fbbbb3d6cc..c72e043c71 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs @@ -1721,27 +1721,9 @@ private int GetPointerStateIndexFor(InputControl control, bool createIfNotExists ////REVIEW: Any way we can cut down on the hops all over memory that we're doing here? var device = control.device; - ////TODO: We're repeatedly inspecting the control setup here. Do this once and only redo it if the control setup changes. - - ////REVIEW: It seems wrong that we are picking up an input here that is *NOT* reflected in our actions. We just end - //// up reading a touchId control implicitly instead of allowing actions to deliver IDs to us. On the other hand, - //// making that setup explicit in actions may be quite awkward and not nearly as robust. // Determine the pointer (and touch) ID. We default the pointer ID to the device // ID of the InputDevice. var controlParent = control.parent; - - if (!(pointerTouchControl is TouchControl) || - (pointerState.eventData.touchId == ((TouchControl)pointerTouchControl).touchId.value)) - { - // For touches, we cache a reference to the control of a pointer so that we don't - // have to continuously do ReadValue() on the touch ID control. - m_CurrentPointerId = m_PointerIds[touchControlIndex]; - m_CurrentPointerIndex = touchControlIndex; - m_CurrentPointerType = UIPointerType.Touch; - return touchControlIndex; - } - } - var pointerId = device.deviceId; var touchId = 0; var touchPosition = Vector2.zero; From 1c47a88fafe8cf007f1129290b499e1b360a02bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freire?= Date: Thu, 24 Oct 2024 16:27:02 +0200 Subject: [PATCH 09/10] Fix CHANGELOG.md --- Packages/com.unity.inputsystem/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 694a68f108..15e2303c21 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -12,7 +12,7 @@ however, it has to be formatted properly to pass verification tests. ### Fixed - 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). -- Fixed `ArgumentN- 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) +- 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) - 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) ### Changed From 9d84e24b3711b0a4983bb4d50df92da23d369f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freire?= Date: Thu, 24 Oct 2024 16:27:46 +0200 Subject: [PATCH 10/10] Removed wrong changelog entry --- Packages/com.unity.inputsystem/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 15e2303c21..4c9fc9a71e 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -27,7 +27,6 @@ however, it has to be formatted properly to pass verification tests. - Fixed "AnalyticsResult" errors on consoles [ISXB-1107] - Fixed wrong `Display Index` value for touchscreen events.[ISXB-1101](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1101) - Fixed event handling when using Fixed Update processing where WasPressedThisFrame could appear to true for consecutive frames [ISXB-1006](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1006) -- 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) ### Added - Added the display of the device flag `CanRunInBackground` in device debug view.