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

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

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

Сообщение lawsonilka 03 янв 2015, 17:57

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

Подведение итогов системы - рантайма.

В прошлой части я остановился отчасти на том как наглядно происходит процесс работы способности в менеджере и на том как происходит процесс ее активации.
И теперь когда менеджер может инициализировать способность через метод(корутину) через который объекту приминяются свойства способности, он также следовательно и должен удалять приложенные изменения при удалении самой способности.
В этом случае нам понадобиться обратный метод финализации(Finalize) через который мы будем финализировать способность - производить противоположное действие инициализации.
pic24.png

Как видно на картинке, при инициализации мы применяем изменения к объекту, а при финализации наборот отменяем все изменения у объекта.

Поэтому давайте вернемся в класс SpellManager и объявим новую корутину FinalizeAbility, которая также как и корутина инициализации будет брать один параметр AbilityData
Синтаксис:
Используется csharp
private IEnumerator FinalizeAbility(AbilityData data) {
 yield return;
}

Дальше мы как и в случае инициализации берем способность и ищем в ней компоненты через интерфейс IAbilityComponentable
Синтаксис:
Используется csharp
private IEnumerator FinalizeAbility(AbilityData data) {
 IAbilityComponentable compSys = data.ability as IAbilityComponentable;
 if (compSys == null) yield break;
 foreach(AbilityComponent component in compSys.components) {

 }
}

Теперь нужно в цикле, перебирая компонент за компонентом отослать их сообщением объекту. В случае инициализации мы делали это через вызов метода SendComponentEvent который согласно перечислению отсылал компонент в нужное место. В том случае мы пользовались перечислением OnComponentExpansion который отсылал компоненты для инициализации, следовательно в этом случае нам необходимо обратное перечисление, я назвал его OnComponentRexpansion(который отличается только одной буквой) и добавил это перечисление в статический класс SpellManagerData
Синтаксис:
Используется csharp
public static class SpellManagerData {
 
 public static SpellManager SpellManager(this GameObject gameObject) {
  return gameObject.GetComponent<SpellManager>();
 }
 
 public enum AbilitySendEvent {
  OnAbilityAdded,
  OnAbilityUsed,
  OnAbilityEndUse,
  OnAbilityRemoved
 }
 
 public enum ComponentSendEvent {
  OnComponentExpansion,
  OnComponentRexpansion
 }
 
}

Также я добавил еще одно событие в перечислении AbilitySendEvent, я добавил OnAbilityEndUse которое будет вызываться когда мы будем заканчивать использовать способность - согласно названию события.

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

Здесь я добавил в самом начале метода вызов события OnAbilityEndUse, дальше я с помощью цикла отправляю компоненты в метод SendComponentEvent который и отошлет компонент через событие.

Тогда переходим сначала в этот метод SendComponentEvent и расширим switch еще одним полем. Теперь он выглядит так
Синтаксис:
Используется csharp
protected void SendComponentEvent(SpellManagerData.ComponentSendEvent e, AbilityComponent component) {
 if (this.objectSpellBihaviour == null) {
  this.SendMessage(e.ToString(), component, SendMessageOptions.DontRequireReceiver);
 } else {
  switch(e) {
   case SpellManagerData.ComponentSendEvent.OnComponentExpansion: this.objectSpellBihaviour.OnComponentExpansion(component);
   break;
   case SpellManagerData.ComponentSendEvent.OnComponentRexpansion: this.objectSpellBihaviour.OnComponentRexpansion(component);
   break;
  }
 }
}

Здесь я вызываю метод OnComponentRexpansion у абстрактного класс AbilityBihaviour которого у него пока еще нет и поэтому нам также придется объявить этот абстрактный метод - возвращаемся в класс AbilityBihaviour и создаем метод OnComponentRexpansion который будет выполнять обратное действие методу OnComponentExpansion, теперь так выглядит класс AbilityBihaviour
Синтаксис:
Используется csharp
public abstract class AbilityBihaviour : MonoBehaviour {
 
 private SpellManager objectManager = null;
 
 public SpellManager spellManager {
  get {
   if (this.objectManager == null) {
    this.objectManager = this.gameObject.GetComponent<SpellManager>();
    if (this.objectManager == null) this.objectManager = this.gameObject.AddComponent<SpellManager>();
   }
   return this.objectManager;
  }
 }
 
 public virtual void OnAbilityAdded(AbilityObject ability) {
  Debug.Log("Character: " + this.name + " got new ability: " + ability.name);
 }
               
 public virtual void OnAbilityUsed(AbilityObject ability) {
  Debug.Log("Character: " + this.name + " used ability: " + ability.name);
 }
               
 public virtual void OnAbilityRemoved(AbilityObject ability) {
  Debug.Log("Character: " + this.name + " has removed ability: " + ability.name);
 }
 
