Skip to main content

Ultra-fast selection of GameObjects hidden behind others

CTRL + scroll up to cycle through the GameObjects under the mouse

This tool allows you to quickly select GameObjects hidden behind others, with only two actions:

  • Click
  • Scroll up / down!

Demonstration​

Cycling through overlapping UI elements with click and scroll

This tool is most useful when you have a lot of UI elements lined up on top of each other.

How does it work?​

I used reflection to access the function Internal_PickClosestGO from Unity's internal API. This function returns the closest GameObject in front of the camera, at the position of the mouse.

That's great, but what about the GameObjects underneath?

This internal function has a nice feature: it takes a list of GameObjects to ignore as a parameter.

So all we have to do, when the user clicks on a GameObject, is to iterate: save the previously selected GameObject inside an ignore list, and iterate again with that new ignore list, until we reach the end.

Here are the most important lines of code of this tool:

private static void FindMethodByReflection()
{
Assembly editorAssembly = typeof(Editor).Assembly;
System.Type handleUtilityType = editorAssembly.GetType("UnityEditor.HandleUtility");
_internalPickClosestGameObject = handleUtilityType.GetMethod("Internal_PickClosestGO", BindingFlags.Static | BindingFlags.NonPublic);
}

private static GameObject PickObjectOnPos(Camera cam, int layers, Vector2 position, GameObject[] ignore, GameObject[] filter, out int materialIndex)
{
materialIndex = -1;
return (GameObject)_internalPickClosestGameObject.Invoke(null, new object[] { cam, layers, position, ignore, filter, materialIndex });
}

Then we handle scrolling up and down to browse this saved list of GameObjects. And that's pretty much it!

Minor problems​

I did, however, encounter a couple of small problems.

The click event is consumed​

The first one is that when we left-click with the mouse in the Scene view, Unity marks the event as "Used". This means the event is eaten, and therefore unusable for us.

That's why I have added the script ExtEventEditor.cs, with the function we're interested in: IsClickOnSceneView();. This method fakes the mouse up, which allows us to trigger the mouse up after the GameObject selection.

Prefab selection​

Secondly, when selecting a prefab, Unity forces the selection of the parent of the selected GameObject. I had to make a small adjustment so we can include that parent in the list:

if (_currentIndex == 0 && Selection.activeGameObject != _underneethGameObjects[0])
{
_underneethGameObjects.Insert(0, Selection.activeGameObject);
}

Scripts​

ClickAndScroll.cs
ClickAndScroll.cs
using hedCommon.extension.editor;
using hedCommon.extension.runtime;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;


