Универсальная система способностей(спеллов) часть XIV

Научился сам? Помоги начинающему.

Универсальная система способностей(спеллов) часть XIV

Сообщение lawsonilka 23 янв 2015, 01:54

Создание системы универсальных способностей(спеллов) на unity3D Часть XIV

Сохранение компонентов

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

Как вы видели из прошлой части работать со скриптовым объектом проще простого, достаточно было наследовать базовый класс AbilityObject
способности и дальше можно было сохранять их как ассеты в папке.
С компонентами мы поступим также. При чем это будет еще проще чем со способностями т к база у нас уже описана.
И сражу перехожу к программной части.

Для начала также унаследуем абстрактный базовый класс AbilityComponent от ScriptableObject
Синтаксис:
Используется csharp
public abstract class AbilityComponent : ScriptableObject {

}

Скрытый текст:
Я привел только заголовок класса AbilityComponent без содержимого т к его наследование касаться не будет

Готово, теперь мы может также сохранять компоненты как ассеты.

Теперь переходим в статический класс EditorSettings где создали методы для сохранения способностей.
pic33.png

Как видите на картинке, папка со способностью еще имеет папку Components где храняться компоненты способности в виде скриптовых
объектов.

Давайте в статическом методе CreateAbilityFolders также создадим в папке со способностью папку Components для компонентов.
Синтаксис:
Используется csharp
public static void CreateAbilityFolders(string abilityName) {
 string path = Application.dataPath + "/" + SPELL_ASSET_FOLDER;
 if (Directory.Exists(path) == false) Directory.CreateDirectory(path);
 path += abilityName + "/";
 if (Directory.Exists(path) == false) Directory.CreateDirectory(path);
 path += AbilitySettings.COMPONENT_FOLDER_NAME;
 if (Directory.Exists(path) == false) Directory.CreateDirectory(path);
 AssetDatabase.Refresh();
}

Перед последней проверкой наличия папки Components, я прибавляю к полному пути способности также имя папки Components которая записана в строковой константе COMPONENT_FOLDER_NAME статического класса AbilitySettings
Синтаксис:
Используется csharp
public static class AbilitySettings {
 
 public const string COMPONENT_FOLDER_NAME = "Components/";
 public const string SPELL_RESOURCES_FOLDER = "UserAbilities/";

}

Вместе со строковой константой SPELL_RESOURCES_FOLDER. Я объявил эти две константы в статическом классе AbilitySettings т к статический
класс EditorSettings находится в области редактора и в рантайме будет не доступен, именно поэтому константу SPELL_ASSET_FOLDER я объявил
в этом классе т к класс AssetDataBase и все его методы доступны только в режиме редактора.

И так теперь папка Components также будет создаваться в папке со способностью. Теперь нужно эту папку наполнить компонентами самой
способности. Остаемся в классе EditorSettings и переходим к методу SaveProgressive где мы уже сохраняем способность.
Синтаксис:
Используется csharp
private static IEnumerable<progress> SaveProgressive(AbilityObject ability) {
 ScriptableObject data = AbilityObject.CreateInstance(ability.GetType());
 yield return new progress(.1f, "Creating instance");

 CreateAbilityFolders(ability.name);
 yield return new progress(.2f, "Creating folders");

 IAbilityComponentable compSys = ability as IAbilityComponentable;
 if (compSys != null) {
  yield return new progress(.8f, "Saving components");
 }

 data = ability;
 data.name = ability.name;
 yield return new progress(.9f, "Updating data");

 string path = "Assets/" + SPELL_ASSET_FOLDER + data.name + "/" + data.name + ".asset";
 AssetDatabase.CreateAsset(data, path);
 AssetDatabase.SaveAssets();  
 yield return new progress(1, "Complete");
}