 public virtual void OnAbilityEndUse(AbilityObject ability) {
  Debug.Log("Character: " + this.name + " stopped to use ability: " + ability.name);
 }
               
 public abstract void OnComponentExpansion(AbilityComponent component);
 public abstract void OnComponentRexpansion(AbilityComponent component);
 
}

Как вы видите я также сразу и добавил метод OnAbilityEndUse который будет вызываться при прекращении действия способности.

И в завершении немного вернемся в класс SpellManager в его метод SendAbilityEvent и добавим в switch еще одно поле.
Синтаксис:
Используется csharp
protected void SendAbilityEvent(SpellManagerData.AbilitySendEvent e, AbilityObject ability) {
 if (this.objectSpellBihaviour == null) {
  this.SendMessage(e.ToString(), ability, SendMessageOptions.DontRequireReceiver);
 } else {
  switch(e) {
   case SpellManagerData.AbilitySendEvent.OnAbilityAdded: this.objectSpellBihaviour.OnAbilityAdded(ability);
   break;
   case SpellManagerData.AbilitySendEvent.OnAbilityRemoved: this.objectSpellBihaviour.OnAbilityRemoved(ability);
   break;
   case SpellManagerData.AbilitySendEvent.OnAbilityUsed: this.objectSpellBihaviour.OnAbilityUsed(ability);
   break;
   case SpellManagerData.AbilitySendEvent.OnAbilityEndUse: this.objectSpellBihaviour.OnAbilityEndUse(ability);
   break;
  }
 }
}

Готово. Теперь все события вызовутся когда это будет необходимо.

Ну а теперь обратно вернемся к принципу финализации. И начну я с инициализации.
На самом деле эти методы имееют еще не окончательный вид, сейчас я попробую объяснить почему мы все еще не привели их к конечному виду.
Если вы вспомните то у нас есть два публичных метода в классе SpellManager, я говорю о методе AddAbility и методе UseAbility.
Первый добавляет способность, второй ее использует.
Как вы помните мы не можем добавлять одну и ту же способность больше ОДНОГО раза в менеджер - т к в методе AddAbility стоит ограничитель который нам это не позволит и следовательно можно не бояться что одна и та же способность будет добавлена больше одного раза.
НО в случае метода UseAbility - одна и та же способность может быть использована на объекте сколь угодно раз. Тоесть допустим, если я возьму ту пассивную способность из предыдущей части, которая увеличивает скорость перемещения, и попытаюсь добавить ее больше одного раза то у меня это не выйдет, но если я вызову метод UseAbility с этой способность, то она столько же раз и инициализируется в менеджере - тоесть сколько раз я ее использую столько раз она и изменит мое свойство скорости перемещения, вызову 10 раз - она и 10 раз увеличит мою скорость перемещения. И если в случае активной способности все вполне логично - сколько раз вызвал столько раз и присвоилось, то в случае пассивной способноси этого нельзя допустить!
Из всего этого следует что нужно както ограничивать или давать понять что способность уже используется объектом и что повторно ее использовать уже нельзя.
Я решил использовать некий флаг - булевую переменную, которая давала бы менеджеру понять что эта способность уже инициализировалась и ее нельзя еще повторно инициализировать.
Этот флаг я поместил в обертку способности - в класс AbilityData и назвал ее isUsing.
Этот флаг я буду активировать при нициализации способности и деактивировать при финализации, пока все логино и просто. Давайте так и сделаем.
Сначала добавим переменную isUsing в класс AbilityData
Синтаксис:
Используется csharp
public class AbilityData {
               
 public readonly AbilityObject ability = null;
               
 public bool isUsing = false;
               
 protected AbilityData(AbilityObject ability) {
  this.ability = ability;
 }
               
}

Далее возвращаемся сначала в метод инициализации способности InitializeAbility класса SpellManager и в самом начале устанавливаем этой переменной значение true.
Синтаксис:
Используется csharp
private IEnumerator InitializeAbility(AbilityData data) {
 data.isUsing = true;
 IAbilityComponentable compSys = data.ability as IAbilityComponentable;
 if (compSys == null) yield break;
 foreach(AbilityComponent component in compSys.components) {
  SendComponentEvent(SpellManagerData.ComponentSendEvent.OnComponentExpansion, component);
  yield return new WaitForFixedUpdate();
 }
}

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

Теперь давайте мысленно проверим что мы сделали.
При инициализации способности, менеджер эту способность помечает как ИНИЦИАЛИЗИРОВАННУЮ, и если мы захотим еще раз использовать эту способность то у нас этого НЕ должно получиться.
Только после финализации, когда менеджер опять пометит эту способность как НЕ ИСПОЛЬЗОВАННУЮ, только тогда мы сможем ее снова применить. К примеру: я добавляю объекту пассивную способность которая увеличивает ему скорость перемещения, дальше я пытаюсь схитрить и т к не могу добавить способность еще раз я пытаюсь ее использовать через метод UseAbility, я опять вызываю этот метод после добавления способности и вижу что ничего не происходит со свойствами объекта - т к способность которую я хочу активировать уже активная. Для этого и нужен этот флаг.
Теперь осталось только решить куда поместить проверку этого флага.
Для этого мне снова пришлось вернуться к принципу работы способностей в таких играх как StarCraft и WarCraft.
Я заметил там одну вещь: если на персонажа использовать какуюто способность которая чтото изменяет на объекте мы увидим эффект этой способности - в нашем случае мы увидим что свойства объекта изменились, и если опять же применить эту способность на персонажа то ни чего не измениться, персонаж также будет иметь этот же эффект. Любой на первый взгляд может решить что мы просто еще раз инициализировали способность, это так но лишь отчасти. Т к если бы мы просто использовали(инициализировали) способность на персонаже то его свойства бы удвоились, но на самом же деле, я решил, что там происходит два действия.
И первым действием является ФИНАЛИЗАЦИЯ способности - тоесть ее завершение, снятие каких либо изменений.
И только тогда вторым действием уже будет ИНИЦИАЛИЗЦИЯ способности по новой - изменения свойств персонажа.

Вот так вот. Тоесть если я добавлю объекту способность которая будет увеличивать его скорость перемещения, и попробую ее еще раз инициализироать(активировать) то сначала я ФИНАЛИЗИРУЮ эту способность - удалю все ее старые эффекты и только потом снова ее инициализирую.
Поэтому нам, в случае повторной инициализации способности, когда мы захотим ее еще раз использовать(применить) к объекту, мы проверим: инициализирована ли способность УЖЕ, и если это так, то мы сначала финализируем эту способность а только потом снова начнем ее инициализацию. Поэтому я добавил эту проверку в метод инициализации InitializeAbility способности.
Вот как теперь она выглядит.
Синтаксис:
Используется 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) {
  SendComponentEvent(SpellManagerData.ComponentSendEvent.OnComponentExpansion, component);
  yield return new WaitForFixedUpdate();
 }
}

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

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

Теперь для полноценной работы финализирования, нужно добавить один маленький штрих - финализация пассивной способности при удалении.
Как вы помните пассивная способность начинает работать сразу же после добавления ее в менеджер, также, в отличие от АКТИВНОЙ способности, мы должны будет финализировать ПАССИВНУЮ способности сражу же после ее удаления.
Поэтому вернемся в метод RemoveAbility класса SpellManager
Синтаксис:
Используется 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);
 if (ability.type == AbilitySettings.AbilityType.Passive)
  this.StartCoroutine(FinalizeAbility(data));
}

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

Теперь когда менеджер полноценно умеет избавляться от способностей давайте проделаем это на примере нашей прошлой способности из предыдущей части, где мы создавали пассивную способности что изменяет скорость перемещения - возвращаемся в класс StandartCharacter и применим в нем изменения что были произведены в классе AbilityBihaviour а точнее перегрузим метод OnComponentRexpansion.
Вот что я получил.
Синтаксис:
Используется 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);
  yield return new WaitForSeconds(5f);
  this.spellManager.RemoveAbility(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);
 }
 
 public override void OnComponentRexpansion(AbilityComponent component) {
  AbilitySettings.ComponentValueTarget valueTarget = component.targetValue;
  object value = component.objectValue;
  Debug.Log("Got component: " + valueTarget + " with type: " + value.GetType() + " in order to remove.");
  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);
 }
}

Как вы видите я перегрузил метод OnComponentRexpansion который должен будет избавляться от эффектов способности.
В нем я также добавил switch который перебирает "цели"(вы также как и в прошлой части можете заменить это все рефлексией) и согласно цели производит действия со свойством объекта, в нашем случае убирает примененное ранее значение в методе OnComponentExpansion.
Также я добавил в метод Start пару действий. После добавления способности, я жду 5 секунд и удаляю ее через метод RemoveAbility.
Теперь можно смело запускать игру и посмотреть что же выйдет у вас в дебаг.
pic25.png

Как видите на изображении я получил много информации.
Первые 5 я трогать не буду т к их мы разбирали еще в предыдущей части и сразу начну с 6го сообщения.
- это сообщения вызывается когда мы финализируем способность.
Следующее сообщение выводится в методе OnComponentRexpansion
- это сообщение показывает что за компонент поступил в объект и какова его "цель".
Далее выводится сообщение о том как изменилось свойство скорости перемещения.
- это сообщение показывает как мы удалили эффект у свойства.

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

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

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

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

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