Search Unity

How did YOU design your ability system?

Discussion in 'Scripting' started by Deleted User, Sep 16, 2015.

  1. Deleted User

    Deleted User

    Guest

    Just curious how people are doing everything.
     
  2. Korno

    Korno

    Joined:
    Oct 26, 2014
    Posts:
    518
    The first time I went crazy with inheritance and my class heirachy got soo deep it was stupid. So be careful about that.

    Second time after tatooing "has a / is a" to my eyeballs it was much better.

    Base ability class which contained core info - cost, cooldown etc.

    Subclssses were Offensive, Self and Defensive. These all had different targetting logic so were their own classes.


    Then each ability had a list of effects. Effects were subclassed into instant and overtime and contained information about what property they effected - health, speed etc and how they effected it and special effects to use.

    Worked well.
     
  3. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    I've posted two prototypes of skill systems: one using ScriptableObjects and another using MonoBehaviours and prefabs.

    In both cases, they use component-based design and avoid inheritance. As Korno mentions, inheritance creates headaches for this kind of system. Say you create a Defensive subclass with a specific type of targeting logic. What if you want to add a new skill that uses Offensive targeting logic, such as a Hypnotize skill that targets an enemy and makes it come to your aid? All of the sudden you're stuck because you're locked into an inheritance tree. It would be much easier to simply add a "targeting logic: enemy" component to the skill.

    I also like plyGame's skill system. Leslie did a similar thing in plyGame but backed it with a nice custom editor.
     
  4. Deleted User

    Deleted User

    Guest

    Nice. II had seen the component based one but not the scritableobject based one. In both cases, how did you handle delayed, repeating, and conditional abilities? For example, if you wanted an ability to heal the target if its an ally but damage it if it's an enemy? Etc.
     
  5. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    I'd once again go component-based. For example, say you already have "Use" components for Heal and Damage. If you're creating a Fireball skill, you'd plug the Damage component into the Fireball's Use slot.

    Now say you're creating a new Divine Light skill that heals Good targets and damages Evil targets. Define a new component called something like Branch, with three slots:
    Code (csharp):
    1. public class Branch : SkillEffect {
    2.  
    3.     public Categorizer categorizer;
    4.     public SkillEffect[] branches;
    5.  
    6.     public override void Use(Transform target) {
    7.         var category = categorizer.GetCategory(target);
    8.         branches[category].Use(target);
    9.     }
    10. }
    11.  
    12. //==============================
    13. public abstract class Categorizer {
    14.     public abstract int GetCategory(Transform target);
    15. }
    16.  
    17. //==============================
    18. public class AlignmentCategorizer : Categorizer {
    19.     public override int GetCategory(Transform target) {
    20.         return AlignmentSystem.IsGood(target) ? 0 : 1;
    21.     }
    22. }
    23.  
    Create a Branch component named Divine Light. In its branches array, set element 0 to Heal and element 1 to Damage. Create an AlignmentCategorizer component, and set the Branch component's categorizer to it.

    So when Branch.Use is invoked, it doesn't actually do a skill. Instead, it gets a category integer from its categorizer component (in this case, category 0 is Good, category 1 is Evil). Then it calls the branch corresponding to the category (e.g., if the category is 1, it calls branches[1], which is Damage).

    For delays, you could swap out the targeting component with a component that only returns a valid target after the delay period is over.
     
    Deleted User likes this.
  6. Deleted User

    Deleted User

    Guest

    Not a big fan of either solution.

    As for me, i initially I went the ScriptableObject route (and of course components) but I ran into a couple issues that made me think monobehaviours would be a better idea but after trying that approach I think I'll go back to ScriptableObjects again.
    I thought monobehaviours would be better because it makes sense for an ability to be attached to a gameobject but then I realized it only really works for simple abilities. For example my projectile ability is a composition of 3 abilities: spawn, setup, and throw. So I created a "scroll" game object to create my abilities but then I realized I wouldnt be able to use monobehaviors to their full potential since everything would be based on the scroll gameobject its attached to and not the caster.
    I would need to pass in the game object casting the ability or add a caster field to my ability effects and do everything through that. I thought it would be easy to forget to call "caster.transform" or "caster.get component" every time for everything and so I don't see many advantages going the monobehavior route save for convenient events and drag & drop functionality which I would love to replicate. This was just 1 problem I had with monobehaviors though.
    One big problem I had with my Scriptable Object approach was that I had things like delayed action or repeating which inherited from action and had a field for action so you could have several nested actions which made it hard to modify nested actions. I think one way to solve this would be for all actions to be created by the ability and be registered with the ability so I can do something like "get ability component" kinda like game object and behaviours but abilities and actions.
     
  7. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    I agree with your analysis of the challenges of using MonoBehaviours. You could get around it by having them register with a manager/dispatcher/coordinator component on the main GameObject -- or by using prefabs, but that's just mimicking ScriptableObject assets.

    For delayed and repeating actions, what about using the same approach I described above for conditional actions that branch to different things based on the target?

    For example, for delayed actions:
    Code (csharp):
    1. public class DelayedEffect : SkillEffect {
    2.  
    3.     public float delay; // When "using," delay this long...
    4.     public SkillEffect skill; //... then actually use this skill.
    5.  
    6.     public override void Use(Transform caster, Transform target) {
    7.         caster.StartCoroutine(UseSkillOnDelay(caster, target));
    8.     }
    9.  
    10.     private IEnumerator UseSkillOnDelay(Transform caster, Transform target) {
    11.         yield return new WaitForSeconds(delay);
    12.         skill.Use(caster, target);
    13.     }
    14. }
    Admittedly the code I'm posting is simplified to make it easier to see the core idea, but any extra code would just be regular management and safeguard stuff.
     
    Deleted User likes this.
  8. Deleted User

    Deleted User

    Guest

    Some good info in this thread. Anyone else? :)
     
  9. Rokkimies

    Rokkimies

    Joined:
    May 18, 2015
    Posts:
    12
    Depends. For the last project I coded the basic functionalities into character controller, (shoot, move). For my game player would get new abilities through items, so I would add the code for control over ability to that item itself, so the player would get the ability with the item. My project is frozen (don't have the rights for it anymore, actually) so I'm not sure if I would give all the items and abilities to player and enable/deactivate them when needed or obtained or just instantiate the item under character itself when obtained.

    The reason I did it this way, was to be able to give enemies the same abilities or "weapons" while not having a need to modify them too much to work. Other reason is that some of the weapons were melee and swinging worked through mouse movement. This way I would have the item in the right position and rotation and the abilities and weapons would be nicely reusable and editable for other projects.

    I like having my code as loose bricks which all have their function and the functionalism would be easily carried over to another project. Not so good description, but feedback is welcome.
     
  10. grimprophecy

    grimprophecy

    Joined:
    Apr 18, 2013
    Posts:
    2
     
  11. Dorscherl

    Dorscherl

    Joined:
    May 29, 2017
    Posts:
    26
    Seems like a solid suggestion. How would an ability that you channel look like using this? Say I want a fireball ability/spell and the longer you hold it the stronger it is.
    How about effects that trigger with an event. With the fireball spell, if it hits something, light it on fire or explode. What if the fireball cast is interupted or canceled, I want to penalize the caster maybe.
    In my game I want abilities to be the focus of game play, it will change the movement states, allow for state modifiers to be applied, fire weapons, use items. Maybe I'm thinking too broad.
     
  12. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    You can still follow the same approach. I recommend working out the basics on paper, then implement it in the cheapest way possible -- no graphics, use Debug.Log() lines instead of actual effects (e.g., log "Do 45 damage" instead of actually inflicting 45 damage on a target), etc. This will give you insights to refine your approach again on paper. Repeat until you're satisfied with the design, then implement it in your project.