Для начала я как всегда должен проверить: может ли способность вообще содержать в себе компоненты с помощью интерфейса
IAbilityComponentable и если это так то перехожу к процессу сохранения их(компонентов) в ассеты - ни чего сложного - обычный цикл.
Синтаксис:
Используется csharp
private static IEnumerable<progress> SaveProgressive(AbilityObject ability) {
 ScriptableObject data = AbilityObject.CreateInstance(ability.GetType());
 yield return new progress(.1f, "Creating instance");

 CreateAbilityFolders(ability.name);
 yield return new progress(.2f, "Creating folders");

 IAbilityComponentable compSys = ability as IAbilityComponentable;
 if (compSys != null) {
  float time = .6f / compSys.components.Length;
  AbilityComponent[] components = compSys.components;
  for(int index = 0; index < components.Length; index++) {
   AbilityComponent component = components[index];
   yield return new progress(.2f + index * time, "Updating components");
  }
  yield return new progress(.8f, "Saving components");
 }

 data = ability;
 data.name = ability.name;
 yield return new progress(.9f, "Updating data");

 string path = "Assets/" + SPELL_ASSET_FOLDER + data.name + "/" + data.name + ".asset";
 AssetDatabase.CreateAsset(data, path);
 AssetDatabase.SaveAssets();  
 yield return new progress(1, "Complete");
}

В первом действии я объявляю числовую переменную time которая будет показывать в прогресс баре процент выполнения действий.
В следующем действии я объявляю массив компонентов и заполняю его елементами массива способности.
Дальше перехожу к циклу и начинаю переберать один компонент за другим.
Синтаксис:
Используется csharp
for(int index = 0; index < components.Length; index++) {
 AbilityComponent component = components[index];
 yield return new progress(.2f + index * time, "Updating components");
 if (component.targetValue == AbilitySettings.ComponentValueTarget.Special) continue;
 ScriptableObject sData = AbilityComponent.CreateInstance(component.GetType());
 sData = component;
 sData.name = component.name;
}

После yield инструкции я проверяю, если "цель" компонента Special то я не буду сохранять этот компонент. Ниже объясню почему.
Дальше я как и со способность - создаю физическую оболочку компонента через метод CreateInstance.
Скрытый текст:
Данные компонента находятся в виртуальной форме, а созданный скриптовый объект(sData) компонента - в физической.
Передав теперь значению физического компонента значения вирутального я получаю получаю физический объект с виртуальными данными.

Теперь когда у меня есть компонент в физической форме перехожу к стадии сохранения как и в случае со способностью.
Синтаксис:
Используется csharp
for(int index = 0; index < components.Length; index++) {
 AbilityComponent component = components[index];
 yield return new progress(.2f + index * time, "Updating components");
 if (component.targetValue == AbilitySettings.ComponentValueTarget.Special) continue;
 ScriptableObject sData = AbilityComponent.CreateInstance(component.GetType());
 sData = component;
 sData.name = component.name;
 string p = "Assets/" + SPELL_ASSET_FOLDER + ability.name + "/";
 p += AbilitySettings.COMPONENT_FOLDER_NAME + component.targetValue.ToString() + ".asset";
 p = AssetDatabase.GenerateUniqueAssetPath(p);
 yield return new progress(.2f + index * time + time / 2f, "Save component");
 AssetDatabase.CreateAsset(sData, p);
 AssetDatabase.SaveAssets();    
}

Здесь я с помощью метода GenerateUniqueAssetPath класса AssetDataBase генерирую не просто путь к ассету, а его уникальное название.
pic34.png

На картинке изображены два компонента с одной "целью", НО с разными именами. Вот для чего мне нужнен уникальное название двух похожих
компонентов. Ведь как я много раз утверждал что способность может иметь сколь угодно одинаковых компонентов, но т к редакторе не может
иметь два и более ассета с одинаковыми именами - приходится давать им уникальные имена при сохранении.

