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


Содержание


  • Часть 0. Перечень GUI элементов, используемых в статьях
  • Часть 1. Атрибуты
  • Часть 2. Окна
  • Часть 3. Редактор класса, наследника от MonoBehavior или ScriptableObject
  • Часть 4. Редактор класса, кастомный редактор для сериализуемой переменной

Предисловие


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

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

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


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

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


Элемент меню
Unity — Scripting API: MenuItem
Скриншотики


[MenuItem("Tools/Initialization Project")]

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

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

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

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

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

Кроме того, на методы можно назначить горячие клавиши. Для этого, прямо в пути к меню нужно написать необходимую комбинацию. Для этого нужно использовать один из служебных символов+буква.
% — ctrl в Windows или cmd в OSX
# — shift
& — alt
В моем проекте, с копированием пути к ассету это выглядит так
[MenuItem("Assets/Copy Path %&c")]
private static void CopyAssetPath()
{
}


Элемент контекстного меню
Unity — Scripting API: ContextMenu
public class UnitController : MonoBehavior 
{
    [SerializeField]
    private new Transform transform = null; //храним собственный трансформ в отдельной переменной, во имя оптимизации

    //инициализируем переменную при помощи вызова через контекстное меню
    [ContextMenu("Initialization")]
    public void Initialization()
    {
        tranform = GetComponent<Transform>();
    }
}


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


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


Ограничение вводимого значения
Unity — Scripting API: RangeAttribute
[Range(float min, float max)]

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

Подпись
Unity — Scripting API: HeaderAttribute
[Header(string title)]

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

Отступ
Unity — Scripting API: SpaceAttribute
[Space]

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

Всплывающая подсказка
Unity — Scripting API: TooltipAttribute
[Tooltip(string tip)]

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

Сериализация переменных
Unity — Scripting API: SerializeField
[SerializeField]

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

Запрет сериализации
Unity — Scripting API: NonSerializable
[NonSerializable]

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

Скрытие переменной в инспекторе
Unity — Scripting API: HideInInspector
[HiddenInInspector]

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

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


Создание экземпляра-наследника от ScriptableObject
Unity — Scripting API: CreateAssetMenuAttribute
[CreateAssetMenu(menuName = "Entity/Weapon")]

ScriptableObject — очень полезный класс, который позволяет хранить условную базу данных в проекте, не прибегая к префабам. В проекте создаются объекты с созданным вами типом. Можно работать также, как с префабами, имеют свои преимущества и недостатки. Вообще, этот класс — тема для отдельной статьи, небольшой, но информативной.
Указанный выше атрибут позволяет создать в проекте объект с вашим типом, по тому пути, где вы открыли контекстное меню.

Исполнение в редакторе
Unity — Scripting API: ExecuteInEditMode
[ExecuteInEditMode]

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

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

Необходимость существования другого компонента
Unity — Scripting API: RequireComponent
[RequireComponent(System.Type type)]

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

Новый элемент в меню добавления компонента
Unity — Scripting API: AddComponentMenu
[AddComponentMenu(string path)]

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

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

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


Unity — Scripting API: PropertyAttribute
Unity — Scripting API: PropertyDrawer
Если вам недостаточно атрибутов приведенных выше, вы всегда можете воспользоваться 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 : MonoBehaviour, 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;    
}

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


Заключение


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

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

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

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

Only registered users can participate in poll. Log in, please.

Опрос для улучшения качества подаваемого материала, чего не хватает статье?

  • 64.8%Больше изображений с примерами57
  • 21.6%Подробных комментариев в коде19
  • 19.3%Ссылок на документацию17
  • 42.0%В целом, всё хорошо37
  • 3.4%Другое (в комментарии)3

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 11

    0
    Вот что действительно полезное забыли, так это
    [CreateAssetMenu(menuName = "Custom/CreateSerializedAsset", fileName = "SerializedAsset.asset")]
    

    Для классов наследуемых от ScriptableObject.
      –1
      Добрый день, я его намеренно не включил, лучше, действительно, включить.
      Также забыл добавить ContextMenuItem и ContextMenu.
      Спасибо за замечание, вечером дополню.
      0
      Думаю многим будет полезно. В свое время львиная часть времени на проекте ушла на создание расширений инспектора, жаль тогда не существовало документации и примеров, по типу нынешнего расширения Odin, или ему подобных.

      Видна лишь часть, но для общей картины хватает :)
        0
        Оффтоп — сколько вижу в статьях или примерах в юнити (у самого опыт с юнити более чем скромный, сразу прошу прощения за нубский вопрос) — квадратная карта ограниченного размера и серое «ничто» вокруг. Но ведь в самих играх этого «края земли» не видно? Как это делается?
          0
          Через Bounding Box. Проще говоря, указываются координаты пределов карты, и если камера к ним приближается — просто блокируется ее передвижение.
            0
            Спасибо. И край закрывается какими-нибудь препятствиями, чтобы не видно было?
              0
              серое — это скайбокс. Он может быть любым, в зависимости от игры. Либо вообще не виден — способом, описанным выше
            0
            Ну на самом деле, так во многих движках, не только в Unity.
            На моей памяти, только в CryEngine не так, по дефолту.
            В Unreal, Source, Unity, Ogre, BigWorld та же ситуация.
            Основными решениями являются ограничения коллаидерами, как уже указали ниже, но есть и варианты.
            Где-то вуалируют данное решение отвесными стенами/домами/лесом, но подавляющее большинство даже не заморачиваются с этой проблемой — игрок просто утыкается в невидимый коллаидер и принимает ограничение игры как есть.
            Я и мои коллеги в основном, выступаем за более деликатные решения — либо, как я уже указал, 3D, либо можно логикой (например, в серии Battlefield пишут, что вы будете дезертиром, если не вернетесь на поле боя через n секунд).
            0
            Спасибо, обязательно буду развивать серию статей.
            0
            За лайвхак спасибо. Следующие статьи жду. Шишки все равно все будут набивать, это норма.
              0
              Кто мне может объяснить почему SerializeField называется именно SerializeField? В моем понимание сериализация — это представление данных в каком-то формате, например в JSON. А как сериализация относится к переменным?

              Only users with full accounts can post comments. Log in, please.