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

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

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

Сообщение lawsonilka 27 дек 2014, 17:51

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

Система.


В прошлый раз мы закончили на том что описали базовые принципы работы менеджера способностей и систему контроля поведения AbilityBihaviour.
pic15.png

Как видите на картинке изображен процесс работы основной задачи менеджера. Объект на картинке(звезда) обладает компонентом - менеджером способностей, в менеджер поступают способности. Где через метод AddAbility она сохраняются в базе данных менеджера.

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

На следующей части изображении показан объект(звезда) который не только имеет компонент менеджера способностей но также имеет компонент поведения AbilityBihaviour на который менеджер уже имеет влияние. Только через компонент поведения менеджер может изменять свойства объекта согласно способности и ее компонентам.

Давайте теперь рассмотрим принцип работы способностей в менеджере.
И так менеджер получает способность и помещает ее в список, дальше, следуя логике дейcтвий метода AddAbility, мы смотрим: если способность пассивная то через метод UseAbility мы производим активацию способности - т к пассивная способность ни какого другого вида aктивации не имеет.

Заглянув в класс менеджера мы видим что у нас есть два метода UseAbility с разным доступом: приватным и публичным.
Первый метод UseAbility с публичным доступом получает в виде параметра способность, точнее ссылку на нее.
Второй метод UseAbility с приватным доступом уже получает параметр в виде обертки в которой хранится способность.
Это сделано так потому что, как я говорил ранее, мы НЕ должны ни в коем случае допустить выход данных способности за пределы менеджера, а так как мы не можем их получить то и не можем присвоить. Следовательно мы можем активировать способность только имея ссылку на нее в виде объекта AbilityObject через метод AddAbility.

Активацию мы производим с помощью корутины(метода) InitializeAbility, который разберает способность на компоненты. Сам менеджер не может использовать эти компоненты и поэтому существует компонент поведения AbilityBihaviour он шлет компоненты в этот класс и там уже пользователь использует компоненты по назначению - согласно цели.

Теперь по аналогии коснемся активной способности. Она также как и пассивная активируется через корутину(метод) InitializeAbility, но т к активную способность игрок должен будет активировать через кнопку castKey нужно весь входящий Input с клавиатуры регистрировать в методе Update. Давайте тогда закончим этот метод и активацию способности.
Просто в методе Update заместо Debug вызовим метод UseAbility в который и передадим способность для активации.
Синтаксис:
Используется csharp
protected virtual void Update() {
 foreach(ActiveAbilityData data in this.data.activeAbilities) {
  if (Input.GetKeyUp(data.ability.castKey)) {
   UseAbility(data);
   return;
  }
 }
}

Готово. Теперь нажав определенную кнопку мы вызовим определенную активную способность.

И для полноценной работы менеджера мы также закончим метод RemoveAbility который будет удалять ненужную способность.
Синтаксис:
Используется csharp
public void RemoveAbility(AbilityObject ability) {
 AbilityData data = this.data.FindAbilityData(ability);
 if (data == null) return;
 this.data.RemoveAbilityData(data);
 SendAbilityEvent(SpellManagerData.AbilitySendEvent.OnAbilityRemoved, ability);
}

Все очень просто. Здесь я ищу обертку способности которую хотим удалить через метод FindAbilityData если обертка существует - значит удаляем ее через метод RemoveAbilityData и напоследок отсылаем сообщение об удалении.

Ну вот теперь мы имеем более менее полноценный менеджер который теперь уже точно может получать\удалять\активировать способности.

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

Для начала, как было изображено, объект должен обладать компонентом менеджера.

Создадим простой кубик на сцене без каких либо других компонентов и кинем на него скрипт менеджера SpellManager.
Теперь в наш куб можно добавлять способности.
Добавленные способности ни как не будут влиять на объект пока мы не добавим компонент поведения ABilitybihaviour. Но этот компонент мы не сможем добавить т к это абстрактный класс и чтобы его можно было добавить к объекту нужно сначала его унаследовать обычному классу.

