Best way to implement skill systems in RPGs

Hey guys, looking for some advice or maybe just some reassurance. I want to have a skill system that’s fairly standard: cooldowns, buffs, debuffs, damage, heals etc as well as different tiers of each skill. I also need the player to be able to swap these skills in and out as they will only be able to have a certain number active at one time. They’ll need to derive from Mono because I plan to have unique animation data, particle effect etc and I want to be able to easily edit these things in the editor. The only way I can see this working well is having separate scripts for each skill under an interface or a base class and using addcomponent/removecomponent as they swap them in and out, keeping a list all of the skills they have to select from via an ID but I’m not crazy about adding and removing components during runtime. I know I could just put all skill scripts on the game object and just enable/disable as I see fit but that will get really messy in the editor and seems unnecessary to have that massive amount of scripts on each unit when most aren’t even being used. I can’t just have a general list that all the units can pull from by itself because of tiers and cooldowns. Scriptable Objects won’t work because they change the data permanently and for all instances. I feel like there’s got to be a better way but it just eludes me. Any advice?

Not really sure if this helps, but the Item & Inventory System | GUI Tools | Unity Asset Store asset kind of does what you want… But the main problem I would conclude you would encounter would be that this focuses on many other things other than the skills (crafting, inventory, fight system, etc), so you would have to delete those.

1 Like

You can definitely use scriptable objects, you just need to separate the immutable data from the mutable data.

A common pattern is to have the SO provide some some form of instanced data (a plain class) that gets used by a run time monobehaviour. That, or you separate the of logic of ‘cooldowns’, etc, outside of the scriptable objects and only handle that on the runtime side of things. The SO’s can just be sources of immutable data.

1 Like

I could see this being good to make “blueprints” so to speak but even doing it this way I don’t see how I could get around the add/remove components for the dynamic data as they swap in and out and having to make sure I’m referencing the correct SO and having to go to a 2nd source just to access the non-dynamic data seems like just as much or more work honestly and more likely prone to error.

Well you’d still need a way to author the data side of things, and since you can’t (easily) author the data of lots of monobehaviours (save hacking it with prefabs), scriptable objects are your friend here.

Basically don’t be afraid to use plain classes alongside monobehaviours. They act as fantastic glue between objects.

Here’s some roughed up code to explain what I mean:

//defines the base methods of a skill
[System.Serializable]
public abstract class SkillBase
{
    public abstract bool ProcessSkill(Monobehaviour container);
  
    public abstract SkillBase GetSkillInstance();
}

//simple skill that heals over time
[System.Serializable]
public class SkillHealOverTime : SkillBase
{
    [SerializeField]
    private float _duration = 10f;
  
    [SerializeField]
    private float _healAmount = 5f;
  
    private float _timePassed = 0f;
  
    public override bool ProcessSkill(Monobehaviour container)
    {
        //get player health component and add _healAmount to it
      
        _timePassed += Time.deltaTime;
      
        return _timePassed > _duration; //returns true when skill has finished
    }
  
    public override SkillBase GetSkillInstance() => return new SkillHealOverTime(this);  
  
    #region Constructors
  
    public SkillHealOverTime() { }
   
    //constructor that provides a copy
    private SkillHealOverTime(SkillHealOverTime hot)
    {
        _duration = hot._duration;
        _healAmount = hot._healAmount;
    }
  
    #endregion
}

//base scriptable object class that defines a skill as an asset
public abstract class SkillObjectBase : ScriptableObject
{
    public abstract SkillBase GetSkillInstance();
}

[CreateAssetMenu(menuName = "Skills/Heal Over Time")]
public class SkillObjectHealOverTime : SkillObjectBase
{
    [SerializeField]
    private SkillHealOverTime _skillHealOverTime = new SkillHealOverTime();
  
    public override SkillBase GetSkillInstance() => _skillHealOverTime.GetSkillInstance();  
}

public class SkillManager : Monobehaviour
{
    private List<SkillBase> _activeSkills = new List<SkillBase>();
  
    private void Update()
    {
        for(int i = 0; i < _activeSkills.Count; i++)
        {
            SkillBase skill = _activeSkills[i];
          
            bool skillFinished = skill.ProcessSkill(this);
          
            if (skillFinished) //if skill is finished
            {
                i--; //maintain the current index
                _activeSkills.Remove(skill); //remove skill from active skills
            }
        }
    }
  
    public void AddActiveSkill(SkillBase skill)
    {
        var skillInstance = skill.GetSkillInstance();
      
        _activeSkills.Add(skillInstance);
    }
  
    public void AddActiveSkill(SkillObjectBase skillObject)
    {
        var skillInstance = skillObject.GetSkillInstance();
      
        _activeSkills.Add(skillInstance);
    }
}

Probably errors, wrote this on notepad++. But in the end you only need the one monobehaviour to govern skills, and you get ‘copies’ from scriptable objects.

As you can see there’s a bit of fart-arse-ing around with a lot of derived types. This can be simplied by use of the SerializeReference attribute, which lets you serialise polymorphic data into the one field. So rather than making classes that derive from SkillObjectBase, you would just instead have:

[CreateAssetMenu(menuName = "Skills/Skill Object")]
public class SkillObjectBase : ScriptableObject
{
    [SerializeReference]
    private SkillBase _skill;
  
    public SkillBase GetSkillInstance() => _skill.GetSkillInstance();
}

Unity doesn’t support the inspector side of this by default, but there are free and paid options (such as Odin Inspector) that let you use this (much better) workflow.

1 Like

Thank you! That looks like a pretty good way of doing it. Better than my idea anyway. It’s true I’d be having to pull from multiple sources even with my original idea. I’ll give it whirl and see how it goes.

I can’t figure out what’s the problem here. How is a skill different from another function in terms of programming? I think your mistake is that you are trying to solve an abstract problem and you have no experience in games. Itself just run some kind of game and just mindlessly copy. And do not do the whole system at once.
As I did, I have a character class, absolutely everything is written there, skills, formulas, the object itself, what kind of equipment, melee or ranged, levels, etc. Most of the variables are dependent on other variables, and they include “get”, in this “get”, formulas, conditions, etc., I still cannot understand why the “set” is needed. I can’t figure out why to use it, but it doesn’t matter.
That is, this class is essentially a garbage can-designer. An instance of this class is already a unique character, and through “bool” variables you enable or disable skills. This is very reminiscent of nasty Skyrim, most likely it was done there, and probably in the entire Elder Scrolls series, but it is already clearly visible there. When you change any “bool” you activate the cycle for the skills to make the skill visible or invisible.

It’s mostly different due to cleanliness reasons. Typically anything that is object specific I can just slap the script on the object but with skills, there are so many that it becomes a mess doing it that way and unlike something like items and weapons which remain static, I can’t just simply pull from a default list of scripts because skills will have dynamically changing variables in those scripts specific to the object using them. I just wanted to make sure I was doing it in an optimal way before just diving straight in.

You won’t be doing it correctly the first time unless you get exceedingly lucky.

Today you simply do not know the full problem space your game will need to work with.

That’s how engineering works.

You engineer, you observe, you test, you refine, you refactor.

You can try and box everything up in a massive crazy hierarchy of little cute classes early on, which is a common newbie mistake.

Then you end up with extremely brittle hard-to-refactor code because every time you change something you are unable to reason about who else it might affect.

I suggest instead an iterative development approach where you engineer a bit, review, perhaps refactor, engineer some more, etc.

Each step of the way should strive to never repeat yourself, have a single point of truth, keep similar systems orthogonal, use as few interfaces and layers of inheritance as possible and above all:

Make sure you have fun making a fun game.

If you go crazy with all the OO and fail to do that final point, well, none of it matters I guess.

3 Likes

No, it’s no different. Whether you pick up a bow or activate a long-range attack skill, it makes no difference. Only the bow is in the weapon slot and the skill in the skill slot. Weapons in many RPG have many stats that can be downgraded or upgraded. I wrote to you about “get”, with the help of it I make the changes dynamic. Sample code.

public bool light_weapons
        {
            get
            {
                if (weapon == false) return true;
                else
                {
                    if (light_weapons_ecipirovan == true | light_weapons_ecipirovan_vtor == true)
                    {
                        return true;
                    }
                    else return false;
                }
            }
        }

Here on the forum they already wrote there is no right or wrong way, there is a working or not working way.