Система.
В прошлый раз мы закончили на том что описали базовые принципы работы менеджера способностей и систему контроля поведения AbilityBihaviour.
Как видите на картинке изображен процесс работы основной задачи менеджера. Объект на картинке(звезда) обладает компонентом - менеджером способностей, в менеджер поступают способности. Где через метод 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;
}
}
}
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);
}
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;
}
[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.");
}
}
[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 и вывожу в дебаг какой компонент и с чем мы получили из менеджера.
Можно кинуть компонент 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.");
}
}
[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.");
}
}
Здесь я создал пассивную способность - пустышку и через секунду добавил ее в менеджер.
Давайте теперь запустим игру и посмотрим что выведется в дебаг.
Здесь изображено что я получил в консоль когда добавил способность в менеджер.
В консоль вывелось два сообщения:
- 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
}
[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);
}
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, и секунду спустя добавляю способность обратно в менеджер.
Давайте теперь запустим игру и посмотрим что выведется в консоль.
Как видите на картинке теперь вывелось три сообщения, первые два мы уже разобрали, теперь рассмотрим третье.
- 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;
}
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);
}
}
[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);
}
}
Давайте теперь запустим игру и посмотрим что выведется в консоль.
На изображении показаны все сообщения. Первые три мы уже разобрали, четвертое сообщение показывает на сколько ед изменилось свойство 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();}
}
}
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