Skip to content

Commit 079f7a0

Browse files
andrew-ocekcoh
andauthored
Fix issues with OnScreenStick when used with PlayerInput and auto-switching of devices (#1586)
* Fix: Fix issues with OnScreenStick when used with PlayerInput and auto-switching of devices. * Fix: Fixed UI tests involving tracked devices so we don't use magic numbers when pointing at UI objects, which can break depending on the shape of the viewport (you can rotate too much or not enough. Now rotation is calculated to unambiguously point at the object you want to point at). * Fix: Excluded some UI tests that don't work on Android on the build farm. Co-authored-by: Håkan Sidenvall <[email protected]>
1 parent fbb9034 commit 079f7a0

File tree

10 files changed

+510
-26
lines changed

10 files changed

+510
-26
lines changed

Assets/Tests/InputSystem/Plugins/OnScreenTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using UnityEngine.InputSystem.LowLevel;
1010
using UnityEngine.InputSystem.OnScreen;
1111
using UnityEngine.InputSystem.UI;
12+
using UnityEngine.InputSystem.Users;
1213
using UnityEngine.TestTools;
1314
using UnityEngine.TestTools.Utils;
1415
using UnityEngine.UI;
@@ -264,6 +265,45 @@ public IEnumerator Devices_CanHaveOnScreenJoystickControls()
264265
Assert.That(Gamepad.all[0].buttonSouth.isPressed, Is.False);
265266
}
266267

268+
[UnityTest]
269+
[Category("Devices")]
270+
public IEnumerator Devices_OnScreenStickDoesNotReceivePointerUpEventsInIsolatedMode()
271+
{
272+
InputSystem.AddDevice<Touchscreen>();
273+
274+
var uiTestScene = new UITestScene(this);
275+
var image = uiTestScene.AddImage();
276+
277+
var stick = image.gameObject.AddComponent<OnScreenStick>();
278+
stick.transform.SetParent(uiTestScene.canvas.transform);
279+
stick.controlPath = "<Gamepad>/leftStick";
280+
stick.useIsolatedInputActions = true;
281+
282+
var stickOriginPosition = ((RectTransform)stick.transform).anchoredPosition;
283+
284+
285+
// PlayerInput listens for unpaired device activity and then switches to that device which has the effect
286+
// of re-resolving bindings, which causes any active actions to cancel. This code replicates that.
287+
InputUser.listenForUnpairedDeviceActivity++;
288+
InputUser.PerformPairingWithDevice(InputSystem.GetDevice<Touchscreen>());
289+
InputUser.onUnpairedDeviceUsed += (control, eventPtr) =>
290+
{
291+
uiTestScene.uiInputModule.actionsAsset.actionMaps[0].LazyResolveBindings(true);
292+
};
293+
294+
yield return uiTestScene.PressAndDrag(image, new Vector2(50, 50));
295+
296+
// The OnScreenStick when being driven from the UI (non-isolated mode) queues the events into the next
297+
// frame, because the UI events are processed after the input system update has completed (as opposed to running
298+
// inside input action callbacks. When events are queued from there, they are processed in the same frame), so
299+
// we need an extra frame here to flush those events.
300+
yield return null;
301+
302+
// The effect on the stick of cancelling the pointer action is that it jumps back to the center position,
303+
// so assert that it hasn't done that
304+
Assert.That(stick.gameObject.GetComponent<RectTransform>().anchoredPosition, Is.Not.EqualTo(stickOriginPosition));
305+
}
306+
267307
// https://fogbugz.unity3d.com/f/cases/1305016/
268308
[Test]
269309
[Category("Devices")]
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using System.Collections;
2+
using UnityEngine;
3+
using UnityEngine.EventSystems;
4+
using UnityEngine.InputSystem.UI;
5+
using UnityEngine.UI;
6+
using static GameObjectBuilder;
7+
8+
/// <summary>
9+
/// Set up a default UI test scene containing:
10+
/// - a main camera
11+
/// - a canvas in ScreenSpaceOverlay mode
12+
/// - an event system using the InputSystemUIInputModule input module
13+
/// </summary>
14+
internal class UITestScene
15+
{
16+
private readonly CoreTestsFixture m_Fixture;
17+
18+
public UITestScene(CoreTestsFixture fixture)
19+
{
20+
m_Fixture = fixture;
21+
MakeGameObject<Camera>("MainCamera");
22+
MakeGameObject<TestEventSystem, InputSystemUIInputModule>("EventSystem");
23+
MakeGameObject<GraphicRaycaster>("Canvas", g => g.gameObject.GetComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay);
24+
25+
((TestEventSystem)eventSystem).OnApplicationFocus(true);
26+
}
27+
28+
public Camera camera => GameObject.Find("MainCamera").GetComponent<Camera>();
29+
public EventSystem eventSystem => GameObject.Find("EventSystem").GetComponent<EventSystem>();
30+
public Canvas canvas => GameObject.Find("Canvas").GetComponent<Canvas>();
31+
public InputSystemUIInputModule uiInputModule => eventSystem.gameObject.GetComponent<InputSystemUIInputModule>();
32+
33+
public RectTransform AddImage(string name = "")
34+
{
35+
var rectTransform = new GameObject(name, typeof(Image)).GetComponent<RectTransform>();
36+
rectTransform.SetParent(canvas.transform);
37+
38+
// center the image in the canvas
39+
rectTransform.anchoredPosition = Vector2.zero;
40+
return rectTransform;
41+
}
42+
43+
public IEnumerator PressAndDrag(RectTransform uiObject, Vector2 distance)
44+
{
45+
m_Fixture.BeginTouch(1, uiObject.position);
46+
yield return null;
47+
((TestEventSystem)eventSystem).Update();
48+
49+
m_Fixture.MoveTouch(1, (Vector2)uiObject.position + distance);
50+
((TestEventSystem)eventSystem).Update();
51+
}
52+
53+
private class TestEventSystem : EventSystem
54+
{
55+
public new void OnApplicationFocus(bool hasFocus)
56+
{
57+
base.OnApplicationFocus(hasFocus);
58+
}
59+
60+
public new void Update()
61+
{
62+
base.Update();
63+
}
64+
}
65+
}

Assets/Tests/InputSystem/Plugins/UITestScene.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/Tests/InputSystem/Plugins/UITests.cs

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,37 @@ public void ClearEvents()
8080
leftChildReceiver.events.Clear();
8181
rightChildReceiver.events.Clear();
8282
}
83+
84+
/// <summary>
85+
/// Calculate a quaternion that will rotate a vector looking straight down the z axis around the y axis
86+
/// to point at the specified UI object.
87+
/// </summary>
88+
/// <param name="from"></param>
89+
/// <param name="uiObject"></param>
90+
/// <param name="worldSpaceUIObjectOffset"></param>
91+
/// <returns></returns>
92+
public Quaternion GetLookAtQuaternion(Vector3 from, GameObject uiObject, Vector3 worldSpaceUIObjectOffset = default)
93+
{
94+
var position = uiObject.GetComponent<RectTransform>().position + worldSpaceUIObjectOffset;
95+
var angle = Mathf.Asin(position.x / (from - position).magnitude);
96+
return Quaternion.Euler(0, angle * Mathf.Rad2Deg, 0);
97+
}
98+
99+
/// <summary>
100+
/// Calculate a quaternion that will rotate a vector pointing at UI object 'from' around the y axis to point
101+
/// at object 'to'.
102+
/// </summary>
103+
/// <param name="position"></param>
104+
/// <param name="from"></param>
105+
/// <param name="to"></param>
106+
/// <returns></returns>
107+
public Quaternion RotateFromTo(Vector3 position, GameObject from, GameObject to)
108+
{
109+
var fromPosition = from.GetComponent<RectTransform>().position;
110+
var toPosition = to.GetComponent<RectTransform>().position;
111+
var angle = Mathf.Asin((fromPosition - toPosition).magnitude / (position - fromPosition).magnitude);
112+
return Quaternion.Euler(0, angle * Mathf.Rad2Deg, 0);
113+
}
83114
}
84115

