What is better for an ability system: one script for all abilities or one script for each ability?

So I’m basically working on an ability system for my game and want to work with a “loadout” style system. So the player can edit their choice of three spells to put in their loadout for a match, and change them out for other spells.

My main issue is actually making these spells. Right now I have each spell as a ScriptableObject which contains a name, a thumbnail, some stats for cooldown and active time, and an enum containing a spell ID, a unique identifier for that spell.

I then have a script which listens for mouse or keyboard inputs and depending on which spell scriptable object is put into which slot, it corresponds to a keybinding. It then passes through that spell ID when the key is pressed into another script which reads the spellID in a switch statement and calls the function associated with that SpellID. Something like this:

public class Spellbook : MonoBehaviour
{
    public SpellID eSpellID;

    public void SpellCheck(SpellID spellID)
    {
        switch (spellID)
        {
            case SpellID.Icebolt:
                Icebolt();
                break;

            case SpellID.Fireball:
                Fireball();
                break;

            case SpellID.Smite:
                Smite();
                break;
        }
    }
}

The issue I can see with this way of doing things is that I would have one giant script with each spell and all of the prefabs and data values associated with that spell, and it doesn’t make sense to load all this data just to cast three spells out of many.

My next thought was just to have individual scripts for each spell, but that would still entail loading every script onto the player, and I could have upwards of 30 spells for example and it starts to become an issue. I could always unload the unused scripts, but they still need to be loaded first and they can be a pain to all show up in the editor.

If there was a way to have individual scriptableobjects contain different behaviors like methods that would solve my problem, but i was told that SO’s should be used for storing data and not behavior. I also am looking into things like inheritance, overrides, and interfaces, but I don’t think that solves the issue of having a bunch of scripts loaded all at once.

Appreciate any help!

Hey there,

in general that sounds 100% like an application for inheritance. You should not worry about the “load of scripts on the player”. This is nothing that really draws on the games performance or the memory needed. It is way more important to have a decent Structure for your code so that your project does not collapse on you in a few weeks.

I’d start with a baseclass called

  public abstract class Spell {
         protected YourStatsContainerClass SpellStats;
         public virtual void Cast()
  }

It does not really have to be a Monobehaviour

From this you can then derive all other spells. For example:

  public class RangedSpell : Spell {
         public float getRange() { return 9001f;}
  }

  public class FireBall : RangedSpell
  public class LightningArc : RangedSpell

Now this gives you a lot of things that you can do.
Firstly, you can now override the Cast function of Spell for each spell individually to give each of them a specific unique behaviour:

  public class FireBall : RangedSpell {
           public override void Cast() {
                      GameObject go = Instantiate(FireballPrefab);
                      go.AddForce Over 9000
           }
  }

At the same time your Spellbook now looks like this:

   public class Spellbook : MonoBehaviour {
            public List<Spell> spells = new List<Spell>();
   }

where you can easily add a new Spell:

    spells.Add(new FireBall()); // now the spellbook contains a fireball.

that you could trigger with:

    spells[indexOfFireBall].Cast();

What I added a bit further up but did not really go into yet is your stats container:
Assuming you have a scriptable object for each spell you can make each of your Spells load this container in its constructor.

For example:

        public class FireBall : RangedSpell {
                 public FireBall(){
                        SpellStats = Resources.Load("Some/Path/To/That/SO"); //dunno if you have to instantiate the SO to use it. Don't remember tbh.
                 }
       }

One more thing that you can do now is that if you use different “middle-classes” like RangedSpell:

   if(spells[someIndex] is RangedSpell rangedSpell) // this checks if the spell in your book at index "someindex" is of type `RangedSpell`. If this is the case we automatically cast it to a rangedSpell and now can access those functions of that class. 
   {
            Debug.Log($"someindex is a rangedSpell with {rangedSpell.getRange()} range");
   }

let me know if that helps or if you have questions or soemthing was unclear.