OK I’ve come up against what I think is a pretty serious UX issue with the EventSystem, and I’m looking for help in fixing it for my game. However I also think it needs addressing as part of Unity’s core code, as it’s a pretty major oversight that can cause frustration and confusion for the player.
(I’ll be honest, I’ve been using Unity since 2009 but I still find the Selectable system code base really confusing and full of UX issues, but that’s a whole other conversation. Let’s focus on this one issue for now.)
How to reproduce:
Set up a few Selectables in your canvas (e.g. 2 or more buttons) and an EventSystem.
Have one of the buttons selected on start and hit Play.
Note that you can navigate between the buttons using the keyboard directions or a controller, as expected.
Now, using either the mouse or touch input, deselect the currently-selected object (i.e. click or tap in empty space). Nothing should be highlighted (also as expected, as if you were using a mouse or touch interface).
Now try pressing a direction on the keyboard or controller again.
Note that nothing happens any more.
You now have to go back to the mouse or touch input, click/tap on a button and hold it down, then drag the cursor/finger off of it and release to get back into ‘button navigation mode’.
I noticed this on Nintendo Switch where my game supports both buttons and touch. A play-tester accidentally touched the touchscreen without realising, and then found that all of the buttons stopped working. The game essentially broke for them and they had no idea how to fix it.
What I would expect to happen (and how I’ve coded my own systems in the past) is that the EventSystem remembers the ‘last selected object’. If you press a direction when nothing is selected, it would immediately re-select that object and continue from there.
Hello! Yeah I ended up figuring out how to write my own fix for it.
The way it works is you add this script to the gameobject that has your StandaloneInputModule and EventSystem.
Every frame it checks to see what the EventSystem has currently selected and - if there is something selected - caches it.
Then if you press any buttons/axes and nothing is selected, it forces re-selection of the last cached object.
The input it looks for is the same as whatever you put in your StandaloneInputModule’s values for Horizontal Axis, Vertical Axis, Submit Button and Cancel Button. By default these are the standard Unity input names.
Hope this helps!
And if anyone from Unity is reading this… please make this the standard behaviour for the EventSystem.
using UnityEngine;
using UnityEngine.EventSystems;
[RequireComponent(typeof(StandaloneInputModule))]
public class ReselectLastSelectedOnInput : MonoBehaviour
{
private StandaloneInputModule standaloneInputModule;
private GameObject lastSelectedObject;
public static ReselectLastSelectedOnInput instance;
void Awake()
{
instance = this;
standaloneInputModule = GetComponent<StandaloneInputModule>();
}
void Update()
{
CacheLastSelectedObject();
if (EventSystemHasObjectSelected())
return;
// If any axis/submit/cancel is pressed.
// This looks at the input names defined in the attached StandaloneInputModule. You could use your own instead if you want.
if ((Input.GetAxisRaw(standaloneInputModule.horizontalAxis) != 0) ||
(Input.GetAxisRaw(standaloneInputModule.verticalAxis) != 0) ||
(Input.GetButtonDown(standaloneInputModule.submitButton)) ||
(Input.GetButtonDown(standaloneInputModule.cancelButton)))
{
// Reselect the cached 'lastSelectedObject'
ReselectLastObject();
return;
}
}
// Called whenever a UI navigation/submit/cancel button is pressed.
public static void ReselectLastObject()
{
// Do nothing if this is not active (maybe input objects were disabled)
if (!instance.isActiveAndEnabled || !instance.gameObject.activeInHierarchy)
return;
// Otherwise we can proceed with setting the currently selected object to be 'lastSelectedObject'...
// Current must be set to null first, otherwise it doesn't work properly because Unity UI is weird ¯\_(ツ)_/¯
EventSystem.current.SetSelectedGameObject(null);
// Set current to lastSelectedObject
EventSystem.current.SetSelectedGameObject(instance.lastSelectedObject);
}
// Returns whether or not the EventSystem has anything selected
static bool EventSystemHasObjectSelected()
{
if (EventSystem.current.currentSelectedGameObject == null)
return false;
else
return true;
}
// Caches last selected object for later use
void CacheLastSelectedObject()
{
// Don't cache if nothing is selected
if (EventSystemHasObjectSelected() == false)
return;
lastSelectedObject = EventSystem.current.currentSelectedGameObject.gameObject;
}
}
Sorry for not paying attention to the forum. This is awesome and will definitely help me.
In different scenes, I did push and pop of the button to ensure that the button can be re-selected after the scene is switched. Must be executed after the new scene has been displayed (active = true).
In the current scenes, plus the script you provide, it is perfect!
holy shit, that script was exacly what I was looking for, thank you very much.
If someone finds this thread and is using the new input system, here is the same code but modified a little bit so It works.
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem.UI;
[RequireComponent(typeof(InputSystemUIInputModule))]
public class ReselectLastSelectedOnInput : MonoBehaviour
{
private InputSystemUIInputModule standaloneInputModule;
private GameObject lastSelectedObject;
public static ReselectLastSelectedOnInput instance;
void Awake()
{
instance = this;
standaloneInputModule = GetComponent<InputSystemUIInputModule>();
}
void Update()
{
CacheLastSelectedObject();
if (EventSystemHasObjectSelected())
return;
// If any axis/submit/cancel is pressed.
// This looks at the input names defined in the attached StandaloneInputModule. You could use your own instead if you want.
if ( standaloneInputModule.move.action.WasPressedThisFrame() ||
standaloneInputModule.submit.action.WasPressedThisFrame() ||
standaloneInputModule.cancel.action.WasPressedThisFrame())
{
// Reselect the cached 'lastSelectedObject'
ReselectLastObject();
return;
}
}
// Called whenever a UI navigation/submit/cancel button is pressed.
public static void ReselectLastObject()
{
// Do nothing if this is not active (maybe input objects were disabled)
if (!instance.isActiveAndEnabled || !instance.gameObject.activeInHierarchy)
return;
// Otherwise we can proceed with setting the currently selected object to be 'lastSelectedObject'...
// Current must be set to null first, otherwise it doesn't work properly because Unity UI is weird ¯\_(ツ)_/¯
EventSystem.current.SetSelectedGameObject(null);
// Set current to lastSelectedObject
EventSystem.current.SetSelectedGameObject(instance.lastSelectedObject);
}
// Returns whether or not the EventSystem has anything selected
static bool EventSystemHasObjectSelected()
{
if (EventSystem.current.currentSelectedGameObject == null)
return false;
else
return true;
}
// Caches last selected object for later use
void CacheLastSelectedObject()
{
// Don't cache if nothing is selected
if (EventSystemHasObjectSelected() == false)
return;
lastSelectedObject = EventSystem.current.currentSelectedGameObject.gameObject;
}
}
Had the same problem but the solution turned out to be really simple. I’m using the new input system as well, in EventSystem > Input System UI Input Module > unticked “Deselect On Background Click” which was on by default:
While setting “Deselect On Background Click” to false should work for the vast majority of games there is one gotcha: We had a runtime level editor in Unity and when players were done editing number fields we wanted the level to save and the preview to update immediately. With “Deselect On Background Click” set to false, this would only work if player clicked into another number field. If they just clicked outside of the field to deselect it, it wouldn’t deselect and the automatic behavior wouldn’t happen. Users were so used to clicking outside the field to trigger the save + update that we had to turn “Deselct On Background Click” back on and find another solution.
It’s a less common case but it was an annoying bug when our level editor suddenly stopped working the way everyone expected it to.
I would like to share the script I have (which is used in production and shipped multiple titles), which has a few differences to the ones above:
it waits for a frame before attempting to select. This is because when the user navigates, we shouldn’t be checking if their current object is null, but rather their destination object will be null. If we check the current object (as some of the scripts above does), we could end up in a situation that we skip over a Selectable, since that navigation event both triggers our selection method as well as any other UI events (such as navigating a button). Imagine if we are currently on button A, In this scenario, the above scripts will attempt to perform 2 selections, and often, our selection is first, therefore we endup selecting button B → select button C in the same frame. When the user sees the UI, they would see we have skipped button B.
We use InputActionReference. this is the recommended way to refer to input actions in Unity’s new input system.
We use FindObjectsByType. This is the most recent unity object query method.