Блог программиста
Кастомизация редактора Unity. Пользовательские атрибуты
25.07.2017Кастомизация редактора

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

Рассмотрим наш класс юнита:


using UnityEngine;

public class Unit : MonoBehaviour
{
	[SerializeField]
	private string _name = "NoName";
	[SerializeField]
	private int _price = 10;
	[SerializeField]
	private float _health = 100f;
	//...code...
}

Поле "_health" может принимать значения в диапазоне от 0 до 100.
Для таких случаев существует атрибут "Range".
Передаём ему минимальное и максимальное значение, поле отображается в виде слайдера и мы не сможем выйти за диапазон.

[Range(0f, 100f)]
[SerializeField]
private float _health = 100f;
С "_health" всё отлично, идём дальше.

Поле "_price" не может быть меньше 1. Ограничений по максимуму нет, поэтому атрибут "Range" тут не поможет.

Можно было бы использовать в качестве максимального значения int.MaxValue или какое-то другое разумное значение, но слайдер крайне неудобен при больших диапазонах.
Атрибута для ограничения минимального значения в Unity нет, поэтому создадим его сами.
Для этого создадим класс-наследник от PropertyAttribute:
using UnityEngine;

public class MinAttribute : PropertyAttribute
{
	public float min = 0f;

	public MinAttribute(float min)
	{
		this.min = min;
	}
}

В поле "min" будет храниться минимальное значение, с которым мы и будем сверять значение переменной, к которой применяем атрибут.
Конструктор вызывается при применении атрибута.
Теперь нужно описать отображение и проверку поля с этим атрибутом. Для этого нужно создать скрипт редактора с классом-наследником от PropertyDrawer:

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(MinAttribute))]
public class MinDrawer : PropertyDrawer
{
	public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
	{
		//Получаем наш атрибут
		MinAttribute minAttr = attribute as MinAttribute;

		//Выводим поле
		EditorGUI.PropertyField (position, property, label);

		//Проверяем тип поля и применяем проверку
		if (property.propertyType == SerializedPropertyType.Float)
		{
			property.floatValue = Mathf.Max (minAttr.min, property.floatValue);
		}
		else if (property.propertyType == SerializedPropertyType.Integer)
		{
			property.intValue = (int)Mathf.Max (minAttr.min, property.intValue);
		}
	}
}

В "CustomPropertyDrawer" указывается атрибут, к которому применяется данный отрисовщик.
Переопределённый метод "OnGUI" будет вызываться Unity при отображении поля, к которому применён наш атрибут.
Функция имеет три параметра:
"position" - позиция и размер, заданные пользователем или рассчитанные Unity (при использовании EditorGUILayout).

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

"property" - поле, к которому применён атрибут.
"label" - имя поля отображаемое в редакторе.

С помощью свойства "attribute" мы получаем экземпляр нашего атрибута, т.е тот, в котором хранится минимальное значение именно для этого поля.
Отображаем поле уже знакомым нам методом "PropertyField".
Далее для каждого типа ограничиваем минимальное значение с помощью Mathf.Max.
Применяем к нашему полю "_price":

[Min(1)]
[SerializeField]
private int _price = 10f;
Внешне в редакторе изменений нет, но теперь нельзя указать значение меньше 1.

Переходим к полю "_name" и создадим новый атрибут.

using UnityEngine;

public class NonEmptyAttribute : PropertyAttribute
{
	public string defaultValue = "NonEmpty";

	public NonEmptyAttribute(string defValue)
	{
		this.defaultValue = defValue;
	}
}
"defaultValue" - значение "по-умолчанию", которое мы задаём при применении атрибута.
Создаём для него отрисовщик:
using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(NonEmptyAttribute))]
public class NonEmptyDrawer : PropertyDrawer
{
	public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
	{
		NonEmptyAttribute attr = attribute as NonEmptyAttribute;

		EditorGUI.PropertyField (position, property, label);

		if (string.IsNullOrEmpty (property.stringValue))
		{
			property.stringValue = attr.defaultValue;
		}
	}
}
Здесь мы проверяем значение поля и, если оно пустое, то присваиваем значение по-умолчанию.
Теперь мы может применять это атрибут к любым строковым полям.
[NonEmpty("NoName")]
[SerializeField]
private string _name = "NoName";

А что, если применить атрибут к полю типа int (или любого другого не строкового типа)?
В консоль посыпятся ошибки, что "тип не поддерживает строковое значение".
Избежать этого можно двумя способами:

  1. Проверять тип поля после вывода и, если тип строковый, производим проверки.
  2. Проверять тип поля до вывода и, если он не стоковый, вместо поля выводить сообщение, что атрибут только для строковых полей.
Но это уже попробуйте сами :)

В качестве "домашнего задания" можете создать атрибуты:

  1. Ограничивающий максимальное значение.
  2. Удаляющий пробелы в начале и в конце.

На этом всё :)

15404