Ultra Fast Selection of gameObjects Hidden behind others


This tool allows you to quickly select gameObjects behind others, with only two actions:
  • Clic
  • Scroll Up/Down !

Demonstration

The moment when this tool is most useful is when you have a lot of UI lined up on top of each other.

How do it work ?

I used reflection to access from the internal API of unity the function
Internal_PickClosestGO
. This function returns the closest gameObject in front of the camera, at the position of the mouse.
That's great, but what about the gameObject underneeth ?
This internal function have the nice feature : it has as parameter an ignore list of gameObjects. So everything we have to do, when the user clic on a gameObject, is to iterate, save the previous selected gameObject inside an ignore list, and iterate again with that new ignore list, until we reach the end.

Here the most important line 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 the scroll up & Down to browse this saved list of gameObjects. And that's pretty much it !







Minor problem:


I did, however, encounter a number of small problems, the first one is that when we operate a left clic with the mouse in the scene view, unity mark the event as "Used".

Which mean that the event is eated, 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 fake the clic Up, and it allow us to trigger the mouseUp AFTER the gameObject Selection.



Secondly, when selecting a prefabs, unity force the selection of the parent of the selected gameObject. I had to do a small ajustement so we can include that parent to the list:

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







Scripts

ClickAndScroll.cs
Download
Copy
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
    }
}
ExtEventEditor.cs
Download
Copy
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);
        }
    }
}









See also:

Enhance unity workflow by keeping track of files / assets / gameObject previously selected !

This tools allow you with simple button to access to previously selected objects in unity


Use extern directory inside your unity project using jonction

A Folder jonction can be pushed in git, unlike a simple symbolic link. Therefore you can have a "framework" directory shared betweens multiple unity projects