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

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

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

Сообщение lawsonilka 11 янв 2015, 17:51

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

Особые компоненты. Суть универсальности.

В прошлой части я закончил на том как избавляться от эффектов которые накладывают компоненты на свойства объектов.
Делали мы это с помощью корутины FinalizeAbility. Этот метод в нашем случае брал способность и как в методе InitializeAbility
раскладывал ее на компоненты и отсылал каждый компонент в класс AbilityBihaviour где пользователь сам решал как применять свойства
полученного компонента.

Сам же пользователь не мог решить нужно ли ему вообще финализировать тот или иной компонент это решает менеджер способностей.
Менеджеру же в свою очередь нет разницы в том что за компонент он получит из способности и отправит объекту. Решение это
принимает, как я сказал, пользователь который определяет это с помощью "цели" компонента.
К чему я это все веду...
Если вы смотрели видео на которое я дал ссылку в конце прошлой части, то там можно заметить - когда я создаю компонент я указываю
одно bool'евое свойство о котором вам еще не рассказывал, т к стараюсь идти последовательно, стараясь не создать путаницы у вас в
головах. Это свойство supportRexpansion, похоже на название метода OnComponentRexpansion абстрактного класса AbilityBihaviour.

Сейчас я попробую вам объяснить что это за свойство и зачем оно нам.

Давайте мысленно создадим АКТИВНУЮ способность которая будет при нажатии увеличивать наше здоровье на 100 ед.
Мы добавляем эту способность в менеджер и активируем ее(нажатием на кнопку клавиатуры, а не в методе AddAbility как в случае с
пассивной способностью), активация происходит через метод инициализации и мы прибавляем 100 ед здоровья к нашему текущему уровню.
Все работает как надо. Но давайте теперь попробуем ЕЩЕ раз активировать эту АКТИВНУЮ способность, и здесь уже, как я говорил в
прошлой части, перед тем как ИНИЦИАЛИЗИРОВАТЬ уже ИНИЦИАЛИЗИРОВАННУЮ способность ее сначала придется ФИНАЛИЗИРОВАТЬ - тоесть снять все предыдущие эффекты.
Что мы получаем - наши 100 ед здоровья снимутся в методе финализации и мы опять вернемся к тому уровню
здоровья что был у объекта до активации способности. Дальше мы опять инициализируем способность и опять прибавляем 100 ед
здоровья. И что же мы получаем в итоге!? - уровень нашего здоровья не увеличиться опять - сколько бы раз мы не использовали
способность, значение уровня здоровья будет просто "прыгать" туда сюда, то +100 ед, то обратно -100 ед. Тоесть имея допустим 1000
ед здоровья, мы активируем способность и получаем 1100 ед здоровья, активируем еще раз и также получаем 1100 ед здоровья, опять
активируем и опять 1100 ед.

Это работает в случае с пассивными способностями, но с активными такое положение дел меня не устраивало.
Дальше мне нужно было решить одну задачу: как объяснить менеджеру, должен ли он финализировать тот или иной компонент у объекта.

Для начала давайте заглянем в класс AbilitySettings в перечисление ComponentValueTarget - "целей" компонентов.
Вот как он выглядит у меня
Синтаксис:
Используется csharp
public enum ComponentValueTarget : byte {
 [ComponentValueType(typeof(AbilityObjectComponent))] Special = 0,
 [ComponentValueType(typeof(AbilityFloatComponent))]  AddHealth = 1,
 [ComponentValueType(typeof(AbilityFloatComponent))]  AddArmor = 2,
 [ComponentValueType(typeof(AbilityFloatComponent))]  MoveSpeed = 3,
 [ComponentValueType(typeof(AbilityFloatComponent))]  AddMana = 4,
 [ComponentValueType(typeof(AbilityTimeComponent))]   Time = 5
}

Немного замечу что я унаследовал это перечисление от byte и расставил номерки каждой "цели". Сути работы это абсолютно не меняет,
просто чтобы в следующий раз мне не писать полностью всю ссылку "цели" к примеру AbilitySettings.ComponentValueTarget.Special я
просто мог установить ее равной "0" - байтовой ссылкой заместо полной текстовой.
Синтаксис:
Используется csharp
byte target = 0;
AbilitySettings.ComponentValueTarget newTarget = (AbilitySettings.ComponentValueTarget)target;
//И ОБРАТНО
AbilitySettings.ComponentValueTarget target = AbilitySettings.ComponentValueTarget.Special;
byte newTarget = (byte)target;

Это действие называется "эксплисированием", надеюсь что перевел правильно, вы можете почитать об этом на мсдн ссылка.
Скрытый текст:
Хотя "экплисирование" это связь между несовместимыми объектами, ну да ладно. Думаю более опытные программисты меня исправят.


Так вот. Каждая "цель" компонента может трактоватся от того как сам пользователь это решит.
Допустим пусть "цель" AddHealth будет и ИНИЦИАЛИЗИРОВАТЬСЯ и ФИНЛИЗИРОВАТСЬЯ тоесть будет и давать значение и отбирать его каждый раз. Тогда в этом случае чтобы создать способность АКТИВНУЮ ту что мы разбирали выше, которая каждый раз ТОЛЬКО дает значение, нам бы пришлось создать отдельную "цель" и назвать ее допустим AddHealthOneWay - что переводилась бы как "добавить здоровье в одну
сторону", тоесть эта цель бы работала без учета финализации.

И этот способ вполне рабочий. Но я решил что каждый раз для каждого действия создавать отдельную "цель" было бы глупо с точки
зрения оптимизации - сколько бы "целей" сразу добавилось в перечисление тогда!

Тогда я решил, а что если просто добавить свойство в компонент, что то типа флага, который бы указывал финализируем компонент или
нет. Т к компонент ВСЕГДА инициализируем, то этот флаг ни каких логических ошибок создать не должен.
Так я пришел к заключению, что пользователь будет сам определять на стадии редактора, будет ли компонент, иметь ОБРАТНЫЙ эффект.

Поэтому я добавил одно bool свойство в абстрактный класс AbilityComponent и назвал его supportRexpansion.
Давайте так и сделаем.
Синтаксис:
Используется csharp
public abstract class AbilityComponent {
         
 public AbilitySettings.ComponentValueTarget targetValue { get; protected set;}
 public string name = "";
 public bool supportRexpansion = true;
         
 public virtual void DrawComponentBase() {
#if UNITY_EDITOR
  this.name = EditorGUILayout.TextField("Name: ", this.name);
  this.supportRexpansion = EditorGUILayout.Toggle("Allow to set back value", this.supportRexpansion);
#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 расписал новую область где пользователь в редакторе будет менять
свойство supportRexpansion. По умолчанию оно стоит на значении true, поэтому я в прошлом видео каждый раз, если мне это было
нужно, устанавливал это свойство в false.
Вы можете теперь протестировать редактор способностей и увидеть как теперь изменилось его окошко.
pic26.png

Как видно на картинке, теперь у компонента добавилось еще одно редактируемое свойство.

Теперь давайте вернемся в класс SpellManager и в методе финализации(FinalizeАbility) поставим проверку этого флага.
Синтаксис:
Используется csharp
private IEnumerator FinalizeAbility(AbilityData data) {
 data.isUsing = false;
 SendAbilityEvent(SpellManagerData.AbilitySendEvent.OnAbilityEndUse, data.ability);
 IAbilityComponentable compSys = data.ability as IAbilityComponentable;
 if (compSys == null) yield break;
 foreach(AbilityComponent component in compSys.components) {
  if (component.supportRexpansion)
   SendComponentEvent(SpellManagerData.ComponentSendEvent.OnComponentRexpansion, component);
  yield return new WaitForFixedUpdate();
 }
}

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

Вот теперь с частью ШАБЛОННЫХ компонентов мы и разобрались.

Особые компоненты.

С особыми компонентами дела обстоят немного сложнее. Ну наверное это очевидно, ведь они "особые".
Если с ШАБЛОННЫМИ компонентами пользователь лишь решает как и где использовать их значения, то ОСОБЫЕ компоненты также кроме
значения могут нести разную реализацию.
Честно скажу вам, за все время использования системы способностей, я использовал только один особый компонент - это компонент
времени AbilityTimeComponent. Но т к я не могу предсказать как вы обойдетесь с этими компонентами и сколько ИХ будет у вас, я
попробую рассказать вам как делал я на примере компонента времени AbilityTimeComponent.

Самое неприятное что было мне осознать в процессе создания этой системы способностей, что в погоне за УНИВЕРСАЛЬНОСТЬЮ, также
придется чемто жертвовать - и я понял что не все возможно будет автоматизировать.

И частью этой НЕ возможности стали именно те "особые" компоненты.
Как я сказал ранее их реализация заранее не известна, поэтому и менеджер получая этот "особый" компонент также попробует его
отослать объекту как другие остальные компоненты. Ну и пусть я так решил. Пусть опять же пользователь определяет его реализацию т
к хочет, и здесь я столкнулся с проблемой, если вся система практически закрыта от постороннего вмешательства, как тогда я смогу
этими "особыми" компонентами реализовывать работу того же менеджера. В моем случае: как временной компонент должен был бы влиять на
способность из совершенно другого класса, из другой области!? Вот это и стало главной жертвой погони за универсальностью.

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

Поэтому теперь мы займемся реализацией этого особого компонента AbiltyTimeComponent.
В классе SpellManager я создал метод SpecialComponentExpansion в который я буду отправлять "особый" компонент и его базу данных с
которыми он поступил в менеджер.
Это метод я буду вызывать при инициализации способности.
Синтаксис:
Используется csharp
private IEnumerator InitializeAbility(AbilityData data) {
 if (data.isUsing) {
  this.StartCoroutine(FinalizeAbility(data));
  yield return new WaitForFixedUpdate();
 }
 data.isUsing = true;
 IAbilityComponentable compSys = data.ability as IAbilityComponentable;
 if (compSys == null) yield break;
 foreach(AbilityComponent component in compSys.components) {
  if (component.CheckForAttribute<DisableComponentMultiplyUsage>())
   SpecialComponentExpansion(component, data);
  else
   SendComponentEvent(SpellManagerData.ComponentSendEvent.OnComponentExpansion, component);
  yield return new WaitForFixedUpdate();
 }
}

Здесь, в цикле, когда я перебираю компонент за компонентом, я смотрю есть ли на нем аттрибут DisableComponentMultiplyUsage, если
он есть - как в случае временного компонента т к мы ранее его этим компонентом наделили, я отправляю этот компонент а также базу
данных в метод SpecialComponentExpansion где уже буду заниматься им отдельно.

Давайте теперь заглянем в метод SpecialComponentExpansion.
Синтаксис:
Используется csharp
protected virtual void SpecialComponentExpansion(AbilityComponent component, AbilityData data) {
 if (component.targetValue == AbilitySettings.ComponentValueTarget.Time) {
  AbilityTimeComponent timeComponent = component as AbilityTimeComponent;
 }
}

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

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

В конкретно нашем случае я создал обертку - класс TimedComponentData который хранит два свойства только для чтения:
- data - ABilityData свойство, которое будет показывать к какой способности привязан этот временной компонент.
- timeComponent - ссылка на наш временной компонент
Давайте же создадим этот класс
Синтаксис:
Используется csharp
public sealed class TimedComponentData {

 public readonly AbilityData data = null;
 public readonly AbilityTimeComponent timeComponent = null;

 public TimedComponentData(AbilityData data, AbilityTimeComponent component) {
  this.data = data;
  this.timeComponent = component;
 }

}

Также у нас будет еще одно числовое свойство которое будет отвечать за текущее время работы компонента
- startTime - типа "float", будет показывать точное временное действие компонента
Синтаксис:
Используется csharp
public sealed class TimedComponentData {

 public readonly AbilityData data = null;
 public readonly AbilityTimeComponent timeComponent = null;

 public float startTime = 0f;

 public TimedComponentData(AbilityData data, AbilityTimeComponent component) {
  this.data = data;
  this.timeComponent = component;
 }

}

Готово. Теперь у нас есть обертка для работы с временным, "особым" компонентом.

Далее т к способностей с временным компонентом может быть много - в классе SpellManager придется создать список(массив) куда мы
будем кидать способности с этим компонентом.
Синтаксис:
Используется csharp
[DisallowMultipleComponent]
public class SpellManager : MonoBehaviour {

 private List<TimedComponentData> timedAbilities = new List<TimedComponentData>();

}

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

Возвращаемся в метод SpecialComponentExpansion
Синтаксис:
Используется csharp
protected virtual void SpecialComponentExpansion(AbilityComponent component, AbilityData data) {
 if (component.targetValue == AbilitySettings.ComponentValueTarget.Time) {
  AbilityTimeComponent timeComponent = component as AbilityTimeComponent;
 }
}

Теперь нам надо сохранить способность и ее временной компонент в обертку
Синтаксис:
Используется csharp
protected virtual void SpecialComponentExpansion(AbilityComponent component, AbilityData data) {
 if (component.targetValue == AbilitySettings.ComponentValueTarget.Time) {
  AbilityTimeComponent timeComponent = component as AbilityTimeComponent;
  TimedComponentData timedData = new TimedComponentData(data, timeComponent);
  timedData.startTime = Time.time + timeComponent.time;
  this.timedAbilities.Add(timedData);
 }
}

Вроде все просто.
Здесь я создаю новый экземпляр класса TimedComponentData и в его конструктор передаю два параметра: это компонент времени и саму
обертку способности.
Далее я в свойство startTime устанавливаю время когда компонент был добавлен тоесть Time.time - текущее время, и прибавляю к этому
времени значение time из компонента.

Т к эта обертка зависит от времени нам придется обновлять ее в апдейте.
Поэтому нам будет необходим метод Update - стандартный метод unity3d который вызывается каждый кадр.
Синтаксис:
Используется csharp
[DisallowMultipleComponent]
public class SpellManager : MonoBehaviour {

 private List<TimedComponentData> timedAbilities = new List<TimedComponentData>();

 protected virtual void Update() {
 
 }

 protected virtual void SpecialComponentExpansion(AbilityComponent component, AbilityData data) {
  if (component.targetValue == AbilitySettings.ComponentValueTarget.Time) {
   AbilityTimeComponent timeComponent = component as AbilityTimeComponent;
   if (timeComponent == null || this.timedAbilities.Any(d => d.data == data)) return;
   TimedComponentData timedData = new TimedComponentData(data, timeComponent);
   timedData.startTime = Time.time + timeComponent.time;
   this.timedAbilities.Add(timedData);
  }
 }
 
}

Теперь займемся обновлением, для этого я создал вспомогательный метод UpdateTimedAbilities для того чтобы не строить все действия в
одном методе Update
Синтаксис:
Используется csharp
[DisallowMultipleComponent]
public class SpellManager : MonoBehaviour {

 private List<TimedComponentData> timedAbilities = new List<TimedComponentData>();

 protected virtual void Update() {
  UpdateTimedAbilities();
 }

 private void UpdateTimedAbilities() {
 
 }

 protected virtual void SpecialComponentExpansion(AbilityComponent component, AbilityData data) {
  if (component.targetValue == AbilitySettings.ComponentValueTarget.Time) {
   AbilityTimeComponent timeComponent = component as AbilityTimeComponent;
   if (timeComponent == null || this.timedAbilities.Any(d => d.data == data)) return;
   TimedComponentData timedData = new TimedComponentData(data, timeComponent);
   timedData.startTime = Time.time + timeComponent.time;
   this.timedAbilities.Add(timedData);
  }
 }
 
}

Теперь ближе рассмотрим работу метода UpdateTimedAbilities в котором мы с помощью цикла будет перебирать каждый элемент в списке
который хранит временные компоненты.
Синтаксис:
Используется csharp
private void UpdateTimedAbilities() {
 for(int index = 0; index < this.timedAbilities.Count; index++) {
  TimedComponentData data = this.timedAbilities[index];
 }
}

Здесь логика будет простой: многие unity3d разработчики очень часто сталкиваются с написанием таймеров, обычно они это делают
через Update как и в нашем случае, некоторые через корутины, или еще как - вариантов здесь куча и все они зависят от вашего
воображения и знаний. Я же поначалу использовал свою собственную систему таймера на основе корутин, но т к эта система была создана
задолго до системы способностей и не имеет к ней ни какого отношения я не стал рассматривать этот вариант, а решил воспользоваться
обычным Update - более знакомому большинству.
И так вернемся к методу UpdateTimedAbilities, добавим несколько строк
Синтаксис:
Используется csharp
private void UpdateTimedAbilities() {
 for(int index = 0; index < this.timedAbilities.Count; index++) {
  TimedComponentData data = this.timedAbilities[index];
  if (Time.time >= data.startTime) {
   this.UseAbility(data.data);
   if (data.timeComponent.timeType == AbilitySettings.AbilityTimedType.Once) {
    this.timedAbilities.RemoveAt(index);
    if (data.data.ability.type == AbilitySettings.AbilityType.Passive)
     this.RemoveAbility(data.data.ability);
   } else if (data.timeComponent.timeType == AbilitySettings.AbilityTimedType.Repeat) {
    data.startTime = Time.time + data.timeComponent.time;
   }
  }
 }
}

Здесь я проверяю насколько много времени прошло с момента последнего обновления списка оберток и если это время больше чем то что
было задано в обертке то я перехожу по условию к действиям:
1) Первое действие показывает, что как только определенный промежуток времени проходит мы используем снова способность, т к в
этом и заключается основная задача работы временного компонента.
2)Следующим действием я проверяю какого вида таймер у нас: Once - что значит что таймер срабатывает лишь одни раз или Repeat
который показывает что таймер срабатывает сногократно раз.
3) Если наш таймер типа Once при выполнении этого условия мы просто удаляем этот временной компонент и его обертку из списка т к
они больше нам не нужны.
3.1) Но также я проверяю если способность которая имеет этот временной компонент ПАССИВНАЯ то я также и удаляю эту способность
Попробую объяснить на примере почему так: Если допустим объект приобретает способность ПАССИВНУЮ которая добавляет ему
скорость перемещения на ВРЕМЯ то после пройденного промежутка времени эта способность удаляется из объекта, финализируя
все свои свойства.
4) Если же наш таймер типа Repeat - это таймер который повторяется каждый раз через определенное время.
4.1) Согласно типу таймера Repeat мы просто задаем свойству startTime обертки TimedComponentData новый обратный отсчет который
снова и снова будет использовать способность симулируя зацикленность.