85116
[SetUp]
@@ -361,7 +392,7 @@ public IEnumerator UI_CanDriveUIFromPointer(string deviceLayout, UIPointerType p
361392
}
362393
else if (isTracked)
363394
{
364-
trackedOrientation = Quaternion.Euler(0, -35, 0);
395+
trackedOrientation = scene.GetLookAtQuaternion(trackedPosition, scene.leftGameObject);
365396
Set(device, "deviceRotation", trackedOrientation, queueEventOnly: true);
366397
}
367398
else
@@ -674,7 +705,7 @@ public IEnumerator UI_CanDriveUIFromPointer(string deviceLayout, UIPointerType p
674705
}
675706
else if (isTracked)
676707
{
677-
trackedOrientation = Quaternion.Euler(0, -30, 0);
708+
trackedOrientation = scene.GetLookAtQuaternion(trackedPosition, scene.leftGameObject, new Vector3(5, 0, 0));
678709
Set(device, "deviceRotation", trackedOrientation, queueEventOnly: true);
679710
}
680711
else
@@ -778,7 +809,7 @@ public IEnumerator UI_CanDriveUIFromPointer(string deviceLayout, UIPointerType p
778809
}
779810
else if (isTracked)
780811
{
781-
trackedOrientation = Quaternion.Euler(0, 30, 0);
812+
trackedOrientation = scene.GetLookAtQuaternion(trackedPosition, scene.rightGameObject);
782813
Set(device, "deviceRotation", trackedOrientation, queueEventOnly: true);
783814
}
784815
else
@@ -1249,12 +1280,16 @@ public IEnumerator UI_CanDriveUIFromMultiplePointers(UIPointerBehavior pointerBe
12491280
scene.leftChildReceiver.events.Clear();
12501281
scene.rightChildReceiver.events.Clear();
12511282

1283+
// the tracked device tests below don't seem to work on Android when run on the build farm. The pointer
1284+
// positions fail the IsWithinRect checks. They work fine locally. Possibly something to do with tracked
1285+
// devices on Shield? The build farm seems to always run them on Shield devices.
1286+
#if !UNITY_ANDROID
12521287
// Put tracked device #1 over left object and tracked device #2 over right object.
12531288
// Need two updates as otherwise we'd end up with just another pointer of the right object
12541289
// which would not result in an event.
1255-
Set(trackedDevice1, "deviceRotation", Quaternion.Euler(0, -30, 0));
1290+
Set(trackedDevice1, "deviceRotation", scene.GetLookAtQuaternion(Vector3.zero, scene.leftGameObject));
12561291
yield return null;
1257-
Set(trackedDevice2, "deviceRotation", Quaternion.Euler(0, 30, 0));
1292+
Set(trackedDevice2, "deviceRotation", scene.GetLookAtQuaternion(Vector3.zero, scene.rightGameObject));
12581293
yield return null;
12591294

12601295
var leftPosition = scene.From640x480ToScreen(80, 240);
@@ -1313,6 +1348,7 @@ public IEnumerator UI_CanDriveUIFromMultiplePointers(UIPointerBehavior pointerBe
13131348

13141349
scene.leftChildReceiver.events.Clear();
13151350
scene.rightChildReceiver.events.Clear();
1351+
#endif
13161352

13171353
// Touch right object on first touchscreen and left object on second touchscreen.
13181354
BeginTouch(1, secondPosition, screen: touch1);
@@ -1727,15 +1763,15 @@ public IEnumerator UI_CanDriveUIFromMultipleTrackedDevices()
17271763
scene.rightChildReceiver.events.Clear();
17281764

17291765
// Point first device at left child.
1730-
Set(trackedDevice1.deviceRotation, Quaternion.Euler(0, -30, 0));
1766+
Set(trackedDevice1.deviceRotation, scene.GetLookAtQuaternion(Vector3.zero, scene.leftGameObject));
17311767
yield return null;
17321768

17331769
Assert.That(scene.leftChildReceiver.events,
17341770
EventSequence(
17351771
AllEvents("pointerType", UIPointerType.Tracked),
17361772
AllEvents("pointerId", trackedDevice1.deviceId),
17371773
AllEvents("device", trackedDevice1),
1738-
AllEvents("trackedDeviceOrientation", Quaternion.Euler(0, -30, 0)),
1774+
AllEvents("trackedDeviceOrientation", scene.GetLookAtQuaternion(Vector3.zero, scene.leftGameObject)),
17391775
OneEvent("type", EventType.PointerEnter)
17401776
#if UNITY_2021_2_OR_NEWER
17411777
, OneEvent("type", EventType.PointerMove)
@@ -1747,15 +1783,15 @@ public IEnumerator UI_CanDriveUIFromMultipleTrackedDevices()
17471783
scene.leftChildReceiver.events.Clear();
17481784

17491785
// Point second device at left child.
1750-
Set(trackedDevice2.deviceRotation, Quaternion.Euler(0, -31, 0));
1786+
Set(trackedDevice2.deviceRotation, scene.GetLookAtQuaternion(Vector3.zero, scene.leftGameObject, Vector3.left));
17511787
yield return null;
17521788

17531789
Assert.That(scene.leftChildReceiver.events,
17541790
EventSequence(
17551791
AllEvents("pointerType", UIPointerType.Tracked),
17561792
AllEvents("pointerId", trackedDevice2.deviceId),
17571793
AllEvents("device", trackedDevice2),
1758-
AllEvents("trackedDeviceOrientation", Quaternion.Euler(0, -31, 0)),
1794+
AllEvents("trackedDeviceOrientation", scene.GetLookAtQuaternion(Vector3.zero, scene.leftGameObject, Vector3.left)),
17591795
OneEvent("type", EventType.PointerEnter)
17601796
#if UNITY_2021_2_OR_NEWER
17611797
, OneEvent("type", EventType.PointerMove)
@@ -1776,7 +1812,7 @@ public IEnumerator UI_CanDriveUIFromMultipleTrackedDevices()
17761812
AllEvents("pointerType", UIPointerType.Tracked),
17771813
AllEvents("pointerId", trackedDevice1.deviceId),
17781814
AllEvents("device", trackedDevice1),
1779-
AllEvents("trackedDeviceOrientation", Quaternion.Euler(0, -30, 0)),
1815+
AllEvents("trackedDeviceOrientation", scene.GetLookAtQuaternion(Vector3.zero, scene.leftGameObject)),
17801816
OneEvent("type", EventType.PointerDown),
17811817
OneEvent("type", EventType.InitializePotentialDrag),
17821818
OneEvent("type", EventType.PointerUp),
@@ -1796,7 +1832,7 @@ public IEnumerator UI_CanDriveUIFromMultipleTrackedDevices()
17961832
AllEvents("pointerType", UIPointerType.Tracked),
17971833
AllEvents("pointerId", trackedDevice2.deviceId),
17981834
AllEvents("device", trackedDevice2),
1799-
AllEvents("trackedDeviceOrientation", Quaternion.Euler(0, -31, 0)),
1835+
AllEvents("trackedDeviceOrientation", scene.GetLookAtQuaternion(Vector3.zero, scene.leftGameObject, Vector3.left)),
18001836
OneEvent("type", EventType.PointerDown),
18011837
OneEvent("type", EventType.InitializePotentialDrag),
18021838
OneEvent("type", EventType.PointerUp),
@@ -1808,15 +1844,15 @@ public IEnumerator UI_CanDriveUIFromMultipleTrackedDevices()
18081844
scene.leftChildReceiver.events.Clear();
18091845

18101846
// Point first device at right child.
1811-
Set(trackedDevice1.deviceRotation, Quaternion.Euler(0, 30, 0));
1847+
Set(trackedDevice1.deviceRotation, scene.GetLookAtQuaternion(Vector3.zero, scene.rightGameObject));
18121848
yield return null;
18131849

18141850
Assert.That(scene.leftChildReceiver.events,
18151851
EventSequence(
18161852
AllEvents("pointerType", UIPointerType.Tracked),
18171853
AllEvents("pointerId", trackedDevice1.deviceId),
18181854
AllEvents("device", trackedDevice1),
1819-
AllEvents("trackedDeviceOrientation", Quaternion.Euler(0, 30, 0)),
1855+
AllEvents("trackedDeviceOrientation", scene.GetLookAtQuaternion(Vector3.zero, scene.rightGameObject)),
18201856
#if UNITY_2021_2_OR_NEWER
18211857
OneEvent("type", EventType.PointerMove),
18221858
#endif
@@ -1828,7 +1864,7 @@ public IEnumerator UI_CanDriveUIFromMultipleTrackedDevices()
18281864
AllEvents("pointerType", UIPointerType.Tracked),
18291865
AllEvents("pointerId", trackedDevice1.deviceId),
18301866
AllEvents("device", trackedDevice1),
1831-
AllEvents("trackedDeviceOrientation", Quaternion.Euler(0, 30, 0)),
1867+
AllEvents("trackedDeviceOrientation", scene.GetLookAtQuaternion(Vector3.zero, scene.rightGameObject)),
18321868
OneEvent("type", EventType.PointerEnter)
18331869
#if UNITY_2021_2_OR_NEWER
18341870
, OneEvent("type", EventType.PointerMove)
@@ -1840,15 +1876,15 @@ public IEnumerator UI_CanDriveUIFromMultipleTrackedDevices()
18401876
scene.rightChildReceiver.events.Clear();
18411877

18421878
// Point second device at right child.
1843-
Set(trackedDevice2.deviceRotation, Quaternion.Euler(0, 31, 0));
1879+
Set(trackedDevice2.deviceRotation, scene.GetLookAtQuaternion(Vector3.zero, scene.rightGameObject, Vector3.right));
18441880
yield return null;
18451881

18461882
Assert.That(scene.leftChildReceiver.events,
18471883
EventSequence(
18481884
AllEvents("pointerType", UIPointerType.Tracked),
18491885
AllEvents("pointerId", trackedDevice2.deviceId),
18501886
AllEvents("device", trackedDevice2),
1851-
AllEvents("trackedDeviceOrientation", Quaternion.Euler(0, 31, 0)),
1887+
AllEvents("trackedDeviceOrientation", scene.GetLookAtQuaternion(Vector3.zero, scene.rightGameObject, Vector3.right)),
18521888
#if UNITY_2021_2_OR_NEWER
18531889
OneEvent("type", EventType.PointerMove),
18541890
#endif
@@ -1860,7 +1896,7 @@ public IEnumerator UI_CanDriveUIFromMultipleTrackedDevices()
18601896
AllEvents("pointerType", UIPointerType.Tracked),
18611897
AllEvents("pointerId", trackedDevice2.deviceId),
18621898
AllEvents("device", trackedDevice2),
1863-
AllEvents("trackedDeviceOrientation", Quaternion.Euler(0, 31, 0)),
1899+
AllEvents("trackedDeviceOrientation", scene.GetLookAtQuaternion(Vector3.zero, scene.rightGameObject, Vector3.right)),
18641900
OneEvent("type", EventType.PointerEnter)
18651901
#if UNITY_2021_2_OR_NEWER
18661902
, OneEvent("type", EventType.PointerMove)
@@ -2185,7 +2221,7 @@ public IEnumerator UI_CanGetRaycastResultMatchingEvent()
21852221
scene.rightChildReceiver.events.Clear();
21862222

21872223
// Point device at left child.
2188-
Set(trackedDevice.deviceRotation, Quaternion.Euler(0, -30, 0));
2224+
Set(trackedDevice.deviceRotation, scene.GetLookAtQuaternion(Vector3.zero, scene.leftGameObject));
21892225
yield return null;
21902226

21912227
var raycastResult = scene.uiModule.GetLastRaycastResult(trackedDevice.deviceId);
@@ -2253,15 +2289,15 @@ public IEnumerator UI_XRTrackingOriginTransformModifiesTrackedPointers()
22532289
Assert.That(trackedDeviceRaycast.isValid, Is.False);
22542290

22552291
// Point device at left child.
2256-
Set(trackedDevice.deviceRotation, Quaternion.Euler(0, -30, 0));
2292+
Set(trackedDevice.deviceRotation, scene.GetLookAtQuaternion(Vector3.zero, scene.leftGameObject));
22572293
yield return null;
22582294

22592295
trackedDeviceRaycast = scene.uiModule.GetLastRaycastResult(trackedDevice.deviceId);
22602296
Assert.That(trackedDeviceRaycast.isValid, Is.True);
22612297
Assert.That(trackedDeviceRaycast.gameObject, Is.EqualTo(scene.leftGameObject));
22622298

22632299
// Rotate so right object is targetted
2264-
xrTrackingOrigin.rotation = Quaternion.Euler(0f, 60, 0f);
2300+
xrTrackingOrigin.rotation = scene.RotateFromTo(Vector3.zero, scene.leftGameObject, scene.rightGameObject);
22652301
yield return null;
22662302

22672303
trackedDeviceRaycast = scene.uiModule.GetLastRaycastResult(trackedDevice.deviceId);

0 commit comments

Comments
 (0)