Создадим обычного персонажа со стандартными характеристиками: здоровье, броня, скорость перемещения.
Этот компонент я назвал StandartCharacter и унаследовал его от AbilityBihaviour
Синтаксис:
Используется csharp
public class StandartCharacter : AbilityBihaviour {

}

Теперь добавим в него свойства health, armor, moveSpeed.
Синтаксис:
Используется csharp
public class StandartCharacter : AbilityBihaviour {

 [Range(0f, 100f)]
 public float health = 100f;
 [Range(0f, 200f)]
 public float armor = 200f;
 [Range(100f, 500f)]
 public float moveSpeed = 250f;

}

Теперь т к компилятор попросит нас реализовать все абстрактные методы класса AbilityBihaviour этот метод всего один - OnComponentExpansion
Синтаксис:
Используется csharp
public class StandartCharacter : AbilityBihaviour {

 [Range(0f, 100f)]
 public float health = 100f;
 [Range(0f, 200f)]
 public float armor = 200f;
 [Range(100f, 500f)]
 public float moveSpeed = 250f;

 public override void OnComponentExpansion(AbilityComponent component) {
  AbilitySettings.ComponentValueTarget valueTarget = component.targetValue;
  object value = component.objectValue;
  Debug.Log("Got component: " + valueTarget + " with type: " + value.GetType() + " in order to add.");
 }

}

Готово. В этом методе я также раскладываю компонент на "цель" valueTarget и значение value и вывожу в дебаг какой компонент и с чем мы получили из менеджера.
pic20.png

Можно кинуть компонент StandartCharacter на объект - кубик, как на картинке.

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

Ну а теперь можно и создать способность, только пока программно. В компоненте StandartCharacter я в методе Start создам пассивную способность которая пока будет просто пустышкой чтобы вы увидели как работает менеджер и общается с компонентом поведения.
Синтаксис:
Используется csharp
public class StandartCharacter : AbilityBihaviour {

 [Range(0f, 100f)]
 public float health = 100f;
 [Range(0f, 200f)]
 public float armor = 200f;
 [Range(100f, 500f)]
 public float moveSpeed = 250f;

 private IEnumerator Start() {
  PassiveAbility ability = new PassiveAbility("Dummy ability");
  yield return new WaitForSeconds(1f);
  this.spellManager.AddAbility(ability);
 }

 public override void OnComponentExpansion(AbilityComponent component) {
  AbilitySettings.ComponentValueTarget valueTarget = component.targetValue;
  object value = component.objectValue;
  Debug.Log("Got component: " + valueTarget + " with type: " + value.GetType() + " in order to add.");
 }

}

Здесь я создал пассивную способность - пустышку и через секунду добавил ее в менеджер.
Давайте теперь запустим игру и посмотрим что выведется в дебаг.
pic21.png

Здесь изображено что я получил в консоль когда добавил способность в менеджер.
В консоль вывелось два сообщения:
- 1) Сообщение о том что в объект кубик добавилась способность Dummy ability. Это сообщение отсылается в консоль в классе AbilityBihaviour OnAbilityAdded.
- 2) Сообщение о том что объект кубик использовал способность Dummy abbility когда добавил его. Все верно, когда мы добавляем любую пассивную способность она сразу же активируется - и мы получаем сообщение об этом в методе OnAbilityUsed класса AbilityBihaviour
Теперь у объекта кубика есть пассивная способность Dummy ability которая ни чего не делает и не изменяет у объекта т к в ней нет компонентов... пока что.

