How to handle ability upgrades in a roguelike

Hello!
I am currently working on a roguelike and I have started on an upgrade system but I feel like there is a better way of doing things.

This is what I want to accomplish:

I want the player to have a certain number of skill slots that can be filled with different skills over the course of the run. Imagine it like a build your own MOBA character.
Then I want to be able to upgrade these skills in unique ways.
One of the skills is a straightforward projectile that flies from the character in the direction of the cursor that deals damage when it hits an enemy and disappears either when it hits the enemy or travels max distance.

I want it to have three different upgrade paths. The first path splits the projectile into three projectile while reducing the damage. The second path makes it so that the projectile can penetrate x number of targets before disappearing. The third path is for the projectile to seek out the nearest enemy.
I want these paths to be exclusive so when you have chosen one of these three you cant choose the other two.

However I also want it to be that there are generic upgrades for the entire skill, such as applying bleeding on hit but also special upgrades for the different paths such as increasing the damage based on amount of enemies penetrated when it comes to upgrade path 2.
The issue that I am having is making this system scalable and modular.

The current way I have solved it:

I have a abstract skill superclass that handles the cooldown of the skill and has an Activate() method that happens when you press the assigned key. I have my different skills inherit from this class and then I just add the functionality to my Activate() method to do what I want. In the case with the projectile I instantiate a prefab that has everything it needs attached to it and the projectile flies like it should.

I have another class attached to the player object that handles the different abilities and upgrades. Basically I have an array with available skill and upgrades which has a class that has the script names of the abilities or upgrade names together which upgrades should become available next and which upgrades to remove. Then when the player chooses that upgrade/ability it gets moved to a current skills and upgrades array. It also adds any new upgrades that become available after picking that upgrade/skill.
Then in the projectile class I check the array if I have that current upgrade as a string.

Even though this method works it is not very scalable. As you can probably tell that means that if I have the projectile as I mentioned previously the projectile script needs to cover all the possible outcomes such as either just going straight forward or honing in on an enemy or penetrating extra targets. Or the skill script needs to accommodate for spawning extra swords if I have that upgrade even though for two of the upgrades that is completely unnecessary!

What is a better way of doing a system like this? Do you guys have any recommendations?

Sorry for the long post and thank you in advance!

4 Answers

4

I tried doing systems like this a couple of times , the best advice i can think of is :
“your code shouldn’t be logically a reflection of your abilities”
What i mean by that can be interpreted in many ways , for example:

  • If you have ability A that can get upgraded into ability B , that then can get upgraded in ability C , that doesn’t mean that somewhere in your code there needs to be something like this
// this is an example of what NOT to do
public class AbilityA : IAbility {} // base ability implementation

public class AbilityB : AbilityA {} // imagine each class overrides the previous one

public class AbilityC : AbilityB {} // same here

If you want your abilities to be generic and able to change drastically, then make the code generic and loose.
Most ability interfaces i made only needed an interface like this

public interface IAbilityData
{
         public string Name {get;}
         public void OnSetup(GameObject owner);
         public void OnUse(GameObject owner);
         public void OnCleanup(GameObject owner);       
}

Usually i make it a scriptableobject and leave mixing and matching to the inspector , something like

public abstract class AbilityEffectBase : ScriptableObject
{
         public void OnSetup(GameObject owner);
         public void OnUse(GameObject owner);
         public void OnCleanup(GameObject owner);
}
// example for a projective effect
public class ProjectileCastEffect : AbilityEffectBase 
{
      private GameObject projectilePrefab;
      void OnUse(GameObject owner) { // todo : instantiate and launch the projective }
}
public class AbilityData : ScriptableObject , IAbilityData
{
         [SerializeField]
         private string abilityName;
         public string Name => abilityName;
         private AbilityEffectBase[] effects;
         public void OnSetup(GameObject owner) {
             effect.Foreach(e => e.OnSetup(owner));
         }
         public void OnUse(GameObject owner) { 
             effect.Foreach(e => e.OnUse(owner));
         }
         public void OnCleanup(GameObject owner) {
              effect.Foreach(e => e.OnCleanup(owner));
         }
}

obviously, the previous example is simple but you get the idea , give every ability the chance to setup itself and manage it’s functionality and cleanup , instead of having a “common projectile interface” and a “common buff interface” , leave the detailed implementation to the ability itself , keep the interface lean.
Also use the inspector to make you abilities from scriptableobject lego-pieces.