Так ну дальше после yield инструкции идут нам уже знакомые действия - создания ассета в папке Components их его сохранение.
Вот и все с частью сохранения справились довольно быстро.
Весь код метода SaveProgressive
Синтаксис:
Используется csharp
private static IEnumerable<progress> SaveProgressive(AbilityObject ability) {
 ScriptableObject data = AbilityObject.CreateInstance(ability.GetType());
 yield return new progress(.1f, "Creating instance");

 CreateAbilityFolders(ability.name);
 yield return new progress(.2f, "Creating folders");

 IAbilityComponentable compSys = ability as IAbilityComponentable;
 if (compSys != null) {
  float time = .6f / compSys.components.Length;
  AbilityComponent[] components = compSys.components;
  for(int index = 0; index < components.Length; index++) {
   AbilityComponent component = components[index];
   yield return new progress(.2f + index * time, "Updating components");
   if (component.targetValue == AbilitySettings.ComponentValueTarget.Special) continue;
   ScriptableObject sData = AbilityComponent.CreateInstance(component.GetType());
   sData = component;
   sData.name = component.name;
   string p = "Assets/" + SPELL_ASSET_FOLDER + ability.name + "/";
   p += AbilitySettings.COMPONENT_FOLDER_NAME + component.targetValue.ToString() + ".asset";
   p = AssetDatabase.GenerateUniqueAssetPath(p);
   yield return new progress(.2f + index * time + time / 2f, "Save component");
   AssetDatabase.CreateAsset(sData, p);
   AssetDatabase.SaveAssets();    
  }
  yield return new progress(.8f, "Saving components");
 }

 data = ability;
 data.name = ability.name;
 yield return new progress(.9f, "Updating data");

 string path = "Assets/" + SPELL_ASSET_FOLDER + data.name + "/" + data.name + ".asset";
 AssetDatabase.CreateAsset(data, path);
 AssetDatabase.SaveAssets();  
 yield return new progress(1, "Complete");
}

А теперь к вопросу почему я не стал сохранять компонент с целью Special.
Дело в том что этот компонент, если вы помните, я унаследовал от класс AbilityComponentValue со значением UnityEngine.Object.
Сложность в том что этот компонент может нести любой объект который наследуется от UnityEngine.Object НО сам объект UnityEngine.Object не имеет редактора и если вы попробуете написать для него что то вроде этого
Синтаксис:
Используется csharp
UnityEnigne.Object obj = null;
obj = EditorGUILayout("Value: ", obj, typeof(UnityEngine.Object), false);

То у вас повиснет редактор, так что лучше учитесь на чужих ошибках. :)

И так можно сказать что с сохранением покончено!
Теперь расскажу о преимуществах сохранения компонентов в виде скриптовых объектов и почему они хранятся в папке Components.
pic35.png

По изображени становится понятно, что главное преимущество - это доступ самой способности к своим компонентам.
pic36.png

А по этому изображение также видно как, добавляя или удаляя компоненты из папки Components, можно влиять на то какие компоненты может
содержать способность.
К примеру: вы создали способность которая добавляет объекту здоровье, вот вы ее сохранили, но вдруг захотели добавить к ней эффект.
Обратно загружать способность в редактор способностей тяжело, поэтому вы просто добавляете компонент эффекта в папку Components и вот
ваша способность уже имеет компонент с целью "эффект" и уже не только добавляет здоровье, но и делает это эффектно. Вот почему скриптовые
объекты так хорошо вписались в систему способностей - потому что ими проще некуда управлять, захотел удалил компоненты из способности,
захотел добавил, без надобности в создании новой способности!

Но чтобы способность могла загружать и добавлять\удалять в\из себя компоненты нужно сначала написать этот функционал.
Поэтому, как я писал в прошлой части, переходим к редакторам скриптовых объектов и создадим каждый редактор для каждого типа
способности, всего их будет четыре.
Скрытый текст:
Этим действием мы "убьем двух зайцев сразу" т к в отличие от компонентов - способности НЕ редактируемы и поэтому мы, с помощью
редактора, урежим некоторый функционал скриптовых объектов способностей.

Если вы сейчас посмотрите на объект способности который сохранили то вы получите примерно такой результат
pic37.png

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


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

Этот базовый класс я назвал AbilityEditor и сделал его абстрактным, хотя он и не будет содержать ни одного абстрактного метода и
свойства, и унаследую этот редактор от класса Editor т к в отличии от редактора способностей, который наследуется от EditorWindow и
отрисовывается в окошке, это редактор будет отрисовываться в инспекторе.
Синтаксис:
Используется csharp
public abstract class AbilityEditor : UnityEditor.Editor {

}

