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. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Ability Slots System

Discussion in 'Scripting' started by FFaUniHan, Jan 24, 2019.

  1. FFaUniHan

    FFaUniHan

    Joined:
    Feb 26, 2018
    Posts:
    14
    Hello! I wanted to make a very modular ability system similar to DOTA 2 Ability Draft mode. In this mode, each player only has 4 ability slots that can be filled by choosing from a wide pool of abilities. The hotkey to activate these abilities always the same: Q for ability 1, W for ability 2, E for ability 3, and R for ability 4. Each ability has a completely different script and behavior and NOT just a variation in numbers.

    Goal:
    I expected to create a Player object with 4 slots starts empty at the beginning of the level. The player will find new abilities and equip it to these ability slots. They can also remove them and move it to another slot if they want to. To activate these abilities, the player needs to press key 1, 2, 3, or 4.

    Implementation:
    I'm thinking of creating separate scripts for each ability. Then somehow creating a reference to these scripts in my Player inspector. A script in Player gameobject will handle inputs of 1-4 keys and trigger the ability according to the slots.

    Problem:
    I somehow can't reference Ability scripts in the Parent inspector.

    What I've tried:
    I tried to create an abstract PARENT class for my abilities, where a function called "ActivateAbility()" is then being overwritten by unique abilities. In theory, this should allow me to just call ActivateAbility() to run the function. I'm guessing ScriptableObject is what I'm looking for, but as far as I know, you also can't reference a script in a ScriptableObject.

    Any help is appreciated.
     
  2. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,835
    I'm a little unclear on exactly where you're getting tripped up, but it sounds like what you probably want to do is something like this:

    1. Create a base class for all abilities (hereafter Abililty) that inherits from ScriptableObject and includes a method called Activate(). (Rather than making this abstract, I would make it a valid ability that does nothing when cast, but abstract would probably work.)
    2. Create a series of subclasses that implement specific kinds of abilities, like Heal or Shoot, inheriting from Ability and overriding Activate() with suitable behavior.
    3. Create scriptable objects based on these subclasses in your assets directory, give them meaningful names, and fill in their data fields as appropriate.
    4. Create a class that inherits from MonoBehaviour with a public field of type List<Ability>
    5. You should be able to drag abilities from your assets folder to that list in the inspector
     
  3. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    If you want to keep these as separate scripts that don't inherit from a base class, you could have each of the 4 ability scripts assign its own ActivateAbility() method as a delegate in this parent object script. You could establish a reference to the parent object script on the ability script in the inspector, or have it find the parent object script in its Awake method. Then you set the delegate in the awake method as well.

    Not saying this is better at all, but something you could consider.
     
  4. FFaUniHan

    FFaUniHan

    Joined:
    Feb 26, 2018
    Posts:
    14
    I was thinking about this method before. But wouldn't that make each ability subclasses to only have one ScriptableObject? And if so, wouldn't the [CreateAssetMenu] will be filled with these one-time-use scriptable objects?
     
  5. DeeJayVee

    DeeJayVee

    Joined:
    Jan 2, 2015
    Posts:
    121
    I'm sorry, I made this response on the fly, it is a mess. Here, I have cleaned it up.

    I think what you want to achieve is very simple and can be done like this.

    Two scripts. One is the BaseAbility script, it is used as a template similar to ScriptableObjects.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public enum EffectType
    6. {
    7.     SingleTargetDamage,
    8.     SingleTargetHeals,
    9.     AoeDamage,
    10.     AoeHeals
    11. }
    12.  
    13.  
    14. [System.Serializable]
    15. public class BaseAbility{
    16.  
    17.     public string name, description;
    18.  
    19.     public int cost;
    20.  
    21.     public float castTime;
    22.  
    23.     public EffectType effectType;
    24.  
    25.     public GameObject spellPrefab;
    26. }
    Adding the [System.Serializable] to this script allows it to be shown in the editor when called.

    The second script is your AbilityList, now you could modify this script to do a whole plethora of things using switch/if statements corresponding to your BaseAbility script.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class AbilityList : MonoBehaviour {
    6.  
    7.     public List<BaseAbility> ability = new List<BaseAbility>();
    8.  
    9. }
    if you add that script to a GameObject you'll see your ability list.

    you could do things such as;

    Code (CSharp):
    1.  
    2.    public Transform mouseLocation;
    3.  
    4.     public void Cast(int a)
    5.     {
    6.  
    7.         switch (ability[a].effectType)
    8.         {
    9.  
    10.             case EffectType.AoeDamage:
    11.                 Instantiate(ability[a].spellPrefab, mouseLocation.position, Quaternion.identity);
    12.                 break;
    13.  
    14.             case EffectType.AoeHeals:
    15.                 Instantiate(ability[a].spellPrefab, mouseLocation.position, Quaternion.identity);
    16.                 break;
    17.         }
    18.      
    19.     }
    You would only have to reference this one script with a spell id number which is its number in the List and this script could handle everything.

    It would look something like this in the editor;



    I hope this helps.

    Edit:

    You said:

    "I'm thinking of creating separate scripts for each ability. Then somehow creating a reference to these scripts in my Player inspector. A script in Player gameobject will handle inputs of 1-4 keys and trigger the ability according to the slots."

    Well depending on what you want to do or how you're gonna have people move spells to the bar or whatever it could be different.

    For this I'll assume you'll have different Classes or Heroes with differing available abilities.

    each "class" could have it's own ability list but inside the BaseAbility script you could have a bool that says whether it's available or not, or a level at which it unlocks and becomes available, a cost to buy it.

    You could also add a sprite image for a spell button, so you could have a series of "buttons" that use the variables from your List to show the Sprite/name/cost/abilities.

    a bit of psuedo code for your player would be;

    Code (CSharp):
    1. if(choice(xButton)
    2. {
    3. buttonX.spellID = spellID;
    4. }
    if your player chooses to add that spell to the button, then that your BaseAbility script has all the information needed to do that.
     
    Last edited: Jan 25, 2019
  6. FFaUniHan

    FFaUniHan

    Joined:
    Feb 26, 2018
    Posts:
    14
    That was rather smart actually, and really heading straight to what I'm looking for. But what if I wanted to add a completely new ability behavior? From this code, I can only see that it's only just a variation of several components, like "Heal Area", "Heal Unit", etc.
     
  7. DeeJayVee

    DeeJayVee

    Joined:
    Jan 2, 2015
    Posts:
    121

    Could you give me an example of what you mean? Maybe give me a spell idea or something to work with

    EDIT: I think I have an idea of what you mean. So the AbilityList would be your way of keeping your spells organized and what not. You could change the Enum in ability List to have only "AoE, ST" to tell the game where to start the spell prefab.


    But now say you need spell behaviour? Well, you would make a third script to go on your spell prefabs.

    Each spell would need say

    *Movement // Does it move towards the target, does it hover for a bit then hit the ground?

    *What kind of damage or healing does it do, if any? does it tick every second or does it do it in one big go
    does it make the character dizzy so they spazz out for a few seconds?

    *Lifetime - how long does the spell last? If you throw it, how far will it reach?

    I would have something like this on my spell prefabs.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public enum SpellMovement
    6. {
    7.     MoveTowardsTarget,
    8.     MTTThenHover,
    9.     BounceBetweenEnemies,
    10.  
    11. }
    12.  
    13. public enum State
    14. {
    15.     Dizzy,
    16.     Blind,
    17.     Rage,
    18.     Calm
    19.  
    20. }
    21.  
    22.  
    23. public enum StatMods
    24. {
    25.     GainHealth,
    26.     ReduceHealth,
    27.     ReduceStat
    28.  
    29. }
    30.  
    31. public class SpellBehavior : MonoBehaviour {
    32.  
    33.     public SpellMovement spellMovement;
    34.     public State[] State;
    35.     public StatMods[] statMod;
    36.     public float lifeTime; // How long the spell lasts
    37.  
    38.     public int power; //How much Damage or Healing it does when it hits the target, if any.
    39.  
    40.     public Transform target;
    41.  
    42.  
    43.  
    44.     void Start () {
    45.         Movement();
    46.     }
    47.  
    48.  
    49.     void Update () {
    50.      
    51.     }
    52.  
    53.     void Movement()
    54.     {
    55.  
    56.         //Pseudo Code
    57.         switch (spellMovement)
    58.         {
    59.             case SpellMovement.MoveTowardsTarget:
    60.                 //While(collider.contacts !contain (target.tag)
    61.                 //{
    62.                 //gameObject.movetowards(target)
    63.                 //}
    64.  
    65.                 //ReachedTargetPoint();
    66.                 break;
    67.  
    68.             case SpellMovement.MTTThenHover:
    69.                 //While(collider.contacts !contain (target.tag)
    70.                 //{
    71.                 //gameObject.movetowards(target)
    72.                 //}
    73.                 //gameObject.move Vector3.up for seconds
    74.                 break;
    75.  
    76.             case SpellMovement.BounceBetweenEnemies:
    77.                 //While(collider.contacts !contain (target.tag)
    78.                 //{
    79.                 //gameObject.movetowards(target)
    80.                 //}
    81.                 //reachedTargetPoint()
    82.                 //targetcount++
    83.                 //target = target[targetcount]
    84.  
    85.                 break;
    86.         }
    87.  
    88.     }
    89.  
    90.  
    91.     void ReachedTargetPoint()
    92.     {
    93.         //Target.SetState(State)
    94.         //Switch statmod
    95.         //case Statmod.GainHealth
    96.         //target.GainHealth(power)
    97.     }
    98. }
    99.  

    Again, I'm just doing this quickly but the idea is that each spell while different does have similar properties when broken down. Have a template that covers all your bases would be what I would do rather than a personal script for each ability.
     
    Last edited: Jan 26, 2019
  8. FFaUniHan

    FFaUniHan

    Joined:
    Feb 26, 2018
    Posts:
    14
    This is getting even closer to what I intended! I guess I need to fully understand how this code runs. So say, if I created some ability behaviors script:

    1. Dash Forward and strike enemies
    2. Teleport between two points
    3. Perform Rocket Jump

    Where should I put these behaviors inside your script?

    I think we're approaching more towards game architecture rather than general scripting discussion...
     
  9. DeeJayVee

    DeeJayVee

    Joined:
    Jan 2, 2015
    Posts:
    121
    I've made this script much easier on the eyes in the editor and dropped it to two scripts.

    To make a Charge or Teleport script you would need to make a target variable and determine that the target is the player and then move it using translate or Rigidbody.velocity, things like that.

    I hope this helps you, feel free to ask any more questions.

    BaseAbilities :

    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6.  
    7. public enum CastType
    8. {
    9.     SelfCast,
    10.     SelfInstant,
    11.     FriendlyInstant,
    12.     DeadlyInstant,
    13.     FriendlyProjectile,
    14.     DeadlyProjectile,
    15.     FriendlyAoe,
    16.     DeadlyAoe,
    17.     Teleport,
    18.     Propulsion,
    19.     Charge
    20. }
    21.  
    22. public enum States
    23. {
    24.     Null,
    25.     Dizzy,
    26.     Blind,
    27.     Rage,
    28.     Calm,
    29.     Berserker,
    30.  
    31. }
    32.  
    33. public enum Modify
    34. {
    35.     Null,
    36.     IncreaseFlat,
    37.     DecreaseFlat,
    38.     IncreasePercentage,
    39.     DecreasePercentage
    40. }
    41.  
    42. public enum Stats
    43. {
    44.     CurrentHP,
    45.     MaxHP,
    46.     CurrentMP,
    47.     MaxMP,
    48.     Agility,
    49.     Intellect,
    50.     Strength
    51. }
    52.  
    53.  
    54. [System.Serializable]
    55. public class BaseAbilities
    56. {
    57.  
    58.     public string name;
    59.  
    60.     [TextArea][Space]
    61.     public string description;
    62.  
    63.     [Space]
    64.     public int manaCost;
    65.      
    66.     [Space]
    67.     public int lvlAvailable;
    68.  
    69.     [Space]
    70.     public float cooldown;
    71.  
    72.     public bool onCooldown;
    73.  
    74.     [Space]
    75.     public int power;
    76.  
    77.     [Space]
    78.     public bool ticking;
    79.     public float tickTime;
    80.  
    81.     [Space]
    82.     public float projectileLifeTime;
    83.  
    84.     [Space]
    85.     public float spellActiveTime;
    86.  
    87.     [Space]
    88.     public float teleportDistance;
    89.  
    90.     [Space]
    91.     public float chargeDistance;
    92.  
    93.     [Space]
    94.     public Sprite buttonSprite;
    95.  
    96.     [Space]
    97.     public GameObject spellPrefab;
    98.  
    99.     [System.Serializable]
    100.     public struct SpellControlList { public CastType castType; public Modify modify; public Stats stats; public States states; }
    101.  
    102.     [Space]
    103.     public SpellControlList spellControl = new SpellControlList();
    104.  
    105. }
    106.  

    Abilities :
    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class Abilities : MonoBehaviour {
    7.  
    8.     [SerializeField]
    9.     public List<BaseAbilities> ability = new List<BaseAbilities>();
    10.  
    11.  
    12.     //To cast a spell you would use Abilities.Cast[SpellIDNumber]
    13.     //It's very easy to access all the information from BaseAbility
    14.     //Using Switch Statements.
    15.  
    16.     //Here is an example from how I would set up a HoT on my player.
    17.     // I have also added a Cooldown Example.
    18.  
    19.     public void Update()
    20.     {
    21.         if (Input.GetKeyDown(KeyCode.L))
    22.         {
    23.             Cast(0);
    24.         }
    25.     }
    26.  
    27.  
    28.     public void Cast(int a)
    29.     {
    30.         if (ability[a].onCooldown)
    31.         {
    32.             return;
    33.         }
    34.    
    35.         switch (ability[a].spellControl.castType)
    36.         {
    37.        
    38.             case CastType.SelfCast:
    39.                 SelfCast(a);
    40.             break;
    41.            
    42.  
    43.         }
    44.  
    45.    
    46.    
    47.     }
    48.  
    49.  
    50.     public void SelfCast(int a)
    51.     {
    52.         if (ability[a].ticking == true)
    53.         {
    54.             switch (ability[a].spellControl.modify)
    55.             {
    56.                 case Modify.IncreaseFlat:
    57.                     ability[a].onCooldown = true;
    58.                     StartCoroutine(Cooldown(a));
    59.                     StartCoroutine(Ticking(a));
    60.                     break;
    61.             }
    62.         }
    63.         else { }
    64.     }
    65.  
    66.  
    67.  
    68.  
    69.     IEnumerator Cooldown(int a)
    70.     {
    71.         yield return new WaitForSeconds(ability[a].cooldown);
    72.         ability[a].onCooldown = false;
    73.     }
    74.  
    75.     //In this Coroutine you would increase the chosen stat by said amount, you'll have to add your own stat script and modify it through here.
    76.     //For now this will just give you a console log in increments of tickTime for the duration of the spell. So tickTime = how often it ticks
    77.  
    78.  
    79.     IEnumerator Ticking(int a)
    80.     {
    81.         float timer = 0;
    82.  
    83.         while (timer < ability[a].spellActiveTime)
    84.         {
    85.             yield return new WaitForSeconds(ability[a].tickTime);
    86.             timer += ability[a].tickTime;
    87.             Debug.Log(timer);
    88.            
    89.         }
    90.  
    91.  
    92.     }
    93. }
    94.  
    This is what my test HoT looks like;

     
    Last edited: Jan 28, 2019
  10. FFaUniHan

    FFaUniHan

    Joined:
    Feb 26, 2018
    Posts:
    14
    Well done! That's a lot of improvement! If I read through your script correctly, then this script is made specifically as a custom spell, right?

    I asked before about Dash ability and to my knowledge, Dash can't fit inside this script since it has a Translate function.

    So I need to ask if it's possible to put any other "Movement Related Abilities" inside your script. And if it's possible, where should I put the movements script inside your script. Thanks for your help!
     
  11. DeeJayVee

    DeeJayVee

    Joined:
    Jan 2, 2015
    Posts:
    121
    You can put anything inside of this script. I made a dash with it earlier using my own movement code.

    I populated your enum "CastType" in your BaseAbilities list with an assortment of types to choose from, you can add or remove your own. You could add "Dash" (Or use the already made "Charge" one)

    and in your Abilities script Cast method you would put something like this.

    Code (CSharp):
    1. case SpellType.Dash:
    2. player.transform.Translate(new Vector3(10 *Time.deltaTime, 0, 0);
    3. break;
    the Switch (Kind of like an if statement) is saying "if this spells CastType == CastType.Dash then do this;"

    Basically, your Enums at the top of your BaseAbility scripts are just names you could make them anything you want.

    And if you reference the right GameObjects like player, you can reference any of the scripts on your player (or enemies) and control those scripts (Such as stat scripts) including the transform or rigidbodies.


    So in your Abilities script you want to add a reference to your Player. and you want to move that player via translate. Cool. We add a GameObject variable called Target.

    In this case, I will assume your ability script is on your player.


    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. public class Abilities : MonoBehaviour {
    6.     [SerializeField]
    7.     public List<BaseAbilities> ability = new List<BaseAbilities>();
    8.     //To cast a spell you would use Abilities.Cast[SpellIDNumber]
    9.     //It's very easy to access all the information from BaseAbility
    10.     //Using Switch Statements.
    11.     //Here is an example from how I would set up a HoT on my player.
    12.     // I have also added a Cooldown Example.
    13.     public void Update()
    14. {
    15.  
    16.         if (Input.GetKeyDown(KeyCode.L))
    17.         {
    18.             Cast(0);
    19.         }
    20.     }
    21.     public void Cast(int a)
    22.     {
    23.        GameObject target;
    24.  
    25.         if (ability[a].onCooldown)
    26.         {
    27.             return;
    28.         }
    29.  
    30.         switch (ability[a].spellControl.castType)
    31.         {
    32.  
    33.             case CastType.SelfCast:
    34.                 SelfCast(a);
    35.             break;
    36.      
    37.             case CastType.Charge:
    38.             target = gameObject;
    39.             target.transform.Translate(new Vector3(target.transform.position.x + ability[a].chargeDistance, target.transform.position.y, target.transform.position.z);
    40.               break;
    41.  
    42.         }
    43.  
    44.  
    45.     }
    46.     public void SelfCast(int a)
    47.     {
    48.         if (ability[a].ticking == true)
    49.         {
    50.             switch (ability[a].spellControl.modify)
    51.             {
    52.                 case Modify.IncreaseFlat:
    53.                     ability[a].onCooldown = true;
    54.                     StartCoroutine(Cooldown(a));
    55.                     StartCoroutine(Ticking(a));
    56.                     break;
    57.             }
    58.         }
    59.         else { }
    60.     }
    61.     IEnumerator Cooldown(int a)
    62.     {
    63.         yield return new WaitForSeconds(ability[a].cooldown);
    64.         ability[a].onCooldown = false;
    65.     }
    66.     //In this Coroutine you would increase the chosen stat by said amount, you'll have to add your own stat script and modify it through here.
    67.     //For now this will just give you a console log in increments of tickTime for the duration of the spell. So tickTime = how often it ticks
    68.     IEnumerator Ticking(int a)
    69.     {
    70.         float timer = 0;
    71.         while (timer < ability[a].spellActiveTime)
    72.         {
    73.             yield return new WaitForSeconds(ability[a].tickTime);
    74.             timer += ability[a].tickTime;
    75.             Debug.Log(timer);
    76.      
    77.         }
    78.     }
    79. }
    80.  
     
    Last edited: Jan 28, 2019
  12. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,835
    Possibly. However, in most games, most of your abilities will be variations on a theme and can probably share the same code with different parameters. For instance, a fireball and a lightning bolt could be using the same code with different values for range, power, damage type, and special effect.

    If you want to be even more modular, spells might include references to other spells that are triggered under specific conditions. For instance, you could have a generic "projectile" that flies forward until it hits something, and then it automatically casts another spell at the point of impact, which could create an icy patch or a slowing debuff or a ricochet projectile depending on what that second spell is.

    A game where the player can theoretically learn a thousand abilities might only need unique executable code for a few dozen.