Инженер-программист
0,0
рейтинг
26 октября 2015 в 15:25

Разработка → UnityEditor, динамическое содержимое редактора компонента из песочницы

Работая над своей игрой в Unity я подумал, зачем мне создавать множество однотипных скриптов описывающих поведение игровых объектов, но имеющих небольшие отличая друг от друга, если я мог бы создать один, а добавляя его как компонент объекту просто выбирал бы тип того самого объекта, а в редакторе (он же Inspector) видел бы только те свойства и поля, что имеют прямое отношение к выбранному типу. Так же это позволило бы в зависимости от выбора внутри скрипта использовать те или иные методы. В этом мне помогло пространство имён UnityEditor и её класс Editor.


Update 1
Полученные комментарии к публикации натолкнули предостеречь вас. Не смотря на привлекательность описанного метода для решения различных задач, не стоит им злоупотреблять, а использовать аккуратно и рационально. Не делайте так, чтобы ваш код из объектно-ориентированного превращался в кашу.


Первое с чем я столкнулся — как реализовать выпадающий список в редакторе. Находя первые темы на форумах в интернете, казалось, что это не так то просто, но терпение привело меня к использованию обычного перечисления:

using UnityEngine;
public class TestComponent : MonoBehaviour 
{
        //Объявляем перечисление с необходимыми вариантами выбора.
        //А затем объявляем переменную перечисления, которую будем менять в редакторе.
        public enum ComponentType {First = 1, Second = 2};
	public ComponentType component;
}


Результат

Далее. К примеру нам нужно, чтобы наш компонент хранил поле типа string содержащее текст. А также у нас должно быть две целочисленные переменные: первая имеет значение связанное с первым вариантом, вторая соответственно со вторым.

Итоговый файл TestComponent.cs:

using UnityEngine;
public class TestComponent : MonoBehaviour 
{
	public enum ComponentType {First = 1, Second = 2};
	public ComponentType component;
	public string componentName;
	public int variableComponentFirst;
	public int variableComponentSecond;
}


Получилось так, но это не то что нам требуется

Теперь мы хотим чтобы при выборе первого варианта в редакторе мы видели поле для редактирования только первой переменной, вторая скрыта. Если выбран второй вариант — наоборот. Эти переменные могут иметь значения от 0 до 100 и в редакторе представлены в виде ползунка (слайдера). А текстовое поле имеет значение также зависящее от выбранного варианта.

Для реализации всего этого я воспользовался пространством имён UnityEditor и наследование от класса Editor, позволяющие в данном случае перерисовать окно редактора так как нам того хочется прямо из кода. Код написанный для изменения редактора компонента не стоит писать в том же скрипте, где описана функциональность описываемого компонента, так как этот код используется только на стадии разработки, а в готовом проекте его не должно быть. Для этого в папке Assets надо создать специальную папку Editor и в ней хранить все скрипты призванные изменить вид редактора того или иного компонента. Скрипты содержащиеся в этой папке не будут мешаться в списке доступных скриптов при добавление оных через Inspector.

Соответствующий скрипт TestComponentEditor.cs с комментариями:

//Начинаем с добавления необходимого пространства имён
using UnityEditor;
using UnityEngine;

//Этим атрибутом мы объявляем какой компонент подвергнется редактированию
[CustomEditor( typeof(TestComponent) )]
[CanEditMultipleObjects]

public class TestComponentEditor : Editor 
{
	TestComponent subject;
	SerializedProperty compType;
	SerializedProperty compName;
	SerializedProperty varCompFirst;
	SerializedProperty varCompSecond;

	//Передаём этому скрипту компонент и необходимые в редакторе поля
	void OnEnable () 
	{
		subject = target as TestComponent;
		
		compType = serializedObject.FindProperty("component");
		compName = serializedObject.FindProperty ("componentName");

		varCompFirst = serializedObject.FindProperty ("variableComponentFirst");
		varCompSecond = serializedObject.FindProperty ("variableComponentSecond");
	}
	
	//Переопределяем событие отрисовки компонента
	public override void OnInspectorGUI() 
	{
		//Метод обязателен в начале. После него редактор компонента станет пустым и
		//далее мы с нуля отрисовываем его интерфейс.
		serializedObject.Update ();
		
		//Вывод в редактор выпадающего меню
		EditorGUILayout.PropertyField(compType);
		//Вывод в редактор текстового поля
		EditorGUILayout.PropertyField(compName);
		//Изменение значения текстового поля
		compName.stringValue = "None";
		
		//Проверка выбранного пункта в выпадающем меню, 
		if(subject.component == TestComponent.ComponentType.First)
		{
			//Вывод в редактор слайдера
			EditorGUILayout.IntSlider (varCompFirst, 0, 100, new GUIContent ("Variable First"));
			compName.stringValue = "First";
			
		}
		else if(subject.component == TestComponent.ComponentType.Second)
		{
			EditorGUILayout.IntSlider (varCompSecond, 0, 100, new GUIContent ("Variable Second"));
			compName.stringValue = "Second";
		}
		
		//Метод обязателен в конце
		serializedObject.ApplyModifiedProperties ();
	}
}


Выбран первый вариант


Выбран второй вариант

Кстати не обязательно стирать стандартный редактор, а потом полностью его перерисовывать. Если вы хотите сделать незначительные изменения в стандартном редакторе компонента, то можно использовать метод DrawDefaultInspector() для того чтобы не перерисовывать редактор компонента, а дополнять его. Пишем метод в самом начале события отрисовки редактора OnInspectorGUI(). Заметьте, при этом поля редактора созданные в скрипте TestComponentEditor не замещают публичные поля скрипта TestComponent в редакторе, а добавляются ниже. При этом оба будут работать с одной и той же переменной. Чтобы оставить новый вариант поля в редакторе, в скрипте TestComponent надо перед соответствующей публичной переменной добавить атрибут [HideInInspector].

Дальше можно было бы углубиться. Попробовать другие типы переменных и способы их редактирования. Вроде кривых для редактирования и прогресс баров для отображения значения. Класс EditorGUILayot и классы-соседи дают нам такие возможности. Также можно было бы поработать с какими-нибудь методами, когда в зависимости от выбора одни работают другие нет. Но всё это и многое другое оставлю вам для реализации ваших собственных идей, ограниченных вашим же воображением. Я лишь показал вам что можно было бы сделать.

Класс Editor в документации Unity.
Вячеслав @FelixMind
карма
5,0
рейтинг 0,0
Инженер-программист
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (8)

  • +2
    Проблему небольших отличий лучше решать наследованием, интерфейсами и прочим, ну да ладно.
    А много у вас классов? Есть подозрение, что на большом количестве классов и переменных редактор будет подтормаживать при переключении между объектами: сначала будет залипать в OnEnable, а потом в OnInspectorGUI.
    Я бы лучше сделал либо компонент-пустышку, который себя будет заменять на нужный, либо свою менюшку с попапом для добавления компонента. Сомневаюсь, что вам часто придётся менять тип компонента, когда он уже висит на объекте.
  • 0
    Editor script можно сильно упростить:

    [CustomEditor(typeof(TestComponent))]
    [CanEditMultipleObjects]
    public class TestComponentEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            TestComponent testComponent = (TestComponent)target;
    
            testComponent.component = (TestComponent.ComponentType) EditorGUILayout.EnumPopup("Choose", testComponent.component);
            switch (testComponent.component)
            {
                case TestComponent.ComponentType.First:
                    testComponent.variableComponentFirst = EditorGUILayout.IntSlider(testComponent.variableComponentFirst, 0, 100);
                    break;
                case TestComponent.ComponentType.Second:
                    testComponent.variableComponentSecond = EditorGUILayout.IntSlider(testComponent.variableComponentSecond, 0, 100);
                    break;
            }
        }
    }
    

    Хм, правда при этом он теряет возможность одновременного редактирования множества объектов даже при указанном [CanEditMultipleObjects]
    • 0
      А так же пропадает встроенный Undo, гарантированно обеспечиваемый в случае использования serializedObject. Об этом мало кто задумывается, пока не потребуется реализовать.
  • 0
    Да здравствует God object
  • 0
    Первое с чем я столкнулся — как реализовать выпадающий список в редакторе. Находя первые темы на форумах в интернете, казалось, что это не так то просто, но терпение привело меня к использованию обычного перечисления

    Вы можете овверайдить отрисовку проперти атрибутами, т. е. использовать CustomPropertyDrawer.

    Код:
    Scripts/TestCompAttribute.cs
    using System;
    using UnityEngine;
    
    [AttributeUsage(AttributeTargets.Field)]
    public class TestCompAttribute : PropertyAttribute
    {
        //Запишем значения здесь чтоб было в одном месте
        //Но эти значения можно черпать от куда угодно
        public static string[] AttributeValues = new string[]
        {
            "None",
            "First",
            "Second"
        };
    }
    


    Scripts/TestComponent.cs
    using UnityEngine;
    public class TestComponent : MonoBehaviour
    {
        [TestComp] //Атрибутом указываем, что у нас кастомная отрисовка этой проперти. (PS. TestComp вышло от TestCompAttribute без слова Attribute)
        public string componentName;
    
        public int variableComponentFirst;
        public int variableComponentSecond;
    }
    


    Scripts/Editor/TestComponentEditor.cs:
    using UnityEditor;
    using UnityEngine;
    
    //Указываем что будем оверрайдить стандартые отрисовки проперти помеченных TestComp атрибутом
    [CustomPropertyDrawer(typeof(TestCompAttribute))]
    public class TestCompAttribute_Drawer : PropertyDrawer
    {
        //Храним значение
        private int _selected = -1;
    
        //Оверрайдим стандартную отрисовку проперти
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            //Запускаем для будущей проверки изменений редактирования проперти
            EditorGUI.BeginChangeCheck();
    
            //Ищем значение в списке вариантов
            _selected = GetItemIndex(property.stringValue);
    
            //Выпадающий список
            _selected = EditorGUI.Popup(position, label.text.Replace("Component Name", "Select component type:"), _selected, TestCompAttribute.AttributeValues);
    
            //Проверяем был ли изменено проперти
            if (EditorGUI.EndChangeCheck())
            {
                //Устанавливаем в переменную скрипта componentName новое значение
                property.stringValue = TestCompAttribute.AttributeValues[_selected];
                property.serializedObject.ApplyModifiedProperties();
    
                //EditorUtility.SetDirty(property.serializedObject.targetObject);
            }
        }
    
        private int GetItemIndex(string id)
        {
            for (int i = 0; i < TestCompAttribute.AttributeValues.Length; ++i)
            {
                if (Equals(TestCompAttribute.AttributeValues[i], id))
                    return i;
            }
            return 0;
        }
    }
    
    
    //Этим атрибутом мы объявляем какой компонент подвергнется редактированию
    [CustomEditor(typeof(TestComponent))]
    [CanEditMultipleObjects]
    
    public class TestComponentEditor : Editor
    {
        TestComponent subject;
    
        SerializedProperty compName;
        SerializedProperty varCompFirst;
        SerializedProperty varCompSecond;
    
        //Передаём этому скрипту компонент и необходимые в редакторе поля
        void OnEnable()
        {
            subject = target as TestComponent;
    
            compName = serializedObject.FindProperty("componentName");
    
            varCompFirst = serializedObject.FindProperty("variableComponentFirst");
            varCompSecond = serializedObject.FindProperty("variableComponentSecond");
        }
    
        //Переопределяем событие отрисовки компонента
        public override void OnInspectorGUI()
        {
            //Метод обязателен в начале. После него редактор компонента станет пустым и
            //далее мы с нуля отрисовываем его интерфейс.
            serializedObject.Update();
    
            //Вывод в редактор текстового поля (который при отрисовке будет оверрайдится нашим TestCompAttribute_Drawer
            EditorGUILayout.PropertyField(compName);
    
            //Проверка выбранного пункта в выпадающем меню, 
            if (subject.componentName == TestCompAttribute.AttributeValues[1])
            {
                //Вывод в редактор слайдера
                EditorGUILayout.IntSlider(varCompFirst, 0, 100, new GUIContent("Variable First"));
            }
            else if (subject.componentName == TestCompAttribute.AttributeValues[2])
            {
                EditorGUILayout.IntSlider(varCompSecond, 0, 100, new GUIContent("Variable Second"));
            }
    
            //Метод обязателен в конце
            serializedObject.ApplyModifiedProperties();
        }
    }
    


    Получится такой же выпадающий список, только его значения можно брать от куда угодно без модификации кода

    image

  • +1
    Простите, но статья даже не бесполезная, а вредная ибо новички сейчас начитаются этого и начнут делать также.
    То, что у вас возникла ситуация необходимости такого подхода говорит о фиговости архитектуры и надо эти проблемы решать так, как описал BasmanovDaniil выше т.е. через возможности, которые нам щедро предоставляет ООП — интерфейсы, наследование и пр.

    И да, почему минусанули gturk'а вверху, ведь он в чём-то прав по поводу God object'а — подход автора — это прямой путь к нему.

    Всё, что описал автор можно понять прочитав доку по эдитор-скриптам
    • 0
      Ваши доводы мне понятны и верны. В статье я не призываю читателей решать проблемы таким образом. Просто данный способ может оказаться полезным в некоторых ситуациях. А статью я написал, потому что на русском языке не нашёл никакой информации, плюс если у кого-то возникнут проблемы с доком по эдитор-скриптам, то ему не придётся всё испытывать на личном опыте, а только потом решать надо ему это или нет.
      • +1
        Динамический UI — вещь полезная, но пример, в который она обернута далёк от того, в каких случаях стоит использовать его. Правильный пример работы можно подсмотреть в RectTransform'е (посложнее), LayoutElement'е (попроще) и т.д., которые входит в Unity UI. Если не понятно как это сделано у них (хотя там всё в общем-то очевидно), благо есть исходники, которые можно посмотреть здесь bitbucket.org/Unity-Technologies/ui или же декомпилировать Visual Studio (если включена настройка декомпайла) (я пользуюсь этим вариантом ибо быстро).

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.