Пока я добавлю в него базовые методы каждого редактора это:
OnEnable - вызывается при активации(выделении) объекта
OnDisable - вызывается при деактивации объекта(снятии выделения)
OnInspectorGUI - вызывается при отрисовке полей в области инспектора.
Синтаксис:
Используется csharp
public abstract class AbilityEditor : UnityEditor.Editor {
 
 protected virtual void OnEnable() {

 }

 protected virtual void OnDisable()

 }

 public override void OnInspectorGUI() {

 }

}

Первые два метода я сделал виртуальными чтобы каждый наследующийся класс мог их перегрузить.
Но метод OnInspectorGUI уже является виртуальным и я его просто перегружаю. Этот метод и рисует свойства всех компонентов и объектов в
инспекторе.

Теперь я добавлю для начала одно защищенное свойство.
ability - будет представлять собой редактируемую способность.
Синтаксис:
Используется csharp
public abstract class AbilityEditor : UnityEditor.Editor {

 protected AbilityObject ability = null;
 
 protected virtual void OnEnable() {
  this.ability = target as AbilityObject;
 }

 protected virtual void OnDisable()
  EditorUtility.SetDirty(this.ability);
 }

 public override void OnInspectorGUI() {

 }

}

При активации редактора в метод OnEnable я передаю свойству - ссылку на редактируемый объект в виде базового класса способности.
А при деактивации я передаю в метод EditorUtility.SetDirty редактируемый объект. Этот метод помечает редактируемый объект неким флагом
который дает понять редактору unity что этот объект недавно был отредактирован и его нужно обновить в каталоге объектов(где содержаться
все файлы проекта).
Ну а в методе OnInspectorGUI мы будем отображать свойства способности и только те которые будут не так критично восприимчивы к
изменениям.
Синтаксис:
Используется csharp
public abstract class AbilityEditor : UnityEditor.Editor {

 protected AbilityObject ability = null;
 
 protected virtual void OnEnable() {
  this.ability = target as AbilityObject;
 }

 protected virtual void OnDisable()
  EditorUtility.SetDirty(this.ability);
 }

 public override void OnInspectorGUI() {
  if (ability == null) return;
  EditorGUILayout.LabelField("Name: ", this.ability.name);
  EditorGUILayout.LabelField("Type: ", this.ability.type.ToString());
  GUILayout.BeginHorizontal();
   GUILayout.Label("Icon: ");
   this.icon = (Texture2D)EditorGUILayout.ObjectField(this.ability.icon, typeof(Texture2D), false);
  GUILayout.EndHorizontal();
  this.abilityDescription = GUILayout.TextArea(this.ability.abilityDescription);
 }

}

По сути в методе OnInspectorGUI мы делаем тоже самое что и в классе SpellWindow - редактора способностей, только теперь некоторые свойства мы скроем от пользователя. Сейчас мы лишь обозначили базовые свойства каждой способности, теперь нужно обозначить дополнительные свойства АКТИВНОЙ способности.
Синтаксис:
Используется csharp
public abstract class AbilityEditor : UnityEditor.Editor {

 protected AbilityObject ability = null;
 
 protected virtual void OnEnable() {
  this.ability = target as AbilityObject;
 }

 protected virtual void OnDisable()
  EditorUtility.SetDirty(this.ability);
 }

 public override void OnInspectorGUI() {
  if (ability == null) return;
  EditorGUILayout.LabelField("Name: ", this.ability.name);
  EditorGUILayout.LabelField("Type: ", this.ability.type.ToString());
  GUILayout.BeginHorizontal();
   GUILayout.Label("Icon: ");
   this.icon = (Texture2D)EditorGUILayout.ObjectField(this.ability.icon, typeof(Texture2D), false);
  GUILayout.EndHorizontal();
  this.abilityDescription = GUILayout.TextArea(this.ability.abilityDescription);
  if (this.ability is ActiveAbility) {
   ActiveAbility aAbility = this.ability as ActiveAbility;
   this.castKey = (KeyCode)EditorGUILayout.EnumPopup("Cast key: ", this.aAbility.castKey);
   this.cooldown = EditorGUILayout.FloatField("Cooldown: ", this.aAbility.cooldown);
  }
  EditorGUILayout.HelpBox("You can add or remove components from 'Components' folder.", MessageType.Info);
 }

}

