Sélection ultra-rapide des GameObjects cachés derrière d’autres

Cet outil permet de sélectionner rapidement des GameObjects cachés derrière d’autres, en seulement deux actions :
- un clic,
- la molette vers le haut / le bas !
Démonstration

Cet outil est surtout utile quand beaucoup d’éléments d’interface sont empilés les uns sur les autres.
Comment ça marche ?
J’ai utilisé la réflexion pour accéder à la fonction Internal_PickClosestGO de l’API interne d’Unity. Elle renvoie le GameObject le plus proche devant la caméra, à la position de la souris.
Cette fonction interne a une particularité bien pratique : elle prend en paramètre une liste de GameObjects à ignorer.
Il suffit donc, quand l’utilisateur clique sur un GameObject, d’itérer : ajouter le GameObject sélectionné à une liste d’exclusion, puis recommencer avec cette nouvelle liste, jusqu’à arriver au bout.
Voici les lignes de code les plus importantes de cet outil :
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 });
}
Ensuite, on gère la molette vers le haut et vers le bas pour parcourir cette liste de GameObjects. Et c’est à peu près tout !
Petits problèmes
J’ai tout de même rencontré quelques petits problèmes.
L’événement de clic est consommé
Le premier : quand on fait un clic gauche dans la Scene view, Unity marque l’événement comme « Used ». L’événement est donc consommé, et inutilisable pour nous.
C’est pourquoi j’ai ajouté le script ExtEventEditor.cs, avec la fonction qui nous intéresse : IsClickOnSceneView();. Cette méthode simule le relâchement de la souris, ce qui permet de déclencher le mouse up après la sélection du GameObject.
Sélection de prefabs
Ensuite, quand on sélectionne un prefab, Unity force la sélection du parent du GameObject choisi. J’ai dû faire un petit ajustement pour inclure ce parent dans la liste :
if (_currentIndex == 0 && Selection.activeGameObject != _underneethGameObjects[0])
{
_underneethGameObjects.Insert(0, Selection.activeGameObject);
}
Scripts
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
}
}
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);
}
}
}



