Reworking the Ability Architecture

Hey Everybody,

first of all I want to thank Rick and Ben for the awesome course. It really helped me get into the propper mindset to finally push on with my project and not get daunted by all the things that need doing.

Now I want to share some of my experience with the ability system and maybe spare others the hours I spent - definitely not wasted - fixing an unexpected behaviour.

Since I am building an RPG which will need some kind of weapon behaviour. I thought about using the same method we applied for the special abilites with attaching behaviour components but I figured it would be best to centralize as much as possible to not repeat myself. I decided to drop “attacktarget” and instead created a default attack ability. I bet Ben will instantly know where this is going :slight_smile:
Anyway here the trouble began. So what did happen:

In my TestingScene I have a Player and 1 Enemy, both AI controlled. Under normal circumstances they walk up to each other, beat at each other and eventually die.

Now what did appear to happen? The Player would walk up to the enemy and apparently stop doing anything. Still the player and the enemy lost health and eventually died.

I will spare you the process of endless debug.log() calls to figure out what was going on and skip to the finding. The player and the enemy both implemented the DefaultMeleeAttackAbilityConfig. They both attached a MeleeAttackBehaviour BUT we stored the reference to the behaviour in the config when calling

 public void AttachAbilityTo(GameObject objectToattachTo)
        {
            AbilityBehaviour behaviourComponent = GetBehaviourComponent(objectToattachTo);
            behaviourComponent.SetConfig(this);
            behaviour = behaviourComponent;
        }

Then called

public void Use(GameObject trigger, GameObject target)
        {
            behaviour.Use(target);
        }

on the AbilityConfig. This means that at any given time we could only have one reference to a specific AbilityBehaviour since it was stored in the scriptable object.
In my case the Use(GameObject target) call from the player was actually passed on to the behaviour attached to the enemy resulting in an apparently idle player.

How did I fix this?
I wanted to retain the general SO architecture of our abilities since it works well with some other design decisions I have made for my game. So the main issue to address was that we relayed the Use() call over the AbilityConfig. Currently the character did only have reference to the AbilityConfigs attached to it through our array of abilityconfigs but I needed direct access to the abilitybehaviours on the character.

Now the I knew what was going on and what I needed to fix the issue, the actual process only took me a couple of minutes.
So I changed our AttachAbilityTo Function to also return the component.

public AbilityBehaviour AttachAbilityTo(GameObject objectToattachTo)
        {
            AbilityBehaviour behaviourComponent = GetBehaviourComponent(objectToattachTo);
            behaviourComponent.SetConfig(this);
            return behaviourComponent;
        }

Now I introduced a List to store the attached AbilityBehaviours.

public class CharacterAbilities : MonoBehaviour
    {
        [SerializeField] List<AbilityConfig> abilities = new List<AbilityConfig>();
        [SerializeField] List<AbilityBehaviour> abilityBehaviours = new List<AbilityBehaviour>();

        void Start()
        {
            currentWeaponConfig = GetComponent<WeaponSystem>().GetCurrentWeaponConfig();
            AttachInitialAbilities();
        }

        void AttachInitialAbilities()
        {
            for (int abilityIndex = 0; abilityIndex < abilities.Count; abilityIndex++)
            {
                abilityBehaviours.Add(abilities[abilityIndex].AttachAbilityTo(gameObject));
            }
        }
       
        public void AttemptSpecialAbility(int abilityIndex, GameObject target = null)
        {
            var energyCost = abilities[abilityIndex].GetEnergyCost();

            if (IsManaAvailable(energyCost))
            {
                characterStats.ReduceMana(energyCost);
                abilityBehaviours[abilityIndex].Use(target);
            }
            else
            {
                Debug.Log("Not enough Mana!");
            }
        }
}

This way I have moved the last remnant of the behaviour, namely the initial call, out of the SO.
Note that I am still refering to the SO for the static data, like the energycost.

I hope this helps others who think about reusing the created abilites for enemy characters aswell. If you have any feedback, I appreciate any tips to improve on this further.

Privacy & Terms