namespace hedCommon.saveLastSelection
{
/// <summary>
/// This tool permit you to quickly select gameObjects behind others, with only two action:
/// - Clic
/// - Scroll Up/Down !
/// </summary>
[InitializeOnLoad]
public static class ClickAndScroll
{
private const int MAX_DEPTH = 40;
private static List<GameObject> _underneethGameObjects = new List<GameObject>(MAX_DEPTH);
private static MethodInfo _internalPickClosestGameObject;
private static int _currentIndex = 0;

static ClickAndScroll()
{
if (EditorApplication.isPlaying)
{
return;
}
SceneView.duringSceneGui += OnSceneGUI;
FindMethodByReflection();
_currentIndex = 0;
}


/// <summary>
/// Save reference of the Internal_PickClosestGO reflection method
/// </summary>
private static void FindMethodByReflection()
{
Assembly editorAssembly = typeof(Editor).Assembly;
System.Type handleUtilityType = editorAssembly.GetType("UnityEditor.HandleUtility");
_internalPickClosestGameObject = handleUtilityType.GetMethod("Internal_PickClosestGO", BindingFlags.Static | BindingFlags.NonPublic);
}

/// <summary>
/// pick a gameObject from the sceneView at a given mouse position
/// </summary>
/// <param name="cam">current camera</param>
/// <param name="layers">layer accepted</param>
/// <param name="position">mouse position</param>
/// <param name="ignore">ignored gameObjects</param>
/// <param name="filter"></param>
/// <param name="materialIndex"></param>
/// <returns></returns>
private static GameObject PickObjectOnPos(Camera cam, int layers, Vector2 position, GameObject[] ignore, GameObject[] filter, out int materialIndex)
{
materialIndex = -1;
return (GameObject)_internalPickClosestGameObject.Invoke(null, new object[] { cam, layers, position, ignore, filter, materialIndex });
}

/// <summary>
/// get the right click on sceneView
/// </summary>
/// <param name="sceneView"></param>
private static void OnSceneGUI(SceneView sceneView)
{
if (ExtEventEditor.IsClickOnSceneView(Event.current))
{
SaveAllGameObjectUnderneethMouse(Event.current.mousePosition, sceneView);
}
else
{
AttemptToScroll();
}
}

/// <summary>
/// attempt to create a contextMenu
/// </summary>
/// <param name="pos"></param>
/// <param name="sceneView"></param>
private static void SaveAllGameObjectUnderneethMouse(Vector2 pos, SceneView sceneView)
{
_underneethGameObjects.Clear();
Vector2 invertedPos = new Vector2(pos.x, sceneView.position.height - 16 - pos.y);

for (int i = 0; i <= MAX_DEPTH; i++)
{
GameObject clickedGameObject = PickObjectOnPos(sceneView.camera, ~0, invertedPos, _underneethGameObjects.ToArray(), null, out int matIndex);
if (clickedGameObject != null)
{
_underneethGameObjects.AddIfNotContain(clickedGameObject);
}
}
_currentIndex = 0;
}

private static void AttemptToScroll()
{
if (_underneethGameObjects.Count <= 1)
{
return;
}

bool isScrollingDown = ExtEventEditor.IsScrollingDown(Event.current, out float delta);
bool isScrollingUp = ExtEventEditor.IsScrollingUp(Event.current, out delta);

if (!isScrollingDown && !isScrollingUp)
{
return;
}

bool isShiftHeld = Event.current.shift;
bool isControlHeld = Event.current.control;
bool isSpecialKeyHeld = isShiftHeld || isControlHeld;

if (isSpecialKeyHeld && isScrollingDown)
{
if (_currentIndex == 0 && Selection.activeGameObject != _underneethGameObjects[0])
{
_underneethGameObjects.Insert(0, Selection.activeGameObject);
}
_currentIndex++;

SelectItem(isShiftHeld, isControlHeld);
ExtEventEditor.Use();
}
else if (isSpecialKeyHeld && isScrollingUp)
{
if (_currentIndex == 0 && Selection.activeGameObject != _underneethGameObjects[0])
{
_underneethGameObjects.Insert(0, Selection.activeGameObject);
}
else
{
_currentIndex--;
}
SelectItem(isShiftHeld, isControlHeld);
ExtEventEditor.Use();
}
}

private static void SelectItem(bool isShiftHeld, bool isControlHeld)
{
_currentIndex = SetBetween(_currentIndex, 0, _underneethGameObjects.Count - 1);
if (isControlHeld)
{
Selection.activeGameObject = _underneethGameObjects[_currentIndex];
}
if (isShiftHeld)
{
EditorGUIUtility.PingObject(_underneethGameObjects[_currentIndex]);
}
}

private static int SetBetween(int currentValue, int value1, int value2)
{
if (value1 > value2)
{
Debug.LogError("value2 can be less than value1");
return (0);
}

if (currentValue < value1)
{
currentValue = value1;
}
if (currentValue > value2)
{
currentValue = value2;
}
return (currentValue);
}

//end of class
}
}

Download ClickAndScroll.cs

ExtEventEditor.cs
ExtEventEditor.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace hedCommon.extension.editor
{
public static class ExtEventEditor
{
private static bool _wasMouseDown;
private static Vector2 _clickDownPosition;

public enum Modifier
{
NONE = 0,
CONTROL = 10,
SHIFT = 20,
ALT = 30,
}

public enum ButtonType
{
NONE = 0,
LEFT = 10,
RIGHT = 20,
MIDDLE = 30,
}

public static void Use()
{
if (Event.current.type != EventType.Layout && Event.current.type != EventType.Repaint)
{
Event.current.Use();
}
}

private static List<Modifier> SetupModifiers(Event current)
{
List<Modifier> modifiers = new List<Modifier>();
if (current.control)
{
modifiers.Add(Modifier.CONTROL);
}
if (current.alt)
{
modifiers.Add(Modifier.ALT);
}
if (current.shift)
{
modifiers.Add(Modifier.SHIFT);
}
return (modifiers);
}

/// <summary>
/// warning: current.button return 0 if nothing is pressed !! don't use alone
/// </summary>
/// <param name="current"></param>
/// <returns></returns>
private static ButtonType GetButtonType(Event current)
{
if (current.button == 0)
{
return (ButtonType.LEFT);
}
else if (current.button == 1)
{
return (ButtonType.RIGHT);
}
else if (current.button == 2)
{
return (ButtonType.MIDDLE);
}
return (ButtonType.NONE);
}


public static bool IsScrolling(Event current, out Vector2 delta)
{
delta = Vector2.zero;
int controlId = GUIUtility.GetControlID(FocusType.Passive);
EventType eventType = current.GetTypeForControl(controlId);
if (eventType == EventType.ScrollWheel)
{
delta = current.delta;
return (true);
}
return (false);
}

public static bool IsScrollingUp(Event current, out float delta)
{
delta = 0;
int controlId = GUIUtility.GetControlID(FocusType.Passive);
EventType eventType = current.GetTypeForControl(controlId);
if (eventType == EventType.ScrollWheel)
{
delta = current.delta.y;
return (delta > 0);
}
return (false);
}

public static bool IsScrollingDown(Event current, out float delta)
{
delta = 0;
int controlId = GUIUtility.GetControlID(FocusType.Passive);
EventType eventType = current.GetTypeForControl(controlId);
if (eventType == EventType.ScrollWheel)
{
delta = current.delta.y;
return (delta < 0);
}
return (false);
}

public static bool IsLeftMouseUp(Event current)
{
bool clicked = IsMouseClicked(current, out EventType eventType, out List<Modifier> modifiers, out ButtonType buttonType);
return (clicked && eventType == EventType.MouseUp && buttonType == ButtonType.LEFT);
}

public static bool IsLeftMouseDown(Event current)
{
bool clicked = IsMouseClicked(current, out EventType eventType, out List<Modifier> modifiers, out ButtonType buttonType);
return (clicked && eventType == EventType.MouseDown && buttonType == ButtonType.LEFT);
}

public static bool IsRightMouseUp(Event current, bool returnTrueIfMarkedAsUsed = false)
{
bool clicked = IsMouseClicked(current, out EventType eventType, out List<Modifier> modifiers, out ButtonType buttonType);

if (returnTrueIfMarkedAsUsed && eventType == EventType.Used && buttonType == ButtonType.RIGHT)
{
return (true);
}

return (clicked && eventType == EventType.MouseUp && buttonType == ButtonType.RIGHT);
}
public static bool IsRightMouseDown(Event current, bool returnTrueIfMarkedAsUsed = false)
{
bool clicked = IsMouseClicked(current, out EventType eventType, out List<Modifier> modifiers, out ButtonType buttonType);
if (returnTrueIfMarkedAsUsed && eventType == EventType.Used && buttonType == ButtonType.RIGHT)
{
return (true);
}

return (clicked && eventType == EventType.MouseDown && buttonType == ButtonType.RIGHT);
}

public static bool IsMiddleMouseUp(Event current)
{
bool clicked = IsMouseClicked(current, out EventType eventType, out List<Modifier> modifiers, out ButtonType buttonType);
return (clicked && eventType == EventType.MouseUp && buttonType == ButtonType.MIDDLE);
}
public static bool IsMiddleMouseDown(Event current)
{
bool clicked = IsMouseClicked(current, out EventType eventType, out List<Modifier> modifiers, out ButtonType buttonType);
return (clicked && eventType == EventType.MouseDown && buttonType == ButtonType.MIDDLE);
}

public static bool IsKeyUp(Event current, KeyCode keyCode)
{
if (current == null)
{
return (false);
}

int controlId = GUIUtility.GetControlID(FocusType.Passive);
EventType eventType = current.GetTypeForControl(controlId);
bool pressed = current.keyCode == keyCode && eventType == EventType.KeyUp;
return (pressed);
}

public static bool IsKeyDown(KeyCode keyCode)
{
if (Event.current == null)
{
return (false);
}
int controlId = GUIUtility.GetControlID(FocusType.Passive);
EventType eventType = Event.current.GetTypeForControl(controlId);
bool pressed = Event.current.keyCode == keyCode && eventType == EventType.KeyDown;
return (Event.current.keyCode == keyCode);
}

/// <summary>
/// return true if the left mouse button is Down
/// </summary>
/// <param name="current">Event.current (only in a OnGUI() loop)</param>
/// <returns>true if mouse is Down</returns>
public static bool IsMouseClicked(Event current, out EventType eventType, out List<Modifier> modifiers, out ButtonType buttonType)
{
int controlId = GUIUtility.GetControlID(FocusType.Passive);
modifiers = new List<Modifier>();
eventType = current.GetTypeForControl(controlId);

buttonType = GetButtonType(current);

if (eventType == EventType.MouseUp)
{
eventType = EventType.MouseUp;
modifiers = SetupModifiers(current);
return (true);
}

if (eventType == EventType.MouseDown)
{
eventType = EventType.MouseDown;
modifiers = SetupModifiers(current);
return (true);
}

return (false);
}

/// <summary>
/// DOESN'T WORK IN ONE SHOOT, need to be called at least twice,
/// once on mouseDown, and once on mouseUp
/// </summary>
/// <param name="current"></param>
/// <returns></returns>
public static bool IsClickOnSceneView(Event current)
{
if (ExtEventEditor.IsLeftMouseDown(current))
{
_clickDownPosition = current.mousePosition;
_wasMouseDown = true;
return (false);
}
if (!_wasMouseDown)
{
return (false);
}
if (current.type == EventType.Used && current.mousePosition == _clickDownPosition && current.delta == Vector2.zero)
{
_wasMouseDown = false;
return (true);
}
return (false);
}
}
}

Download ExtEventEditor.cs

Source files​

See also​