Готово. Теперь когда написан базовый класс редактора, создадим четыре отдельных редактора для каждого типа способности и просто
перегрузим виртуальные методы.
Вот как это выглядит у меня.
Синтаксис:
Используется csharp
[CustomEditor(typeof(PassiveAbility))]
public sealed class PassiveEditor : AbilityEditor {
 
 protected override void OnEnable() {
  base.OnEnable();
 }
 
 protected override void OnDisable() {
  base.OnDisable();
 }
               
 public override void OnInspectorGUI()  {
  base.OnInspectorGUI();
 }
 
}
 
[CustomEditor(typeof(ActiveAbility))]
public sealed class ActiveEditor : AbilityEditor {
               
 protected override void OnEnable() {
  base.OnEnable();
 }
 
 protected override void OnDisable() {
  base.OnDisable();
 }
               
 public override void OnInspectorGUI() {
  base.OnInspectorGUI();
 }
               
}
       
[CustomEditor(typeof(PassiveCustomAbility))]
public sealed class PassiveCustomEditor : AbilityEditor {
               
 protected override void OnEnable() {
  base.OnEnable();
 }
 
 protected override void OnDisable() {
  base.OnDisable();
 }
               
 public override void OnInspectorGUI()  {
  base.OnInspectorGUI();
 }
               
}
       
[CustomEditor(typeof(ActiveCustomAbility))]
public sealed class ActiveCustomEditor : AbilityEditor {
               
 protected override void OnEnable() {
  base.OnEnable();
 }
 
 protected override void OnDisable() {
  base.OnDisable();
 }
               
 public override void OnInspectorGUI()  {
  base.OnInspectorGUI();
 }
               
}

Готово. Примерно тоже самое что мы делали и с ШАБЛОННЫМИ компонентами.
Теперь каждая способности имеет вроде как общий редактор, но только лично для себя.
Вы можете увидеть результат этой работы, просто создав любую способность и получить результат примерно как на картинках выше.

Теперь займемся только загрузкой компонентов из папки Components, будем работать в классе AbilityEditor.
Создадим там пару свойств и один метод.
Синтаксис:
Используется csharp
public abstract class AbilityEditor : UnityEditor.Editor {

 protected AbilityObject ability = null;

 private List<string> components = null;
 private bool loading = false;
 
 private void LoadComponents() {

 }
 
 protected virtual void OnEnable() {
  this.ability = target as AbilityObject;
 }

 protected virtual void OnDisable()
  EditorUtility.SetDirty(this.ability);
 }

 public override void OnInspectorGUI() {
  if (ability == null) return;
  EditorGUILayout.LabelField("Name: ", this.ability.name);
  EditorGUILayout.LabelField("Type: ", this.ability.type.ToString());
  GUILayout.BeginHorizontal();
   GUILayout.Label("Icon: ");
   this.icon = (Texture2D)EditorGUILayout.ObjectField(this.ability.icon, typeof(Texture2D), false);
  GUILayout.EndHorizontal();
  this.abilityDescription = GUILayout.TextArea(this.ability.abilityDescription);
  if (this.ability is ActiveAbility) {
   ActiveAbility aAbility = this.ability as ActiveAbility;
   this.castKey = (KeyCode)EditorGUILayout.EnumPopup("Cast key: ", this.aAbility.castKey);
   this.cooldown = EditorGUILayout.FloatField("Cooldown: ", this.aAbility.cooldown);
  }
  EditorGUILayout.HelpBox("You can add or remove components from 'Components' folder.", MessageType.Info);
 }

}

components - будет содержать имена компонентов что содержит способность. Имя - потому что не зачем держать всю ссылку на компонент.
loading - будет в роли флага в момент загрузки компонентов.
И переходим в метод LoadComponents, для начала я установлю в начале и конце значения флага loading чтобы показать когда началась
обработка компонентов и когда закончилась.
Синтаксис:
Используется csharp
private void LoadComponents() {
 this.loading = true;

 this.loading = false;
}