  • About managing upgrading abilities , don’t overthink it , ability A and B don’t have to be related in code , it can literally be represented like this
public class AbilityProgress 
{
     public IAbilityData[] allProgressions;
    // to upgrade , just find the index of "currentProgression" and pick the next element as the "upgraded ability"
     public IAbilityData currentProgression;
}

and that’s it , if the progression logic is more involved , you can make a progression tree where you define the conditions and the paths to take.
These kinda topics can take a long time to discuss and iron out , so feel free to ask if something is not clear or wanna discuss this further

To answer the second part of the post :
you can look at ScriptableObjects in this case as “a unity assets that provides a method (OnUse)” , and your job is to mix and match a bunch of these methods to create your ability.
As for how to use it , depends on your setup but the idea is straight forward , here’s a typical example:

Whenever you spawn your player , pass the collection of AbilityDatas to the Monobehaviour on the player that’s supposed to be managing the abilities (let’s call it AbilitiesController)
Your AbilitiesController should keep that collection of datas inside of it and spawn the necessary buttons per abilities.
for example let’s say your button UI script looks like this

public class AbilityButtonUI : MonoBehaviour 
{
      private GameObject owner;
      private AbilityData data;

      // AbilitiesController should spawn a button and call this per AbilityData
      // pass the player's gameobject (owner) and the ability itself (data)
      public void Initialize(GameObject owner, AbilityData data)
      {
             this.data = data;
             this.owner = owner;
      }

      // called when the player clicks the ui
      public void OnClick()
      {
           // calls the method in the data
           data.OnUse(owner);
      }
}

and for the projectile i would create a ScriptableObject and have it in the list of ability effects , like this

public class ProjectileCastEffect : AbilityEffectBase 
{
      private GameObject projectilePrefab;

      // this would be called from the OnClick method , since it calls OnUse , which will call this method
      void OnUse(GameObject owner) 
      {
             // create the projectile and do what's necessary to launch it
             var proj = Instantiate( projectilePrefab , owner.transform.position , owner.transform.rotation);
      }
}

Also , a note about “OnSetup” and “OnCleanup” , these can vary based on your design , personally i call them on player spawn and despawn to do some initialization and one-time variable caching , what i suggested isn’t a complete idea but rather a vague shape of what i would consider a flexible ability system , so consider your desgin/context.

Thank you so much for your answer, this is super interesting and this topic indeed takes a lot of time to figure out :sweat_smile: .
I have used ScriptableObjects a little bit before. I have started implementing test code now just to see how it would work, I was thinking just to start making a basic projectile just to get started.

The main question that I am left with is maybe an easy one, but it is how do I actually use the ScriptableObjects. For example, how do I fire the projectile based on input?

Do I have a MonoBehaviour script and in that script reference the AbiltyData ScriptableObject and run OnSetup, OnUse, OnCleanup on button click?

Okey I think I get it.
How would I go about if I wanted for example add a bleed effect to the projectile?

I already have a system that deals with status effects that are attached to each enemy.
So what I do is that I activate the StartBleed method when the projectiles hits the enemy. How do I differentiate a projectile that has not been upgraded vs an upgraded one to know if I should apply the bleed effect?
And if I have many similar upgrades like slowing, making the enemy vulnerable etc. How do I make them stack on the projectile?

Also if I want the player to be able to pick from a pool of all the different available upgrades/abilities, and add/remove upgrades to the pool based on what abilities/upgrades already exist, is there a good way of doing that?
Previously I have made it so each ability/upgrade has an array of what abilities/upgrades that it should remove or add from the current pool when picked.

Thank you for all your helpful information!