Так. Давайте добавим компонент который бы увеличивал объекту скорость перемещения на 100 ед.
Для этого мне понадобиться сначала создать "цель" MoveSpeed в перечислении ComponentValueTarget.
Синтаксис:
Используется csharp
public enum ComponentValueTarget {
 [ComponentValueType(typeof(AbilityObjectComponent))] Special,
 [ComponentValueType(typeof(AbilityFloatComponent))]  Health,
 [ComponentValueType(typeof(AbilityFloatComponent))]  Armor,
 [ComponentValueType(typeof(AbilityTimeComponent))]   Time,
 [ComponentValueType(typeof(AbilityFloatComponent))]  MoveSpeed
}

Эта цель тоже будет представлять тип компонента со значением "float".
Теперь вернемся в метод Start класса StandartCharacter, теперь он будет выглядеть так
Синтаксис:
Используется csharp
private IEnumerator Start() {
 PassiveCustomAbility ability = new PassiveCustomAbility("Move Speed");
 AbilityFloatComponent component = new AbilityFloatComponent("Speed", AbilitySettings.ComponentValueTarget.MoveSpeed);
 component.value = 100;
 ability.AddComponent(component);
 yield return new WaitForSeconds(1f);
 this.spellManager.AddAbility(ability);
}

Т к теперь наша пассивная способность может содержать в себе компоненты, значит это должна быть Cusom способность, поэтому я использую пассивную способность PassiveCustomAbility.
Далее я создаю экземпляр компонента AbilityFloatComponent и добавляю в него значение 100 ед.
И дальше просто добавляю этот компонент в способность через метод AddComponent, и секунду спустя добавляю способность обратно в менеджер.
Давайте теперь запустим игру и посмотрим что выведется в консоль.
pic22.png

Как видите на картинке теперь вывелось три сообщения, первые два мы уже разобрали, теперь рассмотрим третье.
- 3) Данное сообщение выводится нашим перегруженным методом OnComponentExpansion класса StandartCharacter где сообщается о том
что за "цель" имеет компонент и какого типа значение он несет.

Теперь когда мы получили компонент мы можем использовать его значение как угодно или согласно "цели".
Поэтому немного расширим метод OnComponentExpansion.
Синтаксис:
Используется csharp
public override void OnComponentExpansion(AbilityComponent component) {
 AbilitySettings.ComponentValueTarget valueTarget = component.targetValue;
 object value = component.objectValue;
 Debug.Log("Got component: " + valueTarget + " with type: " + value.GetType() + " in order to add.");
 switch(valueTarget) {
  case AbilitySettings.ComponentValueTarget.MoveSpeed:
   try { this.moveSpeed += (float)value;} catch { throw new System.FieldAccessException();}
  break;
  default: return;
  break;
 }

Debug.Log("Character move speed: " + this.moveSpeed);
}
И так что же тут происходит. С помощью swith я смотрю какая "цель" у компонент, что этот компонент хочет изменить у объекта, в моем случае он хочет изменить скорость перемещения это соответсвует "цели" MoveSpeed, дальше
Синтаксис:
Используется csharp
try { this.moveSpeed += (float)value;} catch { throw new System.FieldAccessException();}

Я беру свойство moveSpeed у объекта и изменяю его - добавляю значение из компонента. Но здесь также я это действие(добавление значения) взял в область try catch чтобы т к сказать организовать "защиту от дураков" в случае когда компонент будет иметь значение НЕ согласно "цели", тоесть допустим если я вместо численного значения скорости объекта буду иметь значение строки, то я не смогу изменить свойство moveSpeed у объект, произойдет ошибка т к свойство moveSpeed численное а значение из компонент "строка". Поэтому в этом случае если значения не совместимы мы просто получим Exception и код дальше без ошибок будет выполняться.
Давайте теперь посмотрим как вылглядит весь класс StandartCharacter
Синтаксис:
Используется csharp
public class StandartCharacter : AbilityBihaviour {

 [Range(0f, 100f)]
 public float health = 100f;
 [Range(0f, 200f)]
 public float armor = 200f;
 [Range(100f, 500f)]
 public float moveSpeed = 250f;