Вот так вот и реализован "особый" временной компонент из той записи в прошлой части.
Синтаксис:
Используется csharp
[DisallowMultipleComponent]
public class SpellManager : MonoBehaviour {

 private List<TimedComponentData> timedAbilities = new List<TimedComponentData>();

 protected virtual void Update() {
  UpdateTimedAbilities();
 }

 private void UpdateTimedAbilities() {
  for(int index = 0; index < this.timedAbilities.Count; index++) {
   TimedComponentData data = this.timedAbilities[index];
   if (Time.time >= data.startTime) {
    this.UseAbility(data.data);
    if (data.timeComponent.timeType == AbilitySettings.AbilityTimedType.Once) {
     this.timedAbilities.RemoveAt(index);
     if (data.data.ability.type == AbilitySettings.AbilityType.Passive)
      this.RemoveAbility(data.data.ability);
    } else if (data.timeComponent.timeType == AbilitySettings.AbilityTimedType.Repeat) {
     data.startTime = Time.time + data.timeComponent.time;
    }
   }
  }
 }

 protected virtual void SpecialComponentExpansion(AbilityComponent component, AbilityData data) {
  if (component.targetValue == AbilitySettings.ComponentValueTarget.Time) {
   AbilityTimeComponent timeComponent = component as AbilityTimeComponent;
   if (timeComponent == null || this.timedAbilities.Any(d => d.data == data)) return;
   TimedComponentData timedData = new TimedComponentData(data, timeComponent);
   timedData.startTime = Time.time + timeComponent.time;
   this.timedAbilities.Add(timedData);
  }
 }
 
}