Проверку на эти флаги я поместил в метод OnInspectorGUI
Синтаксис:
Используется csharp
public override void OnInspectorGUI() {
 if (ability == null || this.loading) return;
 EditorGUILayout.LabelField("Name: ", this.ability.name);
 EditorGUILayout.LabelField("Type: ", this.ability.type.ToString());
 GUILayout.BeginHorizontal();
  GUILayout.Label("Icon: ");
  this.icon = (Texture2D)EditorGUILayout.ObjectField(this.ability.icon, typeof(Texture2D), false);
 GUILayout.EndHorizontal();
 this.abilityDescription = GUILayout.TextArea(this.ability.abilityDescription);
 if (this.ability is ActiveAbility) {
  ActiveAbility aAbility = this.ability as ActiveAbility;
  this.castKey = (KeyCode)EditorGUILayout.EnumPopup("Cast key: ", this.aAbility.castKey);
  this.cooldown = EditorGUILayout.FloatField("Cooldown: ", this.aAbility.cooldown);
 }
 EditorGUILayout.HelpBox("You can add or remove components from 'Components' folder.", MessageType.Info);
}

Это условие будет НЕ давать рисовать редактор пока мы загружаем или обрабатываем компоненты.
Теперь обратно в метод LoadComponents.
Синтаксис:
Используется csharp
private void LoadComponents() {
 this.loading = true;
 this.components = new List<string>();
 IAbilityComponentable comSys = this.ability as IAbilityComponentable;
 if (comSys != null) {

 }
 this.loading = false;
}

Для начала проверка: может ли вообще способность содержать в себе компоненты, как всегда.
Синтаксис:
Используется csharp
private void LoadComponents() {
 this.loading = true;
 this.components = new List<string>();
 IAbilityComponentable comSys = this.ability as IAbilityComponentable;
 if (comSys != null) {
  string path = AbilitySettings.SPELL_RESOURCES_FOLDER + this.ability.name + "/" + AbilitySettings.COMPONENT_FOLDER_NAME;
  UnityEngine.Object[] objects = Resources.LoadAll(path, typeof(AbilityComponent));
 }
 this.loading = false;
}

Дальше я создаю весь пусть к папке Components и загружаю объекты что в ней находятся, только те которые являются компонентами - тоесть
наследуются от класса AbilityComponent и делаю это через метод Resources.LoadAll т к компоненты находятся в папке Resources.
Синтаксис:
Используется csharp
private void LoadComponents() {
 this.loading = true;
 IAbilityComponentable comSys = this.ability as IAbilityComponentable;
 if (comSys != null) {
  string path = AbilitySettings.SPELL_RESOURCES_FOLDER + this.ability.name + "/" + AbilitySettings.COMPONENT_FOLDER_NAME;
  UnityEngine.Object[] objects = Resources.LoadAll(path, typeof(AbilityComponent));
  foreach(AbilityComponent s in comSys.components) {
   comSys.RemoveComponent(s);
  }
 }
 this.loading = false;
}

Дальше я УДАЛЯЮ все компоненты которые хранит в себе способность. Это важный шаг и чтобы понять для чего я его делаю нам нужно вернуться
далеко назад когда мы создавали способности наследующиеся от интерфейса IAbilityComponentable - к классам ActiveCustomAbility и
PassiveCustomAbility.

Как вы знаете для того чтобы сериализовать какой то объект можно просто добавить к этому объекту аттрибут Serializable, и тогда все
свойства объекта буду сохранять свои значения, включая массивы и списки.
К нашим способностям не нужно присваивать этот аттрибут т к они наследуются от класса ScriptableObject и уже могут быть сериализованы в
редакторе unity. Теперь обратим внимание на пару свойств этих классов а именно на
abilityComponents - список который содержит компоненты способности.
abilityArrayComponents - массив который также содержит компоненты способности.
Синтаксис:
Используется csharp
public class ActiveCustomAbility : ActiveAbility, IAbilityComponentable {

 private List<AbilityComponent> abilityComponents = new List<AbilityComponent>();
 private AbilityComponent[] abilityArrayComponents;

}

