Skip to content

Commit da6732a

Browse files
author
Rene Damm
authored
FIX: Navigation not being confined to multiplayer playerRoot (case 1306361, #1443).
1 parent 8c0115c commit da6732a

3 files changed

Lines changed: 154 additions & 32 deletions

File tree

Assets/Tests/InputSystem/Plugins/UITests.cs

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ internal class UITests : CoreTestsFixture
4242
private struct TestObjects
4343
{
4444
public Camera camera;
45+
public Canvas canvas;
4546
public InputSystemUIInputModule uiModule;
4647
public TestEventSystem eventSystem;
4748
public GameObject parentGameObject;
@@ -102,7 +103,7 @@ private static TestObjects CreateUIScene()
102103
// Set up a InputSystemUIInputModule with a full roster of actions and inputs
103104
// and then see if we can generate all the various events expected by the UI
104105
// from activity on input devices.
105-
private static TestObjects CreateTestUI(Rect viewport = default, bool noFirstSelected = false, string namePrefix = "")
106+
private static TestObjects CreateTestUI(Rect viewport = default, bool noFirstSelected = false, string namePrefix = "", bool makeSelectable = false)
106107
{
107108
var objects = new TestObjects();
108109

@@ -126,6 +127,7 @@ private static TestObjects CreateTestUI(Rect viewport = default, bool noFirstSel
126127
canvasObject.AddComponent<GraphicRaycaster>();
127128
canvasObject.AddComponent<TrackedDeviceRaycaster>();
128129
canvas.worldCamera = objects.camera;
130+
objects.canvas = canvas;
129131

130132
// Set up a GameObject hierarchy that we send events to. In a real setup,
131133
// this would be a hierarchy involving UI components.
@@ -141,13 +143,17 @@ private static TestObjects CreateTestUI(Rect viewport = default, bool noFirstSel
141143
leftChildGameObject.AddComponent<Image>();
142144
objects.leftChildReceiver = leftChildGameObject.AddComponent<UICallbackReceiver>();
143145
objects.leftGameObject = leftChildGameObject;
146+
if (makeSelectable)
147+
leftChildGameObject.AddComponent<Selectable>();
144148

145149
var rightChildGameObject = new GameObject(namePrefix + "Right Child");
146150
rightChildGameObject.SetActive(false);
147151
var rightChildTransform = rightChildGameObject.AddComponent<RectTransform>();
148152
rightChildGameObject.AddComponent<Image>();
149153
objects.rightChildReceiver = rightChildGameObject.AddComponent<UICallbackReceiver>();
150154
objects.rightGameObject = rightChildGameObject;
155+
if (makeSelectable)
156+
rightChildGameObject.AddComponent<Selectable>();
151157

152158
parentTransform.SetParent(canvasObject.transform, worldPositionStays: false);
153159
leftChildTransform.SetParent(parentTransform, worldPositionStays: false);
@@ -2451,15 +2457,29 @@ public IEnumerator UI_CanOperateMultiplayerUIGloballyUsingMouse()
24512457

24522458
#endif
24532459

2454-
[UnityTest]
2455-
[Category("UI")]
24562460
// Check that two players can have separate UI and control it using separate gamepads, using
24572461
// MultiplayerEventSystem.
2462+
[UnityTest]
2463+
[Category("UI")]
24582464
public IEnumerator UI_CanOperateMultiplayerUILocallyUsingGamepads()
24592465
{
24602466
// Create devices.
24612467
var gamepads = new[] { InputSystem.AddDevice<Gamepad>(), InputSystem.AddDevice<Gamepad>() };
2462-
var players = new[] { CreateTestUI(new Rect(0, 0, 0.5f, 1)), CreateTestUI(new Rect(0.5f, 0, 0.5f, 1)) };
2468+
2469+
// Create scene with side-by-side split-screen.
2470+
var players = new[]
2471+
{
2472+
CreateTestUI(new Rect(0, 0, 0.5f, 1), namePrefix: "Player1", makeSelectable: true), // Left
2473+
CreateTestUI(new Rect(0.5f, 0, 0.5f, 1), namePrefix: "Player2", makeSelectable: true) // Right
2474+
};
2475+
2476+
// Offset player #2's canvas by moving its camera such that the resulting UI will be to the *right*
2477+
// of that of player #1. This is important as it will, by default, make player #2's UI navigatable
2478+
// from player #1 using the navigation logic in Selectable.
2479+
var screenWidthInWorldSpace =
2480+
Mathf.Abs(players[1].camera.ScreenToWorldPoint(new Vector3(0, 0, players[1].canvas.planeDistance)).x -
2481+
players[1].camera.ScreenToWorldPoint(new Vector3(Screen.width, 0, players[1].canvas.planeDistance)).x);
2482+
players[1].camera.transform.Translate(new Vector3(screenWidthInWorldSpace, 0, 0));
24632483

24642484
for (var i = 0; i < 2; i++)
24652485
{
@@ -2469,9 +2489,9 @@ public IEnumerator UI_CanOperateMultiplayerUILocallyUsingGamepads()
24692489
// Create actions.
24702490
var map = new InputActionMap("map");
24712491
asset.AddActionMap(map);
2472-
var moveAction = map.AddAction("move", type: InputActionType.PassThrough);
2473-
var submitAction = map.AddAction("submit", type: InputActionType.PassThrough);
2474-
var cancelAction = map.AddAction("cancel", type: InputActionType.PassThrough);
2492+
var moveAction = map.AddAction("move", type: InputActionType.Value);
2493+
var submitAction = map.AddAction("submit", type: InputActionType.Button);
2494+
var cancelAction = map.AddAction("cancel", type: InputActionType.Button);
24752495

24762496
// Create bindings.
24772497
moveAction.AddBinding(gamepads[i].leftStick);
@@ -2483,58 +2503,83 @@ public IEnumerator UI_CanOperateMultiplayerUILocallyUsingGamepads()
24832503
players[i].uiModule.submit = InputActionReference.Create(submitAction);
24842504
players[i].uiModule.cancel = InputActionReference.Create(cancelAction);
24852505

2486-
players[i].leftChildReceiver.moveTo = players[i].rightGameObject;
2487-
players[i].rightChildReceiver.moveTo = players[i].leftGameObject;
2488-
24892506
// Enable the whole thing.
24902507
map.Enable();
24912508
}
24922509

2493-
// We need to wait a frame to let the underlying canvas update and properly order the graphics images for raycasting.
24942510
yield return null;
24952511

24962512
Assert.That(players[0].eventSystem.currentSelectedGameObject, Is.SameAs(players[0].leftGameObject));
24972513
Assert.That(players[1].eventSystem.currentSelectedGameObject, Is.SameAs(players[1].leftGameObject));
24982514

2499-
// Reset initial selection
25002515
players[0].leftChildReceiver.events.Clear();
25012516
players[1].leftChildReceiver.events.Clear();
25022517

2503-
// Check Player 0 Move Axes
2504-
InputSystem.QueueDeltaStateEvent(gamepads[0].leftStick, new Vector2(1.0f, 0.0f));
2505-
2518+
// Move right on player #1's gamepad.
2519+
Set(gamepads[0].leftStick, Vector2.right);
25062520
yield return null;
25072521

2522+
// Player #1 should have moved from left to right object.
25082523
Assert.That(players[0].eventSystem.currentSelectedGameObject, Is.SameAs(players[0].rightGameObject));
25092524
Assert.That(players[1].eventSystem.currentSelectedGameObject, Is.SameAs(players[1].leftGameObject));
25102525

2511-
Assert.That(players[0].leftChildReceiver.events, Has.Count.EqualTo(2));
2512-
Assert.That(players[0].leftChildReceiver.events[0].type, Is.EqualTo(EventType.Move));
2513-
Assert.That(players[0].leftChildReceiver.events[1].type, Is.EqualTo(EventType.Deselect));
2514-
players[0].leftChildReceiver.events.Clear();
2526+
Assert.That(players[0].leftChildReceiver.events,
2527+
EventSequence(
2528+
OneEvent("type", EventType.Move),
2529+
OneEvent("type", EventType.Deselect)));
2530+
Assert.That(players[0].rightChildReceiver.events,
2531+
EventSequence(
2532+
OneEvent("type", EventType.Select)));
25152533

2516-
Assert.That(players[0].rightChildReceiver.events, Has.Count.EqualTo(1));
2517-
Assert.That(players[0].rightChildReceiver.events[0].type, Is.EqualTo(EventType.Select));
2534+
players[0].leftChildReceiver.events.Clear();
25182535
players[0].rightChildReceiver.events.Clear();
25192536

2537+
// No change for player #2.
2538+
Assert.That(players[1].leftChildReceiver.events, Is.Empty);
2539+
Assert.That(players[1].rightChildReceiver.events, Is.Empty);
2540+
2541+
// https://fogbugz.unity3d.com/f/cases/1306361/
2542+
// Move right on player #1's gamepad AGAIN. This should *not* cross
2543+
// over to player #2's UI but should instead not result in any selection change.
2544+
Set(gamepads[0].leftStick, Vector2.zero);
25202545
yield return null;
2546+
Set(gamepads[0].leftStick, Vector2.right);
2547+
yield return null;
2548+
2549+
Assert.That(players[0].eventSystem.currentSelectedGameObject, Is.SameAs(players[0].rightGameObject));
2550+
Assert.That(players[1].eventSystem.currentSelectedGameObject, Is.SameAs(players[1].leftGameObject));
2551+
2552+
Assert.That(players[0].leftChildReceiver.events, Is.Empty);
2553+
Assert.That(players[0].rightChildReceiver.events,
2554+
EventSequence(
2555+
OneEvent("type", EventType.Move))); // OnMove will still get called to *attempt* a move.
2556+
2557+
players[0].leftChildReceiver.events.Clear();
2558+
players[0].rightChildReceiver.events.Clear();
2559+
2560+
// No change for player #2.
2561+
Assert.That(players[1].leftChildReceiver.events, Is.Empty);
2562+
Assert.That(players[1].rightChildReceiver.events, Is.Empty);
2563+
2564+
Set(gamepads[0].leftStick, Vector2.zero);
25212565

25222566
// Check Player 0 Submit
25232567
PressAndRelease(gamepads[0].buttonSouth);
2524-
25252568
yield return null;
25262569

2527-
Assert.That(players[0].rightChildReceiver.events, Has.Count.EqualTo(1));
2528-
Assert.That(players[0].rightChildReceiver.events[0].type, Is.EqualTo(EventType.Submit));
2570+
Assert.That(players[0].rightChildReceiver.events,
2571+
EventSequence(OneEvent("type", EventType.Submit)));
2572+
Assert.That(players[1].leftChildReceiver.events, Is.Empty);
2573+
25292574
players[0].rightChildReceiver.events.Clear();
25302575

25312576
// Check Player 1 Submit
25322577
PressAndRelease(gamepads[1].buttonSouth);
2533-
25342578
yield return null;
25352579

2536-
Assert.That(players[1].leftChildReceiver.events, Has.Count.EqualTo(1));
2537-
Assert.That(players[1].leftChildReceiver.events[0].type, Is.EqualTo(EventType.Submit));
2580+
Assert.That(players[1].leftChildReceiver.events,
2581+
EventSequence(OneEvent("type", EventType.Submit)));
2582+
Assert.That(players[0].rightChildReceiver.events, Is.Empty);
25382583
}
25392584

25402585
[UnityTest]
@@ -3718,7 +3763,6 @@ public override string ToString()
37183763
}
37193764

37203765
public List<Event> events = new List<Event>();
3721-
public GameObject moveTo;
37223766

37233767
public void OnPointerClick(PointerEventData eventData)
37243768
{
@@ -3757,8 +3801,6 @@ public void OnPointerMove(PointerEventData eventData)
37573801
public void OnMove(AxisEventData eventData)
37583802
{
37593803
events.Add(new Event(EventType.Move, CloneAxisEventData(eventData)));
3760-
if (moveTo != null)
3761-
EventSystem.current.SetSelectedGameObject(moveTo, eventData);
37623804
}
37633805

37643806
public void OnSubmit(BaseEventData eventData)

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ however, it has to be formatted properly to pass verification tests.
3434
- Fixed calling `IsPressed` on an entire device returning `true` ([case 1374024](https://issuetracker.unity3d.com/issues/inputcontrol-dot-ispressed-always-returns-true-when-using-new-input-system)).
3535
- Fixed `InputSystem.RegisterLayoutOverride` resulting in the layout that overrides are being applied to losing the connection to its base layout ([case 1377719](https://fogbugz.unity3d.com/f/cases/1377719/)).
3636
- Fixed `Touch.activeTouches` still registering touches after the app loses focus ([case 1364017](https://issuetracker.unity3d.com/issues/input-system-new-input-system-registering-active-touches-when-app-loses-focus)).
37+
- Fixed `MultiplayerEventSystem` not preventing keyboard and gamepad/joystick navigation from one player's UI moving to another player's UI ([case 1306361](https://issuetracker.unity3d.com/issues/input-system-ui-input-module-lets-the-player-navigate-across-other-canvases)).
38+
* This fix relies on a `CanvasGroup` being injected into each `playerRoot` and the `interactable` property of the group being toggled back and forth depending on which part of the UI is being updated.
3739
- Fixed `InputTestFixture` incorrectly running input updates out of sync with the player loop ([case 1341740](https://issuetracker.unity3d.com/issues/buttoncontrol-dot-waspressedthisframe-is-false-when-using-inputtestfixture-dot-press)).
3840
* This had effects such as `InputAction.WasPressedThisFrame()` returning false expectedly.
3941

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

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#if PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI
2-
using UnityEngine;
32
using UnityEngine.EventSystems;
3+
using UnityEngine.InputSystem.Utilities;
44

55
namespace UnityEngine.InputSystem.UI
66
{
@@ -19,14 +19,92 @@ public class MultiplayerEventSystem : EventSystem
1919
[Tooltip("If set, only process mouse events for any game objects which are children of this game object.")]
2020
[SerializeField] private GameObject m_PlayerRoot;
2121

22+
/// <summary>
23+
/// The root object of the UI hierarchy that belongs to the given player.
24+
/// </summary>
25+
/// <remarks>
26+
/// This can either be an entire <c>Canvas</c> or just part of the hierarchy of
27+
/// a specific <c>Canvas</c>.
28+
///
29+
/// Note that if the given <c>GameObject</c> has a <c>CanvasGroup</c> component on it, its
30+
/// <c>interactable</c> property will be toggled back and forth by <see cref="MultiplayerEventSystem"/>.
31+
/// If no such component exists on the <c>GameObject</c>, one will be added automatically.
32+
///
33+
/// Only the <c>CanvasGroup</c> corresponding to the <see cref="MultiplayerEventSystem"/> that is currently
34+
/// executing its <see cref="Update"/> method (or did so last) will have <c>interactable</c> set to true.
35+
/// In other words, only the UI hierarchy corresponding to the player that is currently running a UI
36+
/// update (or that did so last) can be interacted with.
37+
/// </remarks>
2238
public GameObject playerRoot
2339
{
2440
get => m_PlayerRoot;
25-
set => m_PlayerRoot = value;
41+
set
42+
{
43+
m_PlayerRoot = value;
44+
InitializeCanvasGroup();
45+
}
46+
}
47+
48+
private CanvasGroup m_CanvasGroup;
49+
private bool m_CanvasGroupWasAddedByUs;
50+
51+
private static int s_MultiplayerEventSystemCount;
52+
private static MultiplayerEventSystem[] s_MultiplayerEventSystems;
53+
54+
protected override void OnEnable()
55+
{
56+
base.OnEnable();
57+
58+
ArrayHelpers.AppendWithCapacity(ref s_MultiplayerEventSystems, ref s_MultiplayerEventSystemCount, this);
59+
60+
InitializeCanvasGroup();
61+
}
62+
63+
private void InitializeCanvasGroup()
64+
{
65+
if (m_PlayerRoot != null)
66+
{
67+
m_CanvasGroup = m_PlayerRoot.GetComponent<CanvasGroup>();
68+
if (m_CanvasGroup == null)
69+
{
70+
m_CanvasGroup = m_PlayerRoot.AddComponent<CanvasGroup>();
71+
m_CanvasGroupWasAddedByUs = true;
72+
}
73+
else
74+
m_CanvasGroupWasAddedByUs = false;
75+
}
76+
else
77+
{
78+
m_CanvasGroup = null;
79+
}
80+
}
81+
82+
protected override void OnDisable()
83+
{
84+
var index = s_MultiplayerEventSystems.IndexOfReference(this);
85+
if (index != -1)
86+
s_MultiplayerEventSystems.EraseAtWithCapacity(ref s_MultiplayerEventSystemCount, index);
87+
88+
if (m_CanvasGroupWasAddedByUs)
89+
Destroy(m_CanvasGroup);
90+
91+
m_CanvasGroup = default;
92+
m_CanvasGroupWasAddedByUs = default;
93+
94+
base.OnDisable();
2695
}
2796

2897
protected override void Update()
2998
{
99+
for (var i = 0; i < s_MultiplayerEventSystemCount; ++i)
100+
{
101+
var system = s_MultiplayerEventSystems[i];
102+
if (system.m_PlayerRoot == null)
103+
continue;
104+
105+
system.m_CanvasGroup.interactable = system == this;
106+
}
107+
30108
var originalCurrent = current;
31109
current = this; // in order to avoid reimplementing half of the EventSystem class, just temporarily assign this EventSystem to be the globally current one
32110
try

0 commit comments

Comments
 (0)