Визуализатор.

Как и в играх StarCraft и WarCraft менеджер способностей не только может контролировать способности объекта, но также может их
както отображать чтобы пользователь мог видет процесс работы менеджера.
pic27.png

Как видно на изображении каждая способность имеет свою иконку которую можно отобразить на экране.
Каждая способность, не важно пассивная или активная имеет это свойство.
Дело остается только за менеджером и тем как именно пользователь хочет отображать эти иконки.

Сначала не много отвлекусь на теорию.
В моем случае, как на той записи можно понять что я рисую иконки с помощью GUI - стандартного инструмента отображения информации.
Если вы знаете любые другие способы отображения текстур иконок, вы можете ими воспользоваться, в этом у вас полная свобода.
В приложении, для которого я создавал эту систему, объекты имели максимум 3 способности, это не так много и не создает большой
нагрузки для приложения, поэтому я и решил использовать обычный GUI для их отображения.
Если же вы преследуете цель создания такой системы как в онлайн играх типа WoW или LineAge, в которых есть уйма способностей, где
практически весь экран ими и заполнен - то в таком случае я не рекомендую использовать вам стандартный GUI для их отображения, т к
это создает очень много Draw Call'ов что съедает очень много кадров на обработку. В таком случае вы можете использовать новый UI
недавно введенный в unity3d или всем известный NGUI.
В этой системе я буду рассматривать отображение иконок средствами стандартного GUI.