Эти два свойства примерно выполняют одну и ту же роль только отличаются доступами и приоритетами.
Скрытый текст:
Я часто использую связку список и массив, т к не люблю когда внешний пользователь получает доступ к списку и может его изменять
через методы Add\Remove\Clear и тд, поэтому при каждом изменении списка я обновляю и массив т к он всегда определенной длины и не имет
конкретных методов управления.

Дело в том что способность полностью сериализуема, и получается что и список abilityComponents и массив abilityArrayComponents
сериализуются(сохранят свои значения).
Я подумал, зачем мне два по сути массива с одними и теми же компонентами и тогда я решил избавиться от одного из них.
Кого же выбрать, кого наградить аттрибутом NonSerialized(не сериализуемый).

Массив? - хорошо, он имеет точное кол-во компонентов и с внешним доступом через свойство components интерфейса IAbilityComponentable,
также легко сериализуемый при частом использовании. Но он зависит от списка, т к только через методы AddComponent или RemoveComponent массив может получить новые значения. Ладно.

Список? - его можно наполнять или стирать в нем значения, именно он принимает первые изменения через методы AddCompoent и
RemoveComponent, также его можно будет и после сериализации динамически изменять. Но у него нет внешнего доступа в отличие от массива и
тогда мне придется както обеспечить доступ к нему добавлением еще одного лишнего метода в интерфейс IAbilityComponentable.

И т к список все же имеет более высший приоритет по сравнению с массивом я решил оставить его в способности, а массив обозначить
NonSerialized свойством. Но тогда если я обращусь к свойству components интерфейса IAbilityComponentable я получу null даже если способность и будет иметь компоненты в себе. Тогда мне пришлось добавить в свойство components пару действий.
Синтаксис:
Используется csharp
public class ActiveCustomAbility : ActiveAbility, IAbilityComponentable {

 private List<AbilityComponent> abilityComponents = new List<AbilityComponent>();
 [NonSerialized]
 private AbilityComponent[] abilityArrayComponents;

 public AbilityComponent[] components {
  get {
   if (this.abilityArrayComponents == null) this.abilityArrayComponents = this.abilityComponents.ToArray();
   return this.abilityArrayComponents;
  }
 }

}

Я в свойстве components делаю одну проверку, если массив "пустой"(не так что его длина равна нулю), тоесть null, я беру список и
заполняю массив его елементами через метод ToArray. Нужно проделать тоже самое и с классом PassiveCustomAbility.

Теперь на счет удаления. Когда мы будем грузить компоненты из папки Components мы будем заново заполнять способность компонентами из
этой папки, и получается т к способность уже будет иметь компоненты и своем списке abilityComponents при загрузке мы можем добавить те
же самые компоненты что и были прежде и тогда они просто будут дублироваться, поэтому для начала нужно полностью освободить способность
от компонентов и только потом заново загрузить. Что мы и сделаем в методе LoadComponents.
Скрытый текст:
Проверка на нахождение уже добавленного компонента не подойдет т к способность может иметь сколько угодно одинаковый компонентов

После удаления компонентов в методе LoadComponents мы загружаем с помощью цикла новые компоненты в способность из папки Components.
Синтаксис:
Используется csharp
private void LoadComponents() {
 this.loading = true;
 this.components = new List<string>();
 IAbilityComponentable comSys = this.ability as IAbilityComponentable;
 if (comSys != null) {
  string path = AbilitySettings.SPELL_RESOURCES_FOLDER + this.ability.name + "/" + AbilitySettings.COMPONENT_FOLDER_NAME;
  UnityEngine.Object[] objects = Resources.LoadAll(path, typeof(AbilityComponent));
  foreach(AbilityComponent s in comSys.components) {
   comSys.RemoveComponent(s);
  }
  for(int index = 0; index < objects.Length; index++) {
   AbilityComponent component = objects[index] as AbilityComponent;
   if (component == null) continue;
   component = comSys.AddComponent(component);
   if (component != null) this.components.Add(component.name);
  }
 }
 this.loading = false;
}

В цикле я перебираю каждый компонент в папке и проверяю точно ли это компонент и если да то добавляю его в способность с помощью метода
AddComponent.

Т к способность не все компоненты может в себя добавить(я о тех что помечены аттрибутом DisableComponentMultiplyUsage), я также не могу
добавить в список редактора все имена этих компонентов.
Метод AddComponent после добавления может вернуть значение добавленного компонента, если он смог его добавить то вернет добавленный компонент если не смог то вернет значение null, это по аналогии с стандартным методом AddComponent в unity позволяет понять успешно ли был добавлен компонент или нет, также и здесь - только в случае если способность смогла добавить компонент в себя я могу также добавить и имя этого компонента в список редактора, что я и сделал.

Ну вот и все, с загрузкой мы разобрались.

Теперь нужно как то отобразить наш список компонентов.
Я добавил в класс AbilityEditor метод DrawComponents который будет рисовать имена компонентов.
Синтаксис:
Используется csharp
private void DrawComponents() {
 foreach(string component in this.components) {
  Rect r = GUILayoutUtility.GetRect(100, 20);
  GUI.Box(r, component);
  GUILayout.Space(5);
 }
}

Все просто. Нужно только при активации вызвать метод LoadComponents для загрузки компонентов.
Синтаксис:
Используется csharp
public abstract class AbilityEditor : UnityEditor.Editor {

 protected AbilityObject ability = null;

 private List<string> components = null;
 private bool loading = false;
 
 protected virtual void OnEnable() {
  this.ability = target as AbilityObject;
  LoadComponents();
 }

 protected virtual void OnDisable()
  EditorUtility.SetDirty(this.ability);
 }

 public override void OnInspectorGUI() {
  if (ability == null || this.loading) return;
  EditorGUILayout.LabelField("Name: ", this.ability.name);
  EditorGUILayout.LabelField("Type: ", this.ability.type.ToString());
  GUILayout.BeginHorizontal();
   GUILayout.Label("Icon: ");
   this.icon = (Texture2D)EditorGUILayout.ObjectField(this.ability.icon, typeof(Texture2D), false);
  GUILayout.EndHorizontal();
  this.abilityDescription = GUILayout.TextArea(this.ability.abilityDescription);
  if (this.ability is ActiveAbility) {
   ActiveAbility aAbility = this.ability as ActiveAbility;
   this.castKey = (KeyCode)EditorGUILayout.EnumPopup("Cast key: ", this.aAbility.castKey);
   this.cooldown = EditorGUILayout.FloatField("Cooldown: ", this.aAbility.cooldown);
  }
  EditorGUILayout.HelpBox("You can add or remove components from 'Components' folder.", MessageType.Info);
  GUI.Space(10);
  DrawComponents();
 }

 private void DrawComponents() {
  foreach(string component in this.components) {
   Rect r = GUILayoutUtility.GetRect(100, 20);
   GUI.Box(r, component);
   GUILayout.Space(5);
  }
 }

 private void LoadComponents() {
  this.loading = true;
  this.components = new List<string>();
  IAbilityComponentable comSys = this.ability as IAbilityComponentable;
  if (comSys != null) {
   string path = AbilitySettings.SPELL_RESOURCES_FOLDER + this.ability.name + "/" + AbilitySettings.COMPONENT_FOLDER_NAME;
   UnityEngine.Object[] objects = Resources.LoadAll(path, typeof(AbilityComponent));
   foreach(AbilityComponent s in comSys.components) {
    comSys.RemoveComponent(s);
   }
   for(int index = 0; index < objects.Length; index++) {
    AbilityComponent component = objects[index] as AbilityComponent;
    if (component == null) continue;
    component = comSys.AddComponent(component);
    if (component != null) this.components.Add(component.name);
   }
  }
  this.loading = false;
 }

}

Готово. Теперь можно спокойно добавлять и удалять компоненты из способности не редактируя ее, как на изображениях выше.
pic35.png


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

Следующая часть

автор: этот хрен, он же llka, он же lawsonilka
У вас нет доступа для просмотра вложений в этом сообщении.
lawsonilka
UNIверсал
 
Сообщения: 390
Зарегистрирован: 21 окт 2014, 14:48

Вернуться в Уроки

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 2