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.
Комментарии (7)
Tar
26.10.2015 20:07Editor 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]Leopotam
26.10.2015 23:28А так же пропадает встроенный Undo, гарантированно обеспечиваемый в случае использования serializedObject. Об этом мало кто задумывается, пока не потребуется реализовать.
Stridemann
27.10.2015 06:44Первое с чем я столкнулся — как реализовать выпадающий список в редакторе. Находя первые темы на форумах в интернете, казалось, что это не так то просто, но терпение привело меня к использованию обычного перечисления
Вы можете овверайдить отрисовку проперти атрибутами, т. е. использовать 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(); } }
Получится такой же выпадающий список, только его значения можно брать от куда угодно без модификации кода
Dimcore
27.10.2015 19:44+1Простите, но статья даже не бесполезная, а вредная ибо новички сейчас начитаются этого и начнут делать также.
То, что у вас возникла ситуация необходимости такого подхода говорит о фиговости архитектуры и надо эти проблемы решать так, как описал BasmanovDaniil выше т.е. через возможности, которые нам щедро предоставляет ООП — интерфейсы, наследование и пр.
И да, почему минусанули gturk'а вверху, ведь он в чём-то прав по поводу God object'а — подход автора — это прямой путь к нему.
Всё, что описал автор можно понять прочитав доку по эдитор-скриптамFelixMind
27.10.2015 22:15Ваши доводы мне понятны и верны. В статье я не призываю читателей решать проблемы таким образом. Просто данный способ может оказаться полезным в некоторых ситуациях. А статью я написал, потому что на русском языке не нашёл никакой информации, плюс если у кого-то возникнут проблемы с доком по эдитор-скриптам, то ему не придётся всё испытывать на личном опыте, а только потом решать надо ему это или нет.
BasmanovDaniil
Проблему небольших отличий лучше решать наследованием, интерфейсами и прочим, ну да ладно.
А много у вас классов? Есть подозрение, что на большом количестве классов и переменных редактор будет подтормаживать при переключении между объектами: сначала будет залипать в OnEnable, а потом в OnInspectorGUI.
Я бы лучше сделал либо компонент-пустышку, который себя будет заменять на нужный, либо свою менюшку с попапом для добавления компонента. Сомневаюсь, что вам часто придётся менять тип компонента, когда он уже висит на объекте.