И так начну с того, что, как я много раз утверждал, менеджер только может показывать на ПРОГРАММНОМ уровне что объект может хранить и
обрабатывать способности в себе. Поэтому за отображения процесса менеджера нам придется использовать другой класс, который будет
просто наследоваться от класса SpellManager чтобы иметь доступ к данным этого класса.
Я назвал этот класс VisualSpellManager.
Синтаксис:
Используется csharp
public class VisualSpellManager : SpellManager {
 
}

По сути, т к этот класс наследуется от SpellManager, он тоже является менеджером, но только уже улучшенной его версией, т к через
этот класс мы будем наблюдать за процессом его работы.
Поэтому для начала перегрузим его виртуальные методы класса SpellManager
Синтаксис:
Используется csharp
public class VisualSpellManager : SpellManager {

 protected override void Awake() {
  base.Awake();
 }
       
 protected override void Start() {
  base.Start();
 }
       
 protected override void Update() {
  base.Update();
 }

}

Отлично, теперь каждый метод будет выполнять свою часть работы вызвав тотже базовый метод.
Теперь же займемся графической обработкой, должны будет отображаться иконки способностей примерно как из того видео.
Для этого объявим метод OnGUI.
Синтаксис:
Используется csharp
public class VisualSpellManager : SpellManager {

 protected override void Awake() {
  base.Awake();
 }
       
 protected override void Start() {
  base.Start();
 }
       
 protected override void Update() {
  base.Update();
 }

 private void OnGUI () {

 }

}

Далее создадим отдельный метод DrawAbilitiesGUI в котором мы уже конкретно будем рисовать наши иконки, чтобы не нагромождать метод
OnGUI всеми действиями сразу.
Синтаксис:
Используется csharp
public class VisualSpellManager : SpellManager {

 protected override void Awake() {
  base.Awake();
 }
       
 protected override void Start() {
  base.Start();
 }
       
 protected override void Update() {
  base.Update();
 }

 private void OnGUI () {
  DrawAbilitiesGUI();
 }

 private void DrawAbilitiesGUI() {

 }

}

Готово.
Теперь объявим пару свойств в этом классе.
- iconSize это числовое свойство будет показывать какого размера рисовать иконки.
- iconRect это свойство будет показывать область в которой мы будем рисовать иконки.
Синтаксис:
Используется csharp
public class VisualSpellManager : SpellManager {