 private IEnumerator Start() {
  PassiveCustomAbility ability = new PassiveCustomAbility("Move Speed");
  AbilityFloatComponent component = new AbilityFloatComponent("Speed", AbilitySettings.ComponentValueTarget.MoveSpeed);
  component.value = 100;
  ability.AddComponent(component);
  yield return new WaitForSeconds(1f);
  this.spellManager.AddAbility(ability);
 }

 public override void OnComponentExpansion(AbilityComponent component) {
  AbilitySettings.ComponentValueTarget valueTarget = component.targetValue;
  object value = component.objectValue;
  Debug.Log("Got component: " + valueTarget + " with type: " + value.GetType() + " in order to add.");
  switch(valueTarget) {
   case AbilitySettings.ComponentValueTarget.MoveSpeed:
    try { this.moveSpeed += (float)value;} catch { throw new System.FieldAccessException();}
   break;
   default: return;
   break;
  }
  Debug.Log("Character move speed: " + this.moveSpeed);
 }

}

Давайте теперь запустим игру и посмотрим что выведется в консоль.
pic23.png

На изображении показаны все сообщения. Первые три мы уже разобрали, четвертое сообщение показывает на сколько ед изменилось свойство moveSpeed компонента StandartCharacter, в данном случае оно увеличилось на 100 ед как и было задуманно.

Теперь мы имеем работоспособный менеджер который отсылает все что нужно и куда нужно.

Немного отвлекусь на перегруженный метод OnComponentExpansion и почему я использую switch в определении "целей".
Опытный программист мог бы не мучаться с определением каждого case в переборе "целей", можно было бы с помощью рефлексии(Reflection) просто найти нужное свойство согласно "цели" и изменить его значение.
Выглядело это бы примерно так.
Синтаксис:
Используется csharp
public override void OnComponentExpansion(AbilityComponent component) {
 AbilitySettings.ComponentValueTarget valueTarget = component.targetValue;
 object value = component.objectValue;
 Debug.Log("Got component: " + valueTarget + " with type: " + value.GetType() + " in order to add.");
 FieldInfo info = this.GetType().GetField(valueTarget.ToString(), BindingFlags.Public | BindingFlags.Instance);
 if (info != null) {
  try { info.SetValue(this, (float)value);}
  catch { throw new TargetException();}
 }
}

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

Ну а мы движемся дальше, но пока только в плане теории.
Теперь мы наглядно продемонстрировали как работает менеджер и зачем нам нужен был компонент AbilityBihaviour - пассивная способность добавилась и разложилась на компоненты которые в свою очередь менеджер отправил на доработку в компонент поведения.
Как вы видели значение свойства moveSpeed увеличилось на 100 ед, когда мы добавили пассивную способность с компонентом.
Это действие больше напоминает мне об эффектах в таких играх как StarCraft и WarCraft. Способность прмиеняет эффект к объекту в нашем случае этот эффект увеличил нашу скорость перемещения. Если же мы через метод RemoveAbility удалим нашу способность у объекта кубика то его значение moveSpeed так и останется неизменным, как бы сказать эффект от способности останется - следовательно нам нужно будет также и удалять эффект от способности когда мы ее удаляем, в нашем случае вернуть значение moveSpeed обратно как было до того момента когда мы добавили способность.

Поэтому в следующей части я расскажу о том как избавляться от эффектов с помощью корутины(метода) FinalizeAbility в менеджере, который будет удалять все эффекты у объекта при удалении способности.

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

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

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

Сообщение newwise 25 фев 2016, 15:07

Добрый день!
В момент вызова старт
private IEnumerator Start() {
PassiveAbility ability = new PassiveAbility("UP Speed"); //создаем новую способность
AbilityFloatComponent component = new AbilityFloatComponent("Speed", AbilitySettings.ComponentValueTarget.MoveSpeed); //Создаем новый компонент с имененм Скорость и назначемц цель MoveSpeed
component.value = 100; //Устанавливаем значение у компонента Speed = 100
ability.AddComponent(component);
yield return new WaitForSeconds(1f);
this.spellManager.AddAbility(ability);
}


