Aller au contenu principal

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

CTRL + molette vers le haut pour parcourir les GameObjects sous la souris

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​

Parcourir des éléments d’interface superposés au clic et à la molette

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.

Très bien, mais qu’en est-il des GameObjects en dessous ?

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
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
}
}

Télécharger 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);
}
}
}

Télécharger ExtEventEditor.cs

Fichiers source​

Voir aussi​