 public float iconSize = 50f;

 private Rect iconRect;

 protected override void Awake() {
  base.Awake();
 }
       
 protected override void Start() {
  base.Start();
  this.iconRect = new Rect(0, Screen.height - this.iconSize, this.iconSize, this.iconSize);
 }
       
 protected override void Update() {
  base.Update();
 }

 private void OnGUI () {
  DrawAbilitiesGUI();
 }

 private void DrawAbilitiesGUI() {

 }

}

В методе Start я также создаю экземпляр области в которой будут отображаться иконки, и задаю этой области размер.
Переходим в метод DrawAbilitiesGUI
Т к у нас есть два списка двух разных типов способности то и обрабатывать я буду два цикла.
Синтаксис:
Используется csharp
private void DrawAbilitiesGUI() {
 this.iconRect.x = 0f;
 foreach(ActiveAbilityData data in this.data.activeAbilities) {

 }
 
 foreach(PassiveAbilityData data in this.data.passiveAbilities) {

 }
}

Для начала в каждом цикле мне надо проверить есть ли вообще у способности иконка, тоесть не равна ли она null
Синтаксис:
Используется csharp
private void DrawAbilitiesGUI() {
 this.iconRect.x = 0f;
 foreach(ActiveAbilityData data in this.data.activeAbilities) {
  if (data.ability.icon != null && this.iconRect.x < Screen.width) {
   GUI.DrawTexture(this.iconRect, data.ability.icon);
   this.iconRect.x += this.iconSize;
  }
 }
 
 foreach(PassiveAbilityData data in this.data.passiveAbilities) {
  if (data.ability.icon != null && this.iconRect.x < Screen.width) {
   GUI.DrawTexture(this.iconRect, data.ability.icon);
   this.iconRect.x += this.iconSize;
  }
 }
}

Все очень просто и наше графическое решение готово. О результате работы вы можете посмотреть в приложении к этой части статьи, или
из видео.
Синтаксис:
Используется csharp
public class VisualSpellManager : SpellManager {

 public float iconSize = 50f;

 private Rect iconRect;

 protected override void Awake() {
  base.Awake();
 }
       
 protected override void Start() {
  base.Start();
  this.iconRect = new Rect(0, Screen.height - this.iconSize, this.iconSize, this.iconSize);
 }
       
 protected override void Update() {
  base.Update();
 }

 private void OnGUI () {
  DrawAbilitiesGUI();
 }

 private void DrawAbilitiesGUI() {
  this.iconRect.x = 0f;
  foreach(ActiveAbilityData data in this.data.activeAbilities) {
   if (data.ability.icon != null && this.iconRect.x < Screen.width) {
    GUI.DrawTexture(this.iconRect, data.ability.icon);
    this.iconRect.x += this.iconSize;
   }
  }
 
  foreach(PassiveAbilityData data in this.data.passiveAbilities) {
   if (data.ability.icon != null && this.iconRect.x < Screen.width) {
    GUI.DrawTexture(this.iconRect, data.ability.icon);
    this.iconRect.x += this.iconSize;
   }
  }
 }

}

Теперь для того чтобы объект не только имел менеджер, но также и мог следить за процессом его работы достаточно добавить объекту
компонент(класс) VisualSpellManager.

Заключение:
Конечно эта система не конечный вариант полноценной системы способностей т к мы здесь рассматриваем только работу пассивной
способности, в частности. Это изза того что в корне эти способности одинаковы, но по мере расширения системы и разница в обработке
этих систем растет. В конечном варианте разница будет не большой, мы просто добавим некоторые особенности в работе с активной
способностью, сейчас же я хотел больше показать как в целом работает система. Поэтому следующая часть будет посвящена не менее
важной задаче, а именно "SaveLoad" системе способностей и конкретно о скриптовом объекте. Пока же можете посмотреть как работает сцена из видео, и глянуть исходный код системы.

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

Пак сцены из видео с исходным кодом.
AbilitiesSystem - Alpha.unitypackage

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

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

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

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