Ругается на "ability.AddComponent(component);"

Assets/_AbilitySystem/Scripts/TestsScripts/StandartCharacter.cs(28,25): error CS1061: Type `PassiveAbility' does not contain a definition for `AddComponent' and no extension method `AddComponent' of type `PassiveAbility' could be found (are you missing a using directive or an assembly reference?)

Проверил:


public class PassiveCustomAbility : PassiveAbility, IAbilityComponentable {

....

public AbilityComponent AddComponent(AbilityComponent component) {
if (this.abilityComponents.Any(c => c.GetType() == component.GetType()))
if (component.CheckForAttribute<DisableComponentMultiplyUsage>()) return null;
this.abilityComponents.Add(component);
this.abilityArrayComponents = this.abilityComponents.ToArray();
return component;
}
...

Где мог ошибиться?
newwise
UNец
 
Сообщения: 25
Зарегистрирован: 23 фев 2016, 22:45

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

Сообщение newwise 25 фев 2016, 15:17

Все, сам разобрался, вы же пишете PassiveCustomAbility
Глаза замылились уже :)

Да и большое, огромное спасибо за вашу работу, осваиваю дальше :)
newwise
UNец
 
Сообщения: 25
Зарегистрирован: 23 фев 2016, 22:45

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

Сообщение ilka 25 фев 2016, 19:15

Да и большое, огромное спасибо за вашу работу, осваиваю дальше

Я не хочу вас расстраивать, но если вы задались целью написать систему способностей по примеру этой огромной статьи то я вам НЕ рекомендую этого делать. Эта статья ХУДШЕЕ из всех статей что я когда либо здесь писал, целый месяц я потратил на то чтобы реализовать систему, но чем дальше я работал тем больше мне становилось понятно что ушел с пути который задумывал изначально. Была б моя воля я бы удалил эту огромную, вводящую в заблуждение статью. Я уже чуть ли не четвертый раз начинал все с нуля, я до сих пор уверен что компонентная система лучшая идея для подобной системы, но все решала именно реализация. Во первых сразу скажу что эта система ВООБЩЕ не похожа на те которые используются в StarCraft или WarCraft, хоть я это и писал в первой части. И считаю что писать подобные системы на unity не имеет смысла вообще. Я искал компромисс между гибкостью и удобством использования и в этом плане компонентная система подходит лучше всего: она дает нужную гибкость плюс сам редактор unity имеет все инструменты для управления такой системой.
Поэтому если вы преследуете цель написать подобную систему, я считаю что это плохой пример в данном плане, ну а если вы просто практикуетесь в ООП тогда можете и дальше изучать материал статьи.
ilka
UNIверсал
 
Сообщения: 478
Зарегистрирован: 21 авг 2015, 19:32

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

Сообщение newwise 26 фев 2016, 13:13

Большое спасибо за комментарий!
В настоящий момент я ищу путь каким пойти в части реализации большого кол-ва способностей.
Представьте 10-ки героев, у каждого до 5 уникальных способностей, влияющих на множество параметров: жизни, скорость атаки, броня, защита, энергия, скорость энергии, разные станы и т.д.
Почти как в Dota, только у NPS тоже могут быть 1-2 способности. Таким образом нужен некий конструктор для быстрого создания/изменения способностей, т.к. с балансом всей этой каши нужно будет много париться.

Сейчас пытаюсь найти для себя вариант.
Найти оптимальное решение по трудоемкости создания/изменения/БАЛАНСА способностей (т.к. их много) и простоты реализации в Unity.


Если, что посоветуете, общий вектор, было бы здорово.

В любом случае спасибо, очень много для себя понял в ООП
newwise
UNец
 
Сообщения: 25
Зарегистрирован: 23 фев 2016, 22:45


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

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

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