Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Trying to create an enemy spell system using scriptable objects.

Discussion in 'Scripting' started by jungle_, Feb 1, 2021.

  1. jungle_

    jungle_

    Joined:
    Sep 13, 2020
    Posts:
    17
    I am creating a 2D platformer where there will be multiple enemies, each with different spells they can cast. I'm trying to create an extensible system where I can easily create custom code for each spell (fire wave vs. bullet shooting toward enemy, vs AoE ability, etc.).

    Here's where I'm at right now:
    • I have an EnemyAttackComponent class (monobehavior) that checks if the enemy can attack, if it's in attack range, and also an abstract Attack class.
    • I have a SpellAttack class that I derives from the above AttackComponent class which overrides the Attack method (see below
    Code (CSharp):
    1. public class SpellAttack : EnemyAttackComponent
    2. {
    3.     public List<Spell> spells;
    4.     public int spellSelector;
    5.  
    6.     public override void Attack(GameObject target) {
    7.         if (CanAttack(target)) {
    8.             GameObject spellCast = Instantiate(spells[spellSelector].spellPrefab, transform.position, Quaternion.identity);
    9.         }
    10.     }
    11. }
    • I have a Spell ScriptableObject which contains the spell name, the spell prefab and the amount of damage the spell does. I read that I can put functions in here to override as well (such as movement), but I'm not sure if that's a good idea.
    • Finally, the prefab itself which has a script on it that controls the behavior of the used spell.
    My Question:
    I want to create multiple spells like I mentioned earlier. What is the best way to select which spell the enemy needs to attack? Surely there's a better way than a switch statement in the SpellAttack code above. Does the way I'm structuring this spell system sound okay? Is there anything you would change?

    Additionally, what about putting methods into my Scriptable Objects? In that case, I'd have to derive from that scriptable object class. Without deriving from monobehavior, how can I update the transform, etc.?
    For reference, here is my Spell SO. I do not currently use the virtual MoveSpell method right now, because my spell script itself relies on deriving from monobehavior to move:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [CreateAssetMenu(fileName = "New Spell", menuName = "Spell")]
    6. public class Spell : ScriptableObject
    7. {
    8.     public string spellName;
    9.     public GameObject spellPrefab;
    10.     public float damage;
    11.  
    12.     public virtual void MoveSpell(GameObject target) {
    13.  
    14.     }
    15. }
    16.  
    Thank you for the help! I'm just a few months into learning Unity and I've purchased a Udemy course on inheritance and composition. Hoping that will help me understand this more.
     
  2. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    There has been an on-going debate about how ScriptableObjects should be used, ever since they were introduced. Some people say that they should be read-only data containers, while others use them as "plug and play" logic handlers, which is very popular for AI programming, for instance. In a certain sense, there is no "right" and "wrong" in using them in a certain way.

    You seem to be aware of the different steps and the logic that your spell system requires, however, your actual classes have different and multiple responsibilities. Personally, I am a strong defender of the single-responsilibity principle, which means that objects should have as few as possible, if not just one, responsability. In programming, "Divide and Conquer" allows us to divide a problem into separate smaller problems and by solving these smaller problems, solve the original problem. This also makes unit testing your code a lot easier.

    An example is your
    EnemyAttackComponent
    . From your text, these are the responsibilities of this object:
    • Check if the enemy is in a state that they can attack (cooldown, is in attack range, ...).
    • Select and keep track of a spell that should be used to attack.
    • Instantiate the selected spell and use it to attack.
    That's a lot for a single object and makes it harder to debug and maintain it. Instead, consider this:
    Code (CSharp):
    1. public class Attack : MonoBehaviour
    2. {
    3.     public Validator castValidator;
    4.     public Selector spellSelector;
    5.     public Factory spellFactory;
    6.  
    7.     public void Attack()
    8.     {
    9.         if(castValidator.IsValid())
    10.         {
    11.             GameObject prefab = spellSelector.Select();
    12.             spellFactory.Create(prefab);
    13.         }
    14.     }
    15. }
    This looks very similar at first, but see how the logic is capsulated into their own components. Now the object only knows the logic of executing an attack using a spell, but it no longer needs to handle all the different parts itself. Furthermore, we can re-use these components everywhere as we see it appropriately.

    This brings me to my next point. Unity is built around the component pattern and shines at it. Using inheritance can be helpful, but going for it entirely usually means discarding many benefits of the component pattern and thus often working against the engine instead of with it. In my previous example code, all the classes could inherit from
    MonoBehaviour
    , allowing you to drag them around in the inspector, reference them in other components and piece your attacks (and spells) together with great freedom. If you use a SO and instances of it, however, you will be forced to hardcode the behaviour entirely into that SO definition. In particular the single-responsibility principle will be quickly discarded this way, because it would require you to create more and more SO definitions and instances (one for spell name and description and so on).

    If I look at your
    Spell
    , it looks like the SO handles moving the spell. Why don't you add this behaviour to the spell prefab, like a
    Move
    inherting from
    MonoBehaviour
    , for instance? Also consider that in your case, all spells deal damage, but do they? Consider a spell that adds a debuff, one that instead knocks the player back, you would have to write a SO definition for every single spell kind. One can do that, but what do you win from it in comparison to having components on the spells themselves, such as
    Move
    ? SOs can be very powerful, however, if you have to create objects with so many details, using prefabs with compontents representing these details is usually the smoother way.