Не так давно разработчики Юнити порадовали нас новой версией 4.5 (а в 4.6, совсем скоро, нас ждет новый GUI, ура!), и среди списка изменений один пункт касается сортировки в окне Hierarchy: «sorting of elements is now based on transform order instead of name».
Что это значит? Вы можете вручную перетаскивать элементы в окне иерархии в нужном вам порядке. И если раньше вы собирали сцену из расчета на автоматическую сортировку по имени, то теперь список объектов на сцене превратился в кашу.
Мы могли бы просто сделать как было… но сделаем лучше :)
На картинке вы видите привычную сортировку по имени, за исключением трёх вспомогательных объектов, которые располагаются внизу. Кроме того ориентироваться на сложных сценах будет проще за счет цветовой дифференциации штанов иконок.
Для начала хочу сказать, что Юнити чрезвычайно гибкий инструмент, который вы можете модифицировать по своему желанию, или использовать более тысячи готовых модификаций. Для этого нужно писать скрипты с использованием типов, расположенных в пространстве имен UnityEditor, а сами скрипты класть в папку Editor (предварительно создав её).
Первым делом посмотрим на BaseHierarchySort. Если вы работали с интерфейсом IComparable в C#, то сразу поймете, как это работает. В Юнити мы должны создать скрипт, который наследует от класса BaseHierarchySort и переопределяет метод int Compare(GameObject, GameObject), в котором вы сравниваете два объекта. Если первый должен идти до второго, метод должен вернуть -1, если после — 1, иначе — 0.
В документации рекомендуется использовать готовый метод EditorUtility.NaturalCompare(string, string), который сравнивает две строки «человеческим» образом (считая идущие друг за другом цифры одним числом). Таким образом «xx11» идет после «xx2».
Я больше привык к стандартной бесчеловечной виндовой сортировке, так что использую стандартный метод String.Compare(string, string):
public class CustomHierarchySorting : BaseHierarchySort
{
public override int Compare(GameObject lhs, GameObject rhs)
{
if (lhs == rhs) return 0;
if (lhs == null) return -1;
if (rhs == null) return 1;
if (lhs.tag == "Auxiliary" && rhs.tag != "Auxiliary")
return 1;
if (lhs.tag != "Auxiliary" && rhs.tag == "Auxiliary")
return -1;
return String.Compare(lhs.name, rhs.name);
}
}
В моём примере объекты с тэгом «Auxiliary» при сортировке окажутся в конце списка.
Как только этот скрипт окажется в папке Editor в окне Hierarchy появится кнопка, позволяющая выбрать, какой скрипт использовать для сортировки:
Отлично, нужный функционал мы быстро реализовали в несколько строчек. С иконками будет немного сложнее.
На скорую руку я нарисовал несколько разноцветных иконок и добавил их в папку «EditorIcons»:
Добавление иконок, как и различные типы сортировок оставили на реализацию пользователям Юнити, предоставив для этого набор методов и событий.
Мы будем использовать событие EditorApplication.hierarchyWindowChanged(), которое возникает каждый раз при обновлении окна иерархии и EditorApplication.hierarchyWindowItemOnGUI(int, Rect), который вызывается при отрисовке элемента в окне иерархии, передавая подписавшемуся методу id элемента и Rect — область отрисовки.
Так же мы будем использовать атрибут класса InitializeOnLoad, благодаря которому конструктор класса (в котором мы и подпишемся на эти события) будет вызываться автоматически, при запуске Юнити:
[InitializeOnLoad]
public class HierarchyIcons
{
static readonly Texture2D Cyan;
static readonly Texture2D Orange;
static readonly Texture2D Yellow;
static readonly Texture2D Gray;
static readonly List<int> CyanMarked = new List<int>();
static readonly List<int> OrangeMarked = new List<int>();
static readonly List<int> YellowMarked = new List<int>();
static readonly List<int> GrayMarked = new List<int>();
static HierarchyIcons()
{
Cyan = AssetDatabase.LoadAssetAtPath("Assets/Editor/Icons/IconCyan.png", typeof(Texture2D)) as Texture2D;
Orange = AssetDatabase.LoadAssetAtPath("Assets/Editor/Icons/IconOrange.png", typeof(Texture2D)) as Texture2D;
Yellow = AssetDatabase.LoadAssetAtPath("Assets/Editor/Icons/IconYellow.png", typeof(Texture2D)) as Texture2D;
Gray = AssetDatabase.LoadAssetAtPath("Assets/Editor/Icons/IconGray.png", typeof(Texture2D)) as Texture2D;
EditorApplication.hierarchyWindowChanged += Update;
EditorApplication.hierarchyWindowItemOnGUI += DrawHierarchyItemIcon;
}
static void Update()
{
CyanMarked.Clear();
OrangeMarked.Clear();
YellowMarked.Clear();
GrayMarked.Clear();
GameObject[] go = Object.FindObjectsOfType(typeof(GameObject)) as GameObject[];
foreach (GameObject g in go)
{
if (g == null) continue;
int instanceId = g.GetInstanceID();
if (g.tag == "Player")
OrangeMarked.Add(instanceId);
else if (g.tag == "Interactive")
CyanMarked.Add(instanceId);
else if (g.tag == "Auxiliary")
YellowMarked.Add(instanceId);
else
GrayMarked.Add(instanceId);
}
}
static void DrawHierarchyItemIcon(int instanceId, Rect selectionRect)
{
Rect r = new Rect(selectionRect);
r.x += r.width - 25;
r.width = 18;
if (CyanMarked.Contains(instanceId))
GUI.Label(r, Cyan);
if (OrangeMarked.Contains(instanceId))
GUI.Label(r, Orange);
if (YellowMarked.Contains(instanceId))
GUI.Label(r, Yellow);
if (GrayMarked.Contains(instanceId))
GUI.Label(r, Gray);
}
}
В этом примере при каждом обновлении HierarchyView пересобираются списки объектов, соответствующих разным иконкам. Для составления этих списков я использую определение по тэгу, но вы можете использовать поиск по компонентам объекта. Так можно присвоить иконку всем объектам, у которых есть Collider со свойством isTrigger = true, например.
Готово!
Отдельно хочу заметить, что такие манипуляции с иконками могут серьёзно сказаться на производительности во время работы в редакторе, особенно если вы будете использовать поиск по компонентам объектов. Для временного отключения отрисовки иконок, при необходимости протестировать производительность игры, вы могли бы добавить классу HierarchyIcons статические методы для подписки/отписки на события обновления окна иерархии.
Автор: Deadcow