- PVSM.RU - https://www.pvsm.ru -

Лайфхаки редактора Unity 3D. Часть 1: Атрибуты

Лайфхаки редактора Unity 3D. Часть 1: Атрибуты - 1

Предисловие

Здравствуйте, друзья! Заметил, что многие программисты пропускают богатые возможности Unity кастомизации редакторского интерфейса по тем или иным причинам. В этой статье я распишу несколько совсем простых примеров, позволяющих облегчить жизнь геймдизайнерам и художникам, а также парочку примеров посложнее, но также легко реализуемых.

Большая часть взята из опыта использования, куда попала из родной документации движка. Вы легко можете сами найти необходимую информацию, поворошив документацию Unity 3D. Просто, по собственному опыту скажу, что у многих программистов либо нет времени, либо нет желания копаться в мантрах. Поэтому и выкладываю максимально краткое руководство по основным редакторским возможностям, которые я использовал на работе и в своих проектах.

Встроенные атрибуты

Я не буду расписывать все атрибуты, распишу лишь кратко те, которыми самому приходилось пользоваться.

Атрибуты к методам

Элемент меню

Скриншотики

Лайфхаки редактора Unity 3D. Часть 1: Атрибуты - 2

[MenuItem("Tools/Initialization Project")]

Позволяет создать меню для доступа к статическому методу. Через “/” указывается иерархия. Можно располагать новые кнопки в стандартном главном меню движка, указывая путь, например “File/Create New Asset”.

Всего может содержать три параметра.

string path //полный путь в меню
bool valudate //является ли данный метода валидатором функции (делает пункт меню неактивным)
int order //порядок расположения элемента в рамках одной иерархии

[MenuItem("Tools/Initialization Project", true)]
public static bool Initialization()
{
    //просто проверка на то, что выделен любой объект
    return Selection.gameObjects.Length > 0;
}

[MenuItem("Tools/Initialization Project")]
public static void Initialization()
{
    //do something...
}

Также, если использовать элементы главного меню, то дополнительная кнопка будет появляться не только там, но и в контекстном меню на правую кнопку мыши. Например, в своем проекте, я добавил копирование пути к ассету.

Атрибуты к переменным

Пример подписи, подсказки и клампера

Лайфхаки редактора Unity 3D. Часть 1: Атрибуты - 3

Ограничение вводимого значения

[Range(float min, float max)]

Можно сказать, это кастомный редактор для атрибута, который позволяет задать границы задаваемого значения через инспектор. Не клампит в реалтайме — только в инспекторе. Полезно, если задаете, например, вероятность выпадения предметов от 0 до 1 или от 0 до 100.

Подпись

[Header(string title)]

Задает подпись над сериализуемым полем, которая отображается в инспекторе.

Отступ

[Space]

Задает отступ в инспекторе.

Всплывающая подсказка

[Tooltip(string tip)]

Задает подсказку в инспекторе при наведении на сериализуемую переменную.

Сериализация переменных

[SerializeField]

Позволяет сериализовать переменные вне зависимости от их области видимости. Очень полезный атрибут, который позволяет сделать все переменные класса приватными, но настраиваемыми в инспекторе.

Запрет сериализации

[NonSerialized]

Позволяет убирать сериализацию у паблик переменных. Очень не рекомендую данных подход. Уж лучше определить свойство get;set; и получать данные по нему. Кроме того, свойство можно сделать виртуальным и перегрузить, при необходимости, в классах наследниках. А тот факт, что оно публичное, позволяет использовать его в интерфейсах.

Скрытие переменной в инспекторе

[HiddenInInspector]

Позволяет скрыть сериализуемое поле в инспекторе. Неважно, будет оно публичным или приватным/протектным с атрибутом SerializeField.

Атрибуты к классам

Исполнение в редакторе

[ExecuteInEditMode]

Позволяет работать скрипту в редакторе. В основном, полезно для постэффектов, поскольку позволяет сразу оценить результат в камере без запуска проекта. Но иногда можно использовать и для других целей.

Например, в качестве инициализатора сериализуемых полей встроенных типов, типа transform, renderer, rectTranform и т.п. Не рекомендовал бы повсеместно, лучше требовать ручной инициализации, либо написать редакторский скрипт, но иногда удобно.

Необходимость существования другого компонента

[RequireComponent(System.Type type)]

Заставляет редактор требовать наличие определенного компонента на том же объекте, на котором висит скрипт с данным атрибутом. При добавлении сразу создает на том же объекте компонент указанного типа. Также запрещает удалять уже добавленный компонент.

Новый элемент в меню добавления компонента

[AddComponentMenu(string path)]

Добавляет подменю в выпадающий список в меню Components →… и AddComponent. Удобно, если у вас большая библиотека кода и нужно организовать её в редакторе.

На этом, простая часть заканчивается и добавлю совсем немного в меру сложной.

Кастомные атрибуты (CustomPropertyDrawer)

Если вам недостаточно атрибутов приведенных выше, вы всегда можете воспользоваться API для написания собственных настраиваемых атрибутов. Реализация данного инструмента также достаточно проста и заключается в двух шагах.

Во-первых, нужно определить класс-наследник от стандартного класса PropertyAttribute. Я сразу создам его с конструктором, в котором входящим параметром будет путь к списку того, что нам нужно использовать в атрибуте.

public class IntAttribute : PropertyAttribute
{
    private string path = “”;

    public IntAttribute(string path)
    {
        this.path = path;
    }
}

Во-вторых, после этого создаем скрипт редактора, в котором будем рисовать этот самый новый класс. Его нужно унаследовать от PropertyDrawer, а также написать к нему атрибут CustomPropertyDrawer.

[CustomPropertyDrawer(typeof(IntAttribute ))]
public class IntAttributeDrawer : PropertyDrawer
{
}

Я называю классы наиболее общими наименованиями, дабы просто показать принцип использования настраиваемых.

База готова, теперь нам нужно нарисовать данный атрибут в том виде, в котором он нам нужен. В основном, атрибуты я использую в тех случаях, когда возможностей перечисления (enum) недостаточно, но нужно отрисовать выпадающий список с выбором.

Например, у вас есть база эффектов, у которой есть соответствие id → эффект. Вы храните где-то эту базу, неважнов ScriptableObject’e или на каком-то префабе. Вот простейшая реализация “хранилища”

Примечание — всегда создавайте в классах первое сериализуемое поле строковым. Из-за этого в списках элементы будут именоваться не как element 1, element 2.., а таким образом, каким вы назначите переменную в инспекторе.

Код

Для классов, с которыми я взаимодействую “извне”, я всегда пишу интерфейс. У каждого свой подход к этому моменту, но данный подход легко позволит, в случае чего, подменить класс только в одном месте на другой, а остальные так и будут работать с интерфейсом. Тем более, юнити поддерживает работу с интерфейсами в таких методах, как GetComponent(s)…, GetComponent(s)InChildren и т.п.

Интерфейс и класс эффекта

public interface IResource
{
    int ID
    {
        get;
    }
 
    string Name
    {
        get;
    }
}
 
[System.Serializable]
public class Effect : IResource
{
    [SerializeField]
    private string name = “”;
    [SerializeField]
    private int      id = 0;
 
    public int ID
    {
        get
        {
            return id;
        }
    }
 
    public string Name
    {
        get
        {
            return name;
        }
    }
}

Интерфейс и класс контейнера

public interface IContainer
{
    IResource[] Resources
    {
        get;
    }
}
 
public abstract class ResourcesContainer : MonoBehavior, IContainer
{
    public virtual IResource[] Resources
    {
        get 
        {
            return null;
        }
    }
}
 
public class EffectsContainer : ResourcesContainer 
{
    [SerializeField]
    private Effect[] effects = null;
    
    public override IResource[] Resources
    {
        get 
        {
            return effects;
        }
    }
}

Обычно, объекты с такими данными я располагаю в ресурсах, потом беру оттуда. Можно расположить и просто в проекте и где необходимо определить ссылки. Но я иду по более простому и уже проверенному на не одной платформе пути.

Редактор
Осталось дописать редактор:

[CustomPropertyDrawer(typeof(IntAttribute ))]
public class IntAttributeDrawer : PropertyDrawer
{
    protected string[]  values = null;
    protected List<int> idents = null;
 
    protected virtual void Init(SerializedProperty property)
    {
        if (attribute != null)
        {
            IntAttribute intAttribute = (IntAttribute)attribute;
            //можно ввести проверки на null, но, я думаю, вы сами справитесь
            IResource[] resources = Resources.Load<IContainer>(intAttribute.Path).Resources;
            values = new string[resources.Length + 1];
            idents = new List<int>(resources.Length + 1);
           
            //добавляем нулевой элемент для назначения -1 значения
            values[0] = “-1: None”;
            idents.Add(-1);
            for (int i = 0; i < resources.Length; i++)
            {
                values[i+1] = resources[i].ID + “: ” + resources[i].Path;
                idents.Add(resources[i].ID);
            }
        }
    }
 
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (property == null)
        {
            return;
        }
 
        Init(property);
        EditorGUI.BeginProperty(position, label, property);
 
        // Draw label
        position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
 
        // Don't make child fields be indented
        int indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;
 
        // Calculate rects
        Rect pathRect = new Rect(position.x, position.y, position.width - 6, position.height);
 
        int intValue = property.intValue;
        intValue = idents[EditorGUI.Popup(pathRect, Mathf.Max(0, idents.IndexOf(intValue)), Values)];
        property.intValue = intValue;
 
        EditorGUI.indentLevel = indent;
 
        EditorGUI.EndProperty();
    }
}

Располагаем префаб или ScriptableObject по нужному нам пути (я расположил в Resources/Effects/Container). 

Теперь в любом классе объявляем целочисленную переменную и атрибут к ней с путем до префаба.
 
public class Bullet : MonoBehavior
{
    [SerializeField]
    [IntAttribute(“Effects/Container”)]
    private int effectId = -1;    
}

Скриншот с атрибутом

Лайфхаки редактора Unity 3D. Часть 1: Атрибуты - 4

Заключение

Все приведенные выше «лайфхаки» могут упростить не только вашу работу (особенно когда проект разрабатывается несколько месяцев или лет), но и работу новичков, а также художников и геймдизайнеров. Не каждый специалист полезет в код. Конечно, хорошая организация и дисциплина может помочь и так документировать каждый компонент, но не всегда это получается, особенно у независимых разработчиков.

P.S.: Позже, если статья покажется полезной, напишу еще пару статей по другим типам апгрейдов редактора, в которые включу:

CustomEditor;
CustomPropertyDrawer;
EditorWindow;
Класс Debug и как его едят;
Класс Gizmos.

А также дополню примеры окном и пользовательским редактором. Пишите в комментариях, нужны ли подобные статьи или можно обойтись тем, что уже есть на Хабре.

Автор: BoobenDancer

Источник [1]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/c-2/258017

Ссылки в тексте:

[1] Источник: https://habrahabr.ru/post/331042/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox