Редактор
Вот и перехожу я к самой интересной части. Вообще говоря, загляну немного в будущее, скажу что у нас будет ТРИ редактора:
один для способностей, другой для скриптовых объектов, и третий для некоего SpellManager который будет представлять собой
компонент который будет добавляться на объекты которые будут поддерживать систему способностей что мы разработаем.
По сравнению с редакторами способностей таких игр как StarCraft или WarCraft, мой редактор очень скромен на функционал, но
это лишь кажется изза того что другие редакторы уже заранее наполнены компонентами(свойствами) в способностях. В этом
редакторе, как я уже говорил, пользователь сам будет решать чем наполнять уникальную способность. Исходя из этого редактор
только предлагает пользователю сделать выбор какую из двух способностей он хочет создать: пассивную или активную. Дальше
редактор просто будет показывать доступные инструменты для работы со способностью.
Как видно на изображении я разбил редактор на три части.
Первая часть просто предоставляет пользователю выбор какую способность он хочет создать.
Вторая часть, уже после создания способности, предлагает наполнить ПУСТЫШКУ компонентами, эта важная часть - т к если вы
помните - у нас есть по два дополнительных типа "подспособности" которые имеют интсрументы для работы с компонентами. Так вот
эта вторая часть уже дает пользователю выбор при добавлении компонентов использовать следующий вид "подспособности" с
интерфейсом IAbilityComponentable и дальше пользователь переходит в третью часть.
Третья часть: в этой части редактор уже работает не с ПУСТЫШКОЙ, а со способностью в которую можно добавлять компоненты, и
эта часть показывает что за компоненты находятся в способности и что они из себя представляют.
На самом деле, тем кто сталкивался уже с работой о расширении редактора, эта часть статьи покажется знакомой потому что я не
буду использовать какие то сложные или хитрые схемы т к мы уже заранее создали всю базу и теперь просто надо представить это
все в визуальном виде - в виде редактора.
Для редактора я решил использовать специальное окно также известное как EditorWindow.
Ну а теперь переходим к программной части.
Я создал скрипт SpellWindow и унаследовал его от EditorWindow. Не забудьте подключить систему UnityEditor
Синтаксис:
Используется csharp
using System;
#if UNITY_EDITOR
using UnityEditor;
using System.IO;
public class SpellWindow : EditorWindow {
}
#endif
#if UNITY_EDITOR
using UnityEditor;
using System.IO;
public class SpellWindow : EditorWindow {
}
#endif
Как видите я заключил скрипт в некую область UNITY_EDITOR - эта область будет давать понять самому редактору unity3D что мы
работаем в режиме редактора и только в этом режиме, тоесть весь код который заключен в эту область не войдет в финальный билд
приложения.
Дальше я наполнил SpellWindow такими свойствами
defStyle - стиль по умолчанию, чисто визуальная корректировка, чтобы текст отображался симпатичнее.
localAbility - объект способности, будет служить нам переменной в которой будет содержаться редактируемая способность.
abilitiesComponent - интерфейс IAbilityComponentable объекта способности в котором мы будет содержать инструменты для работы
с компонентами.
localComponentType - тип компонента который пользователь захочет добавить.
Вот как выглядит скрипт теперь.
Синтаксис:
Используется csharp
using System;
#if UNITY_EDITOR
using UnityEditor;
using System.IO;
public class SpellWindow : EditorWindow {
public static GUIStyle defStyle = null;
private AbilityObject localAbility = null;
private IAbilityComponentable abilitiesComponent = null;
private AbilitySettings.ComponentValueTarget localComponentType;
}
#if UNITY_EDITOR
using UnityEditor;
using System.IO;
public class SpellWindow : EditorWindow {
public static GUIStyle defStyle = null;
private AbilityObject localAbility = null;
private IAbilityComponentable abilitiesComponent = null;
private AbilitySettings.ComponentValueTarget localComponentType;
}
Так ну теперь дальше я буду приводить только методы скрипта SpellWindow т к каждый раз копировать весь скрипт будет трудно.
Первая функция это знаменитый OnGUI, функция которая обрабатывает GUI элементы.
Синтаксис:
Используется csharp
private void OnGUI() {
if (this.isEditor) {
UpdateWindowGUI();
}
}
if (this.isEditor) {
UpdateWindowGUI();
}
}
Эта функция вызывает другую функцию UpdateWindowGUI в которой я уже буду работать с GUI элементами окна.
Синтаксис:
Используется csharp
private void UpdateWindowGUI() {
if (localAbility == null) DrawMainStartGUI();
else {
DrawAbilityControl();
GUILayout.Space(10);
DrawControlState();
}
}
if (localAbility == null) DrawMainStartGUI();
else {
DrawAbilityControl();
GUILayout.Space(10);
DrawControlState();
}
}
Функция DrawMainStartGUI будет рисовать область в окне когда пользователь будет только в ПЕРВОЙ части редактора, когда ему
будет представлен выбор типа способности.
Функция DrawAbilityControl будет рисовать область в окне когда пользователь уже создал способность определенного типа и
теперь работает именно с ней.
Функция DrawControlState будет рисовать две кнопки "Сохранить способность" и "Удалить способность".
Начнем с функции DrawMainStartGUI. Эта функция будет рисовать две кнопки "Passive" и "Active" которые будут создать экземпляры способности.
Синтаксис:
Используется csharp
private void DrawMainStartGUI() {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 15);
r.height += 25;
GUI.Box(r, "Create new ability");
GUILayout.BeginHorizontal();
if (GUILayout.Button("Passive")) this.localAbility = new PassiveAbility("New Passive");
if (GUILayout.Button("Active")) this.localAbility = new ActiveAbility("New Active");
GUILayout.EndHorizontal();
}
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 15);
r.height += 25;
GUI.Box(r, "Create new ability");
GUILayout.BeginHorizontal();
if (GUILayout.Button("Passive")) this.localAbility = new PassiveAbility("New Passive");
if (GUILayout.Button("Active")) this.localAbility = new ActiveAbility("New Active");
GUILayout.EndHorizontal();
}
Готово. В методе GUILayoutUtility.GetRect я использовал одну статическую переменную WindowSize из статического класса
EditorSettings который я создал специально чтобы он хранил нужные мне данные для редактора. В данном случае этот статический
класс содержит свойство WindowSize Vector2 который помогает мне определить размеры окна. Рекомендую вам тоже создать подобный
класс чтобы наполнить его данными как и в моем случае, т к нам пригодится лишний объект для более удобной работы с редактором.
Теперь когда пользователь нажмет одну из кнопок - он создаст способность ПУСТЫШКУ определенного типа, и дальше через функцию
DrawAbilityControl будет работать именно с ней.
Функция DrawAbilityControl, будет содержать поля со свойствами способности.
Попробуем нарисовать что то типа этого как на картинке.
Синтаксис:
Используется csharp
private void DrawAbilityControl() {
DrawAbilityMainSettings(this.localAbility);
}
DrawAbilityMainSettings(this.localAbility);
}
Вызовим функцию DrawAbilityMainSettings с параметром - экземпляром способности.
Синтаксис:
Используется csharp
private void DrawAbilityMainSettings(AbilityObject ability) {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 20);
GUI.Box(r, "Settings of: " + ability.name + " ability");
GUILayout.Label("Abilities description", defStyle);
ability.abilityDescription = GUILayout.TextArea(ability.abilityDescription);
ability.name = EditorGUILayout.TextField("Name: ", ability.name);
GUILayout.Space(5);
GUILayout.BeginHorizontal();
GUILayout.Label("Icon: ");
ability.icon = (Texture2D)EditorGUILayout.ObjectField(ability.icon, typeof(Texture2D), false);
GUILayout.EndHorizontal();
if (ability.type == AbilitySettings.AbilityType.Active) {
ActiveAbility activeAbility = (ActiveAbility)ability;
activeAbility.castKey = (KeyCode)EditorGUILayout.EnumPopup("Cast button: ", activeAbility.castKey);
activeAbility.cooldown = EditorGUILayout.FloatField("Cooldown: ", activeAbility.cooldown);
}
}
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 20);
GUI.Box(r, "Settings of: " + ability.name + " ability");
GUILayout.Label("Abilities description", defStyle);
ability.abilityDescription = GUILayout.TextArea(ability.abilityDescription);
ability.name = EditorGUILayout.TextField("Name: ", ability.name);
GUILayout.Space(5);
GUILayout.BeginHorizontal();
GUILayout.Label("Icon: ");
ability.icon = (Texture2D)EditorGUILayout.ObjectField(ability.icon, typeof(Texture2D), false);
GUILayout.EndHorizontal();
if (ability.type == AbilitySettings.AbilityType.Active) {
ActiveAbility activeAbility = (ActiveAbility)ability;
activeAbility.castKey = (KeyCode)EditorGUILayout.EnumPopup("Cast button: ", activeAbility.castKey);
activeAbility.cooldown = EditorGUILayout.FloatField("Cooldown: ", activeAbility.cooldown);
}
}
Как видите я выделил каждое поле под каждое свойство способности, но также я проверяю если наша способность "активная" то т к
этот тип способности содержит дополнительные свойства - я также должен их нарисовать.
Теперь вернемся к функции DrawAbilityControl и займемся областью с компонентами.
Синтаксис:
Используется csharp
private void DrawAbilityControl() {
DrawAbilityMainSettings(this.localAbility);
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 15);
GUILayout.Space(5);
r.height += 50;
GUI.Box(r, "Ability components settings");
this.localComponentType = (AbilitySettings.ComponentValueTarget)EditorGUILayout.EnumPopup("Select component type: ",
this.localComponentType);
GUILayout.BeginHorizontal();
if (this.abilitiesComponent != null) {
if (GUILayout.Button("Add component")) TryToAddComponent();
if (GUILayout.Button("Remove all")) this.abilitiesComponent.RemoveComponents<AbilityComponent>();
} else {
if (GUILayout.Button("Add component")) TryToAddComponent();
}
GUILayout.EndHorizontal();
GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 5);
if (this.abilitiesComponent != null) {
DrawAbilitiesComponents();
}
}
DrawAbilityMainSettings(this.localAbility);
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 15);
GUILayout.Space(5);
r.height += 50;
GUI.Box(r, "Ability components settings");
this.localComponentType = (AbilitySettings.ComponentValueTarget)EditorGUILayout.EnumPopup("Select component type: ",
this.localComponentType);
GUILayout.BeginHorizontal();
if (this.abilitiesComponent != null) {
if (GUILayout.Button("Add component")) TryToAddComponent();
if (GUILayout.Button("Remove all")) this.abilitiesComponent.RemoveComponents<AbilityComponent>();
} else {
if (GUILayout.Button("Add component")) TryToAddComponent();
}
GUILayout.EndHorizontal();
GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 5);
if (this.abilitiesComponent != null) {
DrawAbilitiesComponents();
}
}
И так, ну функцию DrawAbilityMainSettings мы уже рассмотрели, дальше я рисую область где с помощью EditorGUILayout.EnumPopup
пользователю предоставляется выбор какой тип компонента добавить в способность, как вы помните мы использовали перечисление
где указывали "цели" компонентов.
Дальше я проверяю если у редактируемой способности уже есть инструментарий для работы с компонентами в виде интерфейса
IAbilityComponentable - значит я рисую две кнопки, первая добавляет выбранный тип компонента и вторая удаляет все имеющиеся
компоненты у способности, если же у редактируемой способности нет этого интерфейса то тогда я просто рисую одну кнопку
которая будет добавлять выбранный компонент в способность.
Дальше я проверяю еще раз если у редактируемой способности имеется интерфейс IAbilityComponentable тогда с помощью функции
DrawAbilitiesComponents будем рисовать область с компонентами и их свойствами.
Так теперь сначала займемся добавлением компонентов а именно функцией TryToAddComponent. Но сначала в статической классе
EditorSettings создадим один метод который будет создавать нам способность которая будет содержать компоненты.
Синтаксис:
Используется csharp
public static class EditorSettings {
public static Vector2 WindowSize = new Vector2(250, 200);
public static AbilityObject CreateCustomAbility(AbilitySettings.AbilityType type) {
AbilityObject ability = null;
switch(type) {
case AbilitySettings.AbilityType.Active: ability = new ActiveCustomAbility("New Active custom");
break;
case AbilitySettings.AbilityType.Passive: ability = new PassiveCustomAbility("New Passive custom");
break;
}
return ability;
}
}
public static Vector2 WindowSize = new Vector2(250, 200);
public static AbilityObject CreateCustomAbility(AbilitySettings.AbilityType type) {
AbilityObject ability = null;
switch(type) {
case AbilitySettings.AbilityType.Active: ability = new ActiveCustomAbility("New Active custom");
break;
case AbilitySettings.AbilityType.Passive: ability = new PassiveCustomAbility("New Passive custom");
break;
}
return ability;
}
}
Как видите метод CreateCustomAbility берет в виде параметра тип способности AbilityType ,а дальше с помощью switch решает
какую способность создать - все очень просто.
Возвращаемся в функцию TryToAddComponent.
Синтаксис:
Используется csharp
private void TryToAddComponent() {
if (this.abilitiesComponent == null) {
this.localAbility = EditorSettings.CreateCustomAbility(this.localAbility.type);
this.abilitiesComponent = this.localAbility as IAbilityComponentable;
if (this.abilitiesComponent != null) TryToAddComponent();
} else {
AbilityComponent component = AbilitySettings.CreateComponentForTarget(this.localComponentType, "New component");
this.abilitiesComponent.AddComponent(component);
}
}
if (this.abilitiesComponent == null) {
this.localAbility = EditorSettings.CreateCustomAbility(this.localAbility.type);
this.abilitiesComponent = this.localAbility as IAbilityComponentable;
if (this.abilitiesComponent != null) TryToAddComponent();
} else {
AbilityComponent component = AbilitySettings.CreateComponentForTarget(this.localComponentType, "New component");
this.abilitiesComponent.AddComponent(component);
}
}
И так в этой функции будет происходить самая черная магия всего редактора.
Для начала я проверяю есть ли у редактируемой способности интерфейс IAbilityComponentable - если у способности его нет, что
значит - мы работает со способность ПУСТЫШКОЙ, в этой случае нам сначала нужно с помощью метода CreateCustomAbility
статического класса EditorSettings создать новую способность, но уже способность которая содержит инструменты для работы с
компонентами. Дальше я присваиваю переменной this.abilitiesComponent объект
способности но как(as) интерфейс IAbilityComponentable, и если все таки эта новая способность им обладает еще раз вызываем
функцию TryToAddComponent чтобы в этот раз способность добавила в себя выбранный ранее компонент т к теперь она имеет для
этого нужный инструментарий.
В области else я проверяю если редактируемая способность имеет инструменты для работы с компонентами, тогда я создаю новый
компонент с помощью метода AbilitySettings.CreateComponentForTarget который мы объявили в статическом классе AbilitySettings
когда работали с "целями" в прошлых частях статьи. И после создания компонента просто добавляем его в способность с помощью
метода AddComponent.
Ну теперь дальнейшие действия будут по проще.
Вернемся к кнопке "Remove all" которая будет стирать все компоненты из способности.
Как вы помните мы добавляли метод RemoveComponents<T> где с помощью преобразования находили нужный компонент и удаляли его,
так вот как вы еще должны помнить все компоненты в способности представлены в виде объекта AbilityComponent т к когда я
передаю этот тип в метод RemoveComponents<AbilityComponent> то выбирают все компоненты данного типа - а раз каждый компонент
это и есть AbilityComponent то выбирутся все компоненты в способности и удаляться. Вот такая вот простая махинация.
Опять возвращаемся к функции DrawAbilityControl
После всех этих кнопок я наконец хочу нарисовать область где буду отображать компоненты способности с помощью функции DrawAbilitiesComponents
Синтаксис:
Используется csharp
private void DrawAbilitiesComponents() {
if (this.abilitiesComponent == null) return;
int count = this.abilitiesComponent.components.Length;
GUILayout.Label("Ability components count: " + count, defStyle);
GUILayout.Space(5);
foreach(AbilityComponent component in this.abilitiesComponent.components) {
DrawComponentSettings(component);
}
}
if (this.abilitiesComponent == null) return;
int count = this.abilitiesComponent.components.Length;
GUILayout.Label("Ability components count: " + count, defStyle);
GUILayout.Space(5);
foreach(AbilityComponent component in this.abilitiesComponent.components) {
DrawComponentSettings(component);
}
}
Сначала я проверяю может ли редактируемая способность содержать в себе компоненты, дальше я рисую область где отображается
колво компонентов у способности.
Дальше я с помощью цикла буду рисовать специальную область под каждый компонент с помощью метода DrawComponentSettings
который будет брать в виде аргумента сам компонент. Перейдем теперь к методу DrawComponentSettings.
Но сначала теория!
И так что мы можем сказать о компоненте, какими свойствами он обладает!? Как вы знаете все компоненты в способности
сохраняются в общий список компонентов КАК объекты AbilityComponent а теперь если мы посмотрим какие свойствами обладает этот
объект: name, targetValue и objectValue, но эти все свойства не дают точного определения КАКОЕ именно значение в себе имеет
компонент, у нас есть свойство objectValue но: во первых это геттер который просто вернет значение, а во вторых редактор
unity3D не знает что это за значение и какую область рисовать для этого объекта, то ли это число или строка, или текстура - для
каждого этого объекта у unity3D есть свое определенное поле. Те кто раньше работал с расширением редактора могут решить: а
почему бы не использовать универсальную область PropertyField у редактора unity3D ведь он сам нарисует нам нужную область,
ведь для него нужно только знать тип значение, а его мы можем получить через свойство objectValue.GetType() и это верно. НО!
Это верно только для ШАБЛОНОВ, для компонентов с НЕОПРЕДЕЛЕННОЙ реализацией, которые содержать элементарные типы данных, а
что тогда делать с компонентами вроде AbilityTimeComponent ведь этот компонент уже имеет КОНКРЕТНУЮ реализацию, также он имеет
в себе посторонние свойства вроде timesRepeat, теперь мы не можем просто обойтись универсальной областью PropertyField. В
решении этой проблемы есть маленькая хитрость(не скажу что она больно уж оригинальна). Когда мы работаем с объектами на сцене
мы можем увидеть в окне Inspector все компоненты выбранного объекта - эти компоненты рисуют свои области для своих свойств,
тоесть нам не обязательно для каждого компонента отдельно расширять редактор чтобы нарисовать свойства этого компонента, это
уже имеется в наличии у стандартного редактора и он просто рисует свойства компонента через него самого - в этом и
заключается хитрость. В нашем случае редактор не знает сколько будет свойств у компонента, но мы можем просто отдать работу
по обработке свойств самому компоненту - пусть он сам рисует свойства которые содержит не вещая эту работу на редактор.
Для реализации этой хитрости мне нужно вызывать некую функцию у компонента которая будет содержать в себе нужную для
каждого компонента реализацию - следовательно мне нужно это сделать в базовом объекте AbilityComponent но при этом чтобы
каждый компонент мог сам решить какую реализацию выбрать в данной функции.
Я решил сделать эту функцию виртуальной и назвал ее DrawComponentBase, эта функция просто будет в зависимости от компонента
рисовать его свойства самостоятельно, но т к мы все еще работает в режиме редактора нужно будет эту функцию вынести в область
UNITY_EDITOR, и так вот как теперь выглядит базовый абстрактный класс AbilityComponent
Синтаксис:
Используется csharp
public abstract class AbilityComponent {
public AbilitySettings.ComponentValueTarget targetValue { get; protected set;}
public string name = "";
public virtual void DrawComponentBase() {
#if UNITY_EDITOR
this.name = EditorGUILayout.TextField("Name: ", this.name);
#endif
}
public bool CheckForAttribute<T>() where T : Attribute {
foreach(Attribute attribute in Attribute.GetCustomAttributes(this.GetType())) {
if (attribute is T) return true;
}
return false;
}
public abstract System.Object objectValue { get;}
}
public AbilitySettings.ComponentValueTarget targetValue { get; protected set;}
public string name = "";
public virtual void DrawComponentBase() {
#if UNITY_EDITOR
this.name = EditorGUILayout.TextField("Name: ", this.name);
#endif
}
public bool CheckForAttribute<T>() where T : Attribute {
foreach(Attribute attribute in Attribute.GetCustomAttributes(this.GetType())) {
if (attribute is T) return true;
}
return false;
}
public abstract System.Object objectValue { get;}
}
Как видите вся реализация функции DrawComponentBase заключена в область редактора чтобы допустим вызвав ее в рантайме не
произошла ошибка.
Я мог сделать эту функцию абстрактной, но тогда бы мне пришлось ее еще перегрузить в классе AbilityComponentValue и наполнить
тем же самым, поэтому я решил объявить ее виртуальной.
И так теперь когда я вызову эту функцию у компонента в редакторе способностей, компонент сам будет рисовать свои свойства, в
данном случае для начала он будет рисовать его имя.
А теперь вернемся в функцию DrawComponentSettings
Синтаксис:
Используется csharp
private void DrawComponentSettings(AbilityComponent component) {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 20);
GUI.Box(r, component.name);
component.DrawComponentBase();
if (GUILayout.Button("Remove component: " + component.targetValue)) TryToRemoveComponent(component);
}
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 20);
GUI.Box(r, component.name);
component.DrawComponentBase();
if (GUILayout.Button("Remove component: " + component.targetValue)) TryToRemoveComponent(component);
}
Как вы видите теперь я просто вызываю функцию DrawComponentBase у компонента и дальше редактору не нужно беспокоится сколько
и какие свойства находятся в компоненте.
А теперь попробуем построить что то типа этого как на картинке.
Теперь когда я независимо от типа компонента вызываю его редактор который будет рисовать только его свойства мне нужно чтобы
каждый шаблон в зависимости от типа своего значения рисовал нужную мне область - т к помните что Unity3D требует точно
определять какую область рисовать.
Переходим к шаблонам компонентов.
Первым будет шаблон AbilityFloatComponent который будет рисовать область для числа т к его значение имеет "float" тип.
Для начала перегрузим функцию DrawComponentBase
Синтаксис:
Используется csharp
public override void DrawComponentBase() {
base.DrawComponentBase();
#if UNITY_EDITOR
this.value = EditorGUILayout.FloatField(this.valueName, this.value);
#endif
}
base.DrawComponentBase();
#if UNITY_EDITOR
this.value = EditorGUILayout.FloatField(this.valueName, this.value);
#endif
}
Как вы видите я сначала вызываю функцию DrawComponentBase у базового типа который в свою очередь нарисует область с именем
компонента из того примера что в базовом абстрактном классе AbilityComponent и только потом, опять же заключая область
свойства в UNITY_EDITOR, я рисую область FloatField для компонента который будет хранить в себе данное значение. Ну а теперь
давайте сделаем это же для каждого шаблона, вот теперь как они будят выглядеть.
Синтаксис:
Используется csharp
public class AbilityFloatComponent : AbilityComponentValue<float> {
public override void DrawComponentBase () {
base.DrawComponentBase();
#if UNITY_EDITOR
this.value = EditorGUILayout.FloatField(this.valueName, this.value);
#endif
}
public AbilityFloatComponent(string name, AbilitySettings.ComponentValueTarget target) : base(name, target) {}
}
public class AbilityIntComponent : AbilityComponentValue<int> {
public override void DrawComponentBase () {
base.DrawComponentBase();
#if UNITY_EDITOR
this.value = EditorGUILayout.IntField(this.valueName, this.value);
#endif
}
public AbilityIntComponent(string name, AbilitySettings.ComponentValueTarget target) : base(name, target) {}
}
public class AbilityStringComponent : AbilityComponentValue<string> {
public override void DrawComponentBase () {
base.DrawComponentBase();
#if UNITY_EDITOR
this.value = EditorGUILayout.TextField(this.valueName, this.value);
#endif
}
public AbilityStringComponent(string name, AbilitySettings.ComponentValueTarget target) : base(name, target) {}
}
public class AbilityObjectComponent : AbilityComponentValue<UnityEngine.Object> {
public AbilityObjectComponent(string name, AbilitySettings.ComponentValueTarget target) : base(name, target) {}
}
public override void DrawComponentBase () {
base.DrawComponentBase();
#if UNITY_EDITOR
this.value = EditorGUILayout.FloatField(this.valueName, this.value);
#endif
}
public AbilityFloatComponent(string name, AbilitySettings.ComponentValueTarget target) : base(name, target) {}
}
public class AbilityIntComponent : AbilityComponentValue<int> {
public override void DrawComponentBase () {
base.DrawComponentBase();
#if UNITY_EDITOR
this.value = EditorGUILayout.IntField(this.valueName, this.value);
#endif
}
public AbilityIntComponent(string name, AbilitySettings.ComponentValueTarget target) : base(name, target) {}
}
public class AbilityStringComponent : AbilityComponentValue<string> {
public override void DrawComponentBase () {
base.DrawComponentBase();
#if UNITY_EDITOR
this.value = EditorGUILayout.TextField(this.valueName, this.value);
#endif
}
public AbilityStringComponent(string name, AbilitySettings.ComponentValueTarget target) : base(name, target) {}
}
public class AbilityObjectComponent : AbilityComponentValue<UnityEngine.Object> {
public AbilityObjectComponent(string name, AbilitySettings.ComponentValueTarget target) : base(name, target) {}
}
Как видите я не коснулся только двух компонентов это ШАБЛОНА AbilityObjectComponent т к я не знаю какой именно объект вы
будете хранить в этом шаблоне - в этом случае вам придется самим решать какого типа ObjectField вам рисовать. И я не тронул
компонент AbilityTimeComponent т к там помимо базовых свойств также содержаться дополнительные.
Синтаксис:
Используется csharp
[DisableComponentMultiplyUsage]
public class AbilityTimeComponent : AbilityFloatComponent {
private int componentRepeatTimes = 0;
public AbilitySettings.AbilityTimedType timeType;
public override void DrawComponentBase () {
base.DrawComponentBase();
#if UNITY_EDITOR
this.timeType = (AbilitySettings.AbilityTimedType)EditorGUILayout.EnumPopup("Timed type: ", this.timeType);
if (this.timeType == AbilitySettings.AbilityTimedType.Repeat) {
this.repeatTimes = EditorGUILayout.IntField("Repeat times: ", this.repeatTimes);
}
#endif
}
public int repeatTimes {
get { return this.componentRepeatTimes;}
set { this.componentRepeatTimes = (value < 0) ? 0 : value;}
}
public float time {
get { return base.value;}
set { base.value = (value < 0f) ? 0f : value;}
}
public AbilityTimeComponent(string name, AbilitySettings.ComponentValueTarget target) : base(name,
AbilitySettings.ComponentValueTarget.Time) {}
}
public class AbilityTimeComponent : AbilityFloatComponent {
private int componentRepeatTimes = 0;
public AbilitySettings.AbilityTimedType timeType;
public override void DrawComponentBase () {
base.DrawComponentBase();
#if UNITY_EDITOR
this.timeType = (AbilitySettings.AbilityTimedType)EditorGUILayout.EnumPopup("Timed type: ", this.timeType);
if (this.timeType == AbilitySettings.AbilityTimedType.Repeat) {
this.repeatTimes = EditorGUILayout.IntField("Repeat times: ", this.repeatTimes);
}
#endif
}
public int repeatTimes {
get { return this.componentRepeatTimes;}
set { this.componentRepeatTimes = (value < 0) ? 0 : value;}
}
public float time {
get { return base.value;}
set { base.value = (value < 0f) ? 0f : value;}
}
public AbilityTimeComponent(string name, AbilitySettings.ComponentValueTarget target) : base(name,
AbilitySettings.ComponentValueTarget.Time) {}
}
Вот как теперь выглядит функция DrawComponentBase у компонента AbilityTimeComponent, он будет рисовать помимо базовых
областей еще дополнительные для своих ОСОБЫХ свойств.
Ну теперь все готово и можно смело рисовать каждый компонент не взирая на его тип просто вызвав функцию DrawComponentBase.
И тогда возвращаемся в редактор.
Синтаксис:
Используется csharp
private void DrawComponentSettings(AbilityComponent component) {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 20);
GUI.Box(r, component.name);
component.DrawComponentBase();
if (GUILayout.Button("Remove component: " + component.targetValue)) TryToRemoveComponent(component);
}
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 20);
GUI.Box(r, component.name);
component.DrawComponentBase();
if (GUILayout.Button("Remove component: " + component.targetValue)) TryToRemoveComponent(component);
}
Теперь я думаю вам понятен принцип работы функции DrawComponentSettings.
Ну тогда завершим наш редактор функцией DrawControlState которая будет рисовать две кнопки сохранения и удаления редактируемой
способности.
Синтаксис:
Используется csharp
private void DrawControlState() {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 2.5f);
r.height += 27.5f;
GUI.Box(r, "");
GUILayout.BeginHorizontal();
if (GUILayout.Button("Save: " + this.localAbility.name)) TryToSave();
if (GUILayout.Button("Delete")) {}
GUILayout.EndHorizontal();
}
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 2.5f);
r.height += 27.5f;
GUI.Box(r, "");
GUILayout.BeginHorizontal();
if (GUILayout.Button("Save: " + this.localAbility.name)) TryToSave();
if (GUILayout.Button("Delete")) {}
GUILayout.EndHorizontal();
}
Ну тут все просто: рисуем кнопку "Save" которая пока что НЕ будет сохранять способность т к об этом я расскажу в другой части
статьи, и кнопку "Delete" которая будет стирать способность, он ней я тоже расскажу в другой части.
И так теперь нам нужно создать это окно в редакторе unity3D, это делается легко.
Я создал статическую функцию InitWindow в статической классе EditorSettings и добавил аттрибут MenuItem чтобы эту функция
вызывалась по нажатию MenuItem, вот что эта функция в себе содержит.
Синтаксис:
Используется csharp
[MenuItem("Window/Spell editor")]
public static void InitWindow() {
SpellWindow window = (SpellWindow)EditorWindow.GetWindow(typeof(SpellWindow));
window.maxSize = window.minSize = WindowSize;
if (SpellWindow.defStyle == null) {
SpellWindow.defStyle = new GUIStyle();
SpellWindow.defStyle.alignment = TextAnchor.LowerCenter;
SpellWindow.defStyle.fontStyle = FontStyle.Bold;
}
}
public static void InitWindow() {
SpellWindow window = (SpellWindow)EditorWindow.GetWindow(typeof(SpellWindow));
window.maxSize = window.minSize = WindowSize;
if (SpellWindow.defStyle == null) {
SpellWindow.defStyle = new GUIStyle();
SpellWindow.defStyle.alignment = TextAnchor.LowerCenter;
SpellWindow.defStyle.fontStyle = FontStyle.Bold;
}
}
При нажатии на кнопку выпадающего меню Window.Spell editor в окне редактора unity3D
Что изображено на картинке, я создам окно SpellWindow нашего редактора а также стиль defStyle который я объявил как
статический в SpellWindow.
Ну вот в принципе и готов редактор, точнее его окно.
Привожу весь код редакторы чтобы вы сверили то что получилось у вас.
Синтаксис:
Используется csharp
using UnityEngine;
using System.Collections;
using System;
#if UNITY_EDITOR
using UnityEditor;
using System.IO;
namespace AbilitiesSystem.Editor {
#region Editor settings
public static class EditorSettings {
public static Vector2 WindowSize = new Vector2(250, 200);
[MenuItem("Window/Spell editor")]
public static void InitWindow() {
SpellWindow window = (SpellWindow)EditorWindow.GetWindow(typeof(SpellWindow));
window.maxSize = window.minSize = WindowSize;
if (SpellWindow.defStyle == null) {
SpellWindow.defStyle = new GUIStyle();
SpellWindow.defStyle.alignment = TextAnchor.LowerCenter;
SpellWindow.defStyle.fontStyle = FontStyle.Bold;
}
}
public static AbilityObject CreateAbility(AbilitySettings.AbilityType type) {
AbilityObject ability = null;
switch(type) {
case AbilitySettings.AbilityType.Active: ability = new ActiveAbility("New Active");
break;
case AbilitySettings.AbilityType.Passive: ability = new PassiveAbility("New Passive");
break;
}
return ability;
}
public static AbilityObject CreateCustomAbility(AbilitySettings.AbilityType type) {
AbilityObject ability = null;
switch(type) {
case AbilitySettings.AbilityType.Active: ability = new ActiveCustomAbility("New Active custom");
break;
case AbilitySettings.AbilityType.Passive: ability = new PassiveCustomAbility("New Passive custom");
break;
}
return ability;
}
}
#endregion
#region Editor window
public class SpellWindow : EditorWindow {
public static GUIStyle defStyle = null;
private AbilityObject localAbility = null;
private IAbilityComponentable abilitiesComponent = null;
private AbilitySettings.ComponentValueTarget localComponentType;
private void OnGUI() {
if (this.isEditor) {
UpdateWindowGUI();
}
}
private void OnInspectorUpdate() {
if (this.isEditor) {
if (this.abilitiesComponent != null) {
EditorSettings.WindowSize.y = 250 + (100 * this.abilitiesComponent.components.Length);
this.maxSize = this.minSize = EditorSettings.WindowSize;
if (this.abilitiesComponent.components.Length <= 0) {
this.abilitiesComponent = null;
this.localAbility = EditorSettings.CreateAbility(this.localAbility.type);
}
}
}
}
private void UpdateWindowGUI() {
if (localAbility == null) DrawMainStartGUI();
else {
DrawAbilityControl();
GUILayout.Space(10);
DrawControlState();
}
}
private void DrawAbilityMainSettings(AbilityObject ability) {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 20);
GUI.Box(r, "Settings of: " + ability.name + " ability");
GUILayout.Label("Abilities description", defStyle);
ability.abilityDescription = GUILayout.TextArea(ability.abilityDescription);
ability.name = EditorGUILayout.TextField("Name: ", ability.name);
GUILayout.Space(5);
GUILayout.BeginHorizontal();
GUILayout.Label("Icon: ");
ability.icon = (Texture2D)EditorGUILayout.ObjectField(ability.icon, typeof(Texture2D), false);
GUILayout.EndHorizontal();
if (ability.type == AbilitySettings.AbilityType.Active) {
ActiveAbility activeAbility = (ActiveAbility)ability;
activeAbility.castKey = (KeyCode)EditorGUILayout.EnumPopup("Cast button: ", activeAbility.castKey);
activeAbility.cooldown = EditorGUILayout.FloatField("Cooldown: ", activeAbility.cooldown);
}
}
private void DrawAbilityControl() {
DrawAbilityMainSettings(this.localAbility);
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 15);
GUILayout.Space(5);
r.height += 50;
GUI.Box(r, "Ability components settings");
this.localComponentType = (AbilitySettings.ComponentValueTarget)EditorGUILayout.EnumPopup("Select component type: ", this.localComponentType);
GUILayout.BeginHorizontal();
if (this.abilitiesComponent != null) {
if (GUILayout.Button("Add component")) TryToAddComponent();
if (GUILayout.Button("Remove all")) this.abilitiesComponent.RemoveComponents<AbilityComponent>();
} else {
if (GUILayout.Button("Add component")) TryToAddComponent();
}
GUILayout.EndHorizontal();
GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 5);
if (this.abilitiesComponent != null) {
DrawAbilitiesComponents();
}
}
private void DrawAbilitiesComponents() {
if (this.abilitiesComponent == null) return;
int count = this.abilitiesComponent.components.Length;
GUILayout.Label("Ability components count: " + count, defStyle);
GUILayout.Space(5);
foreach(AbilityComponent component in this.abilitiesComponent.components) {
DrawComponentSettings(component);
}
}
private void DrawComponentSettings(AbilityComponent component) {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 20);
GUI.Box(r, component.name);
component.DrawComponentBase();
if (GUILayout.Button("Remove component: " + component.targetValue)) TryToRemoveComponent(component);
}
private void TryToRemoveComponent(AbilityComponent component) {
if (this.abilitiesComponent != null)
this.abilitiesComponent.RemoveComponent(component);
}
private void TryToRemoveComponent(int componentIndex) {
if (componentIndex < 0 || this.abilitiesComponent == null) return;
else if (componentIndex >= this.abilitiesComponent.components.Length) return;
TryToRemoveComponent(this.abilitiesComponent.components[componentIndex]);
}
private void TryToAddComponent() {
if (this.abilitiesComponent == null) {
string oldName = this.localAbility.name;
this.localAbility = EditorSettings.CreateCustomAbility(this.localAbility.type);
this.localAbility.name = oldName;
this.abilitiesComponent = this.localAbility as IAbilityComponentable;
if (this.abilitiesComponent != null) TryToAddComponent();
} else {
AbilityComponent com = AbilitySettings.CreateComponentForTarget(this.localComponentType, "New component");
this.abilitiesComponent.AddComponent(com);
}
}
private void TryToSave() {
}
private void DrawMainStartGUI() {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 15);
r.height += 25;
GUI.Box(r, "Create new ability");
GUILayout.BeginHorizontal();
if (GUILayout.Button("Passive")) this.localAbility = EditorSettings.CreateAbility(AbilitySettings.AbilityType.Passive);
if (GUILayout.Button("Active")) this.localAbility = EditorSettings.CreateAbility(AbilitySettings.AbilityType.Active);
GUILayout.EndHorizontal();
}
private void DrawControlState() {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 2.5f);
r.height += 27.5f;
GUI.Box(r, "");
GUILayout.BeginHorizontal();
if (GUILayout.Button("Save: " + this.localAbility.name)) TryToSave();
if (GUILayout.Button("Delete")) {}
GUILayout.EndHorizontal();
}
public bool isEditor {
get { return Application.isPlaying == false;}
}
}
#endregion
}
#endif
using System.Collections;
using System;
#if UNITY_EDITOR
using UnityEditor;
using System.IO;
namespace AbilitiesSystem.Editor {
#region Editor settings
public static class EditorSettings {
public static Vector2 WindowSize = new Vector2(250, 200);
[MenuItem("Window/Spell editor")]
public static void InitWindow() {
SpellWindow window = (SpellWindow)EditorWindow.GetWindow(typeof(SpellWindow));
window.maxSize = window.minSize = WindowSize;
if (SpellWindow.defStyle == null) {
SpellWindow.defStyle = new GUIStyle();
SpellWindow.defStyle.alignment = TextAnchor.LowerCenter;
SpellWindow.defStyle.fontStyle = FontStyle.Bold;
}
}
public static AbilityObject CreateAbility(AbilitySettings.AbilityType type) {
AbilityObject ability = null;
switch(type) {
case AbilitySettings.AbilityType.Active: ability = new ActiveAbility("New Active");
break;
case AbilitySettings.AbilityType.Passive: ability = new PassiveAbility("New Passive");
break;
}
return ability;
}
public static AbilityObject CreateCustomAbility(AbilitySettings.AbilityType type) {
AbilityObject ability = null;
switch(type) {
case AbilitySettings.AbilityType.Active: ability = new ActiveCustomAbility("New Active custom");
break;
case AbilitySettings.AbilityType.Passive: ability = new PassiveCustomAbility("New Passive custom");
break;
}
return ability;
}
}
#endregion
#region Editor window
public class SpellWindow : EditorWindow {
public static GUIStyle defStyle = null;
private AbilityObject localAbility = null;
private IAbilityComponentable abilitiesComponent = null;
private AbilitySettings.ComponentValueTarget localComponentType;
private void OnGUI() {
if (this.isEditor) {
UpdateWindowGUI();
}
}
private void OnInspectorUpdate() {
if (this.isEditor) {
if (this.abilitiesComponent != null) {
EditorSettings.WindowSize.y = 250 + (100 * this.abilitiesComponent.components.Length);
this.maxSize = this.minSize = EditorSettings.WindowSize;
if (this.abilitiesComponent.components.Length <= 0) {
this.abilitiesComponent = null;
this.localAbility = EditorSettings.CreateAbility(this.localAbility.type);
}
}
}
}
private void UpdateWindowGUI() {
if (localAbility == null) DrawMainStartGUI();
else {
DrawAbilityControl();
GUILayout.Space(10);
DrawControlState();
}
}
private void DrawAbilityMainSettings(AbilityObject ability) {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 20);
GUI.Box(r, "Settings of: " + ability.name + " ability");
GUILayout.Label("Abilities description", defStyle);
ability.abilityDescription = GUILayout.TextArea(ability.abilityDescription);
ability.name = EditorGUILayout.TextField("Name: ", ability.name);
GUILayout.Space(5);
GUILayout.BeginHorizontal();
GUILayout.Label("Icon: ");
ability.icon = (Texture2D)EditorGUILayout.ObjectField(ability.icon, typeof(Texture2D), false);
GUILayout.EndHorizontal();
if (ability.type == AbilitySettings.AbilityType.Active) {
ActiveAbility activeAbility = (ActiveAbility)ability;
activeAbility.castKey = (KeyCode)EditorGUILayout.EnumPopup("Cast button: ", activeAbility.castKey);
activeAbility.cooldown = EditorGUILayout.FloatField("Cooldown: ", activeAbility.cooldown);
}
}
private void DrawAbilityControl() {
DrawAbilityMainSettings(this.localAbility);
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 15);
GUILayout.Space(5);
r.height += 50;
GUI.Box(r, "Ability components settings");
this.localComponentType = (AbilitySettings.ComponentValueTarget)EditorGUILayout.EnumPopup("Select component type: ", this.localComponentType);
GUILayout.BeginHorizontal();
if (this.abilitiesComponent != null) {
if (GUILayout.Button("Add component")) TryToAddComponent();
if (GUILayout.Button("Remove all")) this.abilitiesComponent.RemoveComponents<AbilityComponent>();
} else {
if (GUILayout.Button("Add component")) TryToAddComponent();
}
GUILayout.EndHorizontal();
GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 5);
if (this.abilitiesComponent != null) {
DrawAbilitiesComponents();
}
}
private void DrawAbilitiesComponents() {
if (this.abilitiesComponent == null) return;
int count = this.abilitiesComponent.components.Length;
GUILayout.Label("Ability components count: " + count, defStyle);
GUILayout.Space(5);
foreach(AbilityComponent component in this.abilitiesComponent.components) {
DrawComponentSettings(component);
}
}
private void DrawComponentSettings(AbilityComponent component) {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 20);
GUI.Box(r, component.name);
component.DrawComponentBase();
if (GUILayout.Button("Remove component: " + component.targetValue)) TryToRemoveComponent(component);
}
private void TryToRemoveComponent(AbilityComponent component) {
if (this.abilitiesComponent != null)
this.abilitiesComponent.RemoveComponent(component);
}
private void TryToRemoveComponent(int componentIndex) {
if (componentIndex < 0 || this.abilitiesComponent == null) return;
else if (componentIndex >= this.abilitiesComponent.components.Length) return;
TryToRemoveComponent(this.abilitiesComponent.components[componentIndex]);
}
private void TryToAddComponent() {
if (this.abilitiesComponent == null) {
string oldName = this.localAbility.name;
this.localAbility = EditorSettings.CreateCustomAbility(this.localAbility.type);
this.localAbility.name = oldName;
this.abilitiesComponent = this.localAbility as IAbilityComponentable;
if (this.abilitiesComponent != null) TryToAddComponent();
} else {
AbilityComponent com = AbilitySettings.CreateComponentForTarget(this.localComponentType, "New component");
this.abilitiesComponent.AddComponent(com);
}
}
private void TryToSave() {
}
private void DrawMainStartGUI() {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 15);
r.height += 25;
GUI.Box(r, "Create new ability");
GUILayout.BeginHorizontal();
if (GUILayout.Button("Passive")) this.localAbility = EditorSettings.CreateAbility(AbilitySettings.AbilityType.Passive);
if (GUILayout.Button("Active")) this.localAbility = EditorSettings.CreateAbility(AbilitySettings.AbilityType.Active);
GUILayout.EndHorizontal();
}
private void DrawControlState() {
Rect r = GUILayoutUtility.GetRect(EditorSettings.WindowSize.x, 2.5f);
r.height += 27.5f;
GUI.Box(r, "");
GUILayout.BeginHorizontal();
if (GUILayout.Button("Save: " + this.localAbility.name)) TryToSave();
if (GUILayout.Button("Delete")) {}
GUILayout.EndHorizontal();
}
public bool isEditor {
get { return Application.isPlaying == false;}
}
}
#endregion
}
#endif
Заключение: теперь у вас есть редактор способностей, можно поиграться с ним по добавлять разных способностей, но пока еще рано
говорить о Save\Load системе и скриптовых объектах, поэтому следующая часть будет посвящена другому объекту а именно SpellManager который и будет управлять способностями персонажа,
контроллировать их и тд но уже в рантайме.
Следующая часть
автор: этот хрен, он же llka, он же lawsonilka