Unlocking Abilities

Hello all, I have finally completed the final instalment of the RPG series Shops & Abilities.

At the end of the course I vowed that I would set up a talent tree system, or rather, a skill tree similar to that of Diablo 2 and other late 90s to early 2000 RPGs. Generally these RPGS consisted of a row of skills that had dependencies / prerequisites on certain skills before they could be unlocked. The skill tree also allowed the user to dump multiple points to gain more benefits from each individual skill.

Firstly I created a script called ‘Skill’ and inherited from Ability.cs

    public class Skill : Ability
    {
        [Header("Skill Configurations")]

        /// <summary>
        /// A Skill's current level. A level depicts how effective of an ability can become, maximizing it out allows the player to deal extraordinary damage or support.
        /// </summary>
        [SerializeField] int skill_Level;
        /// <summary>
        /// The maximum amount of skill points that can be attained from any Skill for the player.
        /// </summary>
        [SerializeField] int skill_CapLevel;
        /// <summary>
        /// Represents a unique identification, which is required to obtain data about any particular Skill.
        /// </summary>
        [SerializeField] int skill_ID;
        /// <summary>
        /// A collection of integers to determine whether a Skill has Dependencies before it can be unlocked.
        /// </summary>
        [SerializeField] List<int> skill_Dependencies = new List<int>();
        /// <summary>
        /// A boolean variable for gatekeeping a Skill locked if it's prerequisites are not met.
        /// </summary>
        [SerializeField] bool skill_Unlocked;
        /// <summary>
        /// Amount of player skill points required to unlock this Skill.
        /// </summary>
        [SerializeField] int _skillRequiredPointsToUnlock;

        public int SkillLevel { get { return skill_Level; } set { skill_Level = value; } }
        public int SkillCap { get { return skill_CapLevel; } set { skill_CapLevel = value; } }
        public int SkillID { get { return skill_ID; } set { skill_ID = value; } }
        public List<int> Dependencies { get { return skill_Dependencies; } set { skill_Dependencies = value; } }
        public bool Unlocked { get { return skill_Unlocked; }  set { skill_Unlocked = value; } }
        public int RequiredPointsToUnlock { get { return _skillRequiredPointsToUnlock; } set { _skillRequiredPointsToUnlock = value; } }
     }

These configurations will dictate exactly how our skill works. How many levels does it currently have? Does it require 5 points to get the next level up? What is the maximum level? Is it already unlocked? Does it require other skills to be unlocked before it, itself can be unlockable? The id will be our way to grab the reference of any skill inside of a Dictionary.

Now you should be able to create your first Skill. Ensure you’re using a different CreateAssetMenu then the typical ‘Ability’ inside your Skill.cs

The most crucial part about this particular Skill set up is that the ‘Skill_ID’ needs to be unique. It cannot share any number of any other skill, so you can only have one skill use ‘0’. Start from 0 and just increment by 1 for every skill you create after. Because inside our Dictionary we’re going to be building it based on the keys we find from a skills list and once it’s stumbled across a key that’s already in use, it will throw an exception.

Let’s populate our first skill configuration as follows:
Skill_Level: 0
Skill_CapLevel: 5
Skill_ID: 0
Skill_Dependencies: 0
Skill_Unlocked: False
Skill Required Points To Unlock: 1

Then within the Ability configurations on your Skill Asset:
Choose your own name, description, image.
Category needs to be ‘Abilities’
Optional: Abilities shouldn’t be stackable. Leave it unchecked
Optional: Consumabe will remove your ability entirely upon use. Leave it unchecked.
Optional: Choose your filter / target / effect strategies.
Optional: Put a cool down of your choosing.
Optional: Put how much mana it expends of your choosing.

Okay great, that should be all ready to be usable. Though of course we don’t have any way of utilizing any of this just yet.

Go to your UI Canvas Prefab → Create a new gameObject child and call it Skill UI
→ Create a new script and call it SkillTree.cs and attach it to Skill UI.

public class SkillTree : MonoBehaviour
    {
        [SerializeField] int availablePoints;
        [SerializeField] List<Skill> skillsList = new List<Skill>();
        Dictionary<int, Skill> skillsDictionary = new Dictionary<int, Skill>();
        Skill inspectedSkill;
    }

Our availablePoints is just here to test. I’d imagine building something like Sam and the team did for Traits inside the Progression asset to obtain more points upon leveling up.

Our list of Skills is where we will populate every skill into. It’s basically the library of all skills we can obtain inside our game.

Save & head back into Unity.

Go ahead and slot your first skill into that new list. Whenever you create more skills, you will need to do the same every time, just add them to this list.

Finally our Dictionary is to build a collection of references for every skill by using an ID the skill has as for our integer, to look up a particular skill in our list. We will be able to use these for later, from our Skills list to do cool things like checking whether the skill has any conditions before it can be unlocked or get data of already unlocked skills.

Let’s expand our script more.

        void Awake()
        {
            if (skillsList == null)
            {
                Debug.LogError($"Skills list is null. Populate to remove this error");
            }

            BuildSkillList();
        }

        void BuildSkillList()
        {
            foreach (Skill skill in skillsList)
            {
                if (skillsDictionary.ContainsValue(skill)) continue;

                skillsDictionary.Add(skill.SkillID, skill);
            }
        }

So inside of our Awake method we’re checking to see whether our Library list isn’t null, else we go ahead and begin BuildSkillList() method.

Inside BuildSkillList() we do a very simple foreach loop over every skill inside our library list, checking whether it’s already added or not - before populating our dictionary using the unique ID from that skill and the skill reference of itself.

        public void CanBeUnlocked(int id_skill)
        {
            if (skillsDictionary.TryGetValue(id_skill, out inspectedSkill))
            {
                IsSkillUnlocked(id_skill);
            }
        }

This function will be called within a button’s OnClick() event of the skill you wish to unlock. Ensure that you’re using the correct “id_Skill” when calling this. It needs to be the exact same as the ID inside your Dictionary of that particular skill.

So if we find a skill that exists we proceed to execute this bool IsSkillUnlocked() method.

        public bool IsSkillUnlocked(int id_skill)
        {
            if (SkillDependencies(id_skill))
            {
                return UnlockSkill(id_skill);
            }

            return false;
        }

But notice before we return whether we can unlock this skill it’s also wanting to know whether we have any dependencies or not. And depending on if the skill’s conditions are satisfied it will yield true or exit out and return false meaning you’re still unable to get this skill.

public bool SkillDependencies(int id_skill)
        {
            // We assume it can be unlocked, else we exit out with false.
            bool canUnlock = true;

            // The skill exists.
            if (skillsDictionary.TryGetValue(id_skill, out inspectedSkill))
            {
                // Enough points available.
                if (inspectedSkill.RequiredPointsToUnlock <= availablePoints)
                {
                    List<int> dependencies = inspectedSkill.Dependencies;
                    for (int i = 0; i < dependencies.Count; i++)
                    {
                        if (skillsDictionary.TryGetValue(dependencies[i], out inspectedSkill))
                        {
                            if (!inspectedSkill.Unlocked)
                            {
                                // Dependencies[i] is currently not unlocked.
                                canUnlock = false;
                                break;
                            }
                        }
                        else
                        {
                           // If one of the dependencies doesn't exist, the skill can't be unlocked.
                            return false;
                        }
                    }
                }
                else
                {
                    // If the player doesn't have enough skill points, can't unlock the new skill.
                    return false;
                }

            }
            else
            {
                // If the skill id doesn't exist, the skill can't be unlocked.
                return false;
            }

            // We can go ahead and begin the 'UnlockSkill' method.
            return canUnlock;
        }

Here we’re iterating over the 'inspectedSkill’s dependencies and player points to check conditions are met. If your Skill_ID doesn’t exist, it will return false. If any of your dependencies are locked, it will return false. If the player’s availablePoints are less than the Required Points To Unlock it returns false. But if we get through all of these checks, it will return true.

When it’s true your check will carry on and execute UnlockSkill() method.

 public bool UnlockSkill(int id_Skill)
        {
            // The skill exists.
            if (skillsDictionary.TryGetValue(id_Skill, out inspectedSkill))
            {
                // Have enough points and the skill level isn't maxed out.
                if (inspectedSkill.RequiredPointsToUnlock <= availablePoints && inspectedSkill.SkillLevel < inspectedSkill.SkillCap)
                {
                    availablePoints -= inspectedSkill.RequiredPointsToUnlock;

                    // Return whatever the smallest number is for our level.
                    inspectedSkill.SkillLevel = Mathf.Min(inspectedSkill.SkillLevel += 1, inspectedSkill.SkillCap);

                    // We replace the entry on the dictionary with the new one (already unlocked).
                    skillsDictionary.Remove(id_Skill);
                    skillsDictionary.Add(id_Skill, inspectedSkill);

                    // First time unlocking slot this into first found empty ActionSlot.
                    if (!inspectedSkill.Unlocked)
                    {
                        inspectedSkill.Unlocked = true;
                        Inventory inventory = FindObjectOfType<Inventory>();
                        InventoryItem abilityItem = inspectedSkill as InventoryItem;
                        inventory.AddToFirstEmptySlot(abilityItem, 1);
                        return true;
                    }

                    // We've successfully unlocked the skill after it's already been unlocked for the first time.
                    return true;
                }
                else
                {
                    // The skill can't be unlocked. Not enough points
                    return false;
                }
            }
            else
            {
                // The skill doesn't exist
                return false;
            }
        }

When our ability is unlocked for the first time, it will auto equip the ability to one empty action slot. This behaviour requires my modification to auto equipping items to equipment by default and potions / abilities to action slots that I did a few days ago (from writing this post).

That code extension can be found here: https://www.udemy.com/course/rpg-shops-abilities/learn/lecture/26466832#questions/17274260

Finally on the UnlockSkill() method we triple check that the correct skill exists and that we have enough points to deduct from our player to increment the level if it isn’t already not maxed out.

Notice that we remove the ID of the current skill and then re-populate it? That’s to ensure that we have our dictionary updated with the new data about that skill.

Create a new script called SkillButtonUI and attach it to the Button you’re using for Skills.

public class SkillButtonUI : MonoBehaviour
{
    [SerializeField] Skill skill;
    [SerializeField] Button button;
    SkillTree skillTree;

    void Awake()
    {
        button = GetComponent<Button>();
        skillTree = FindObjectOfType<SkillTree>();
    }

    void Update()
    {
        if (button != null)
        {
            button.interactable = skillTree.GetAvailablePoints() >= skill.RequiredPointsToUnlock;
        }
    }
}

Back on your SkillTree.cs add a new method

public int GetAvailablePoints()
{
    return availablePoints;
}

Now you will have the button be interactable depending on whether you have the right amount of points.

Okay! Time to test everything out. If you haven’t already made a button, then do so now and make sure that it calls the CanBeUnlocked() function. Double check the id_Skill is the same as the skill’s ID number. You should be able to unlock the skill and increment the level whenever pressed that depletes your availablePoints based on the cost of the skill’s required points to unlock.

If you wish to test the dependencies out as a Challenge please do so:

Make a second Skill and add it to the library list. With your new Skill selected, go to Dependencies & populate it by 1: Then the integer you wish to make it as a dependency is the ID of another skill. So in this case you’d want to have ‘0’ be the number. This means until you unlock your first skill, this cannot be unlocked.

You also need to have the OnClick() function be called except this time it needs to be ‘1’ since it’s referencing itself, whereas the Dependency needs to be checking for data about the other skill.

Well done! You’ve put a lot of work into this!

1 Like

I was wondering with my current set up if I could still use the IPredicateEvaluator interface to add extra conditions for certain abilities.

So far the only real checks in place is whether they’re relying on another skill to be unlocked [Which is awesome!] and if you have enough points. It’s great for a starting point…I just wasn’t 100% sure how I could utilize the condition set up for a Skill and how the Evaluate implementation would look like.

I remember what you typed out in a quick brief from my previous question in the Abilities section on Udemy. You wrote something like the following down below for conditional requirements.

MajorHealing
HasLevel 3
HasMinorHealing.

I think I could some-what achieve this with my dependencies but I’m sure an Evaluate implementation might make it easier down the road. Not sure so I’ll be interested to see your thoughts on this!

Assuming that you have implemented my Condition PropertyDrawer, I would argue that using Conditions is the best way to go. Your SkillTree will need to be an IPredicateEvaluator, and I would make a Predicate of something like HasSkill(string)

Yeah I am already using your Condition PropertyDrawer. I’ll give it a go to implement the IPredicateEvaluator and see what I could come up with. I’ll drop a reply here shortly

1 Like

Back and a bit defeated. Although I managed to get the editor to show off abilities and have the drop down work similarly to how the items work from the PropertyDrawer.

I wanted to be able to see my skills in the editor so I just copy pasted and switched out some things that you did for the InventoryItem and made a new section under ‘HasSkill’ by re-using your OnGUI code and DrawInventoryItemList() method. Although it would be cool to have the functionality of the slider with the chosen skill that way it kills two birds with one stone.

But I’m unsure how to set that part up. Anyways here’s the screenshot of the editor skill working.

Inside OnGUI from your PredicatePropertyDrawer I just followed what you did with the items aforementioned.

private Dictionary<string, Skill> skills;
 if (selectedPredicate == EPredicate.HasSkill)
{
       position.y += propHeight;
       DrawAbilityList(position, parameterZero);
}
void DrawAbilityList(Rect position, SerializedProperty element)
{
      BuildAbilityList();
      List<string> ids = skills.Keys.ToList();
      List<string> displayNames = new List<string>();
      foreach (string id in ids)
      {
           displayNames.Add(skills[id].GetDisplayName());
      }
     int index = ids.IndexOf(element.stringValue);
     EditorGUI.BeginProperty(position, new GUIContent("Skills"), element);
     int newIndex = EditorGUI.Popup(position, "Skill Required:", index, displayNames.ToArray());
     if (newIndex != index)
     {
          element.stringValue = ids[newIndex];
     }
}
void BuildAbilityList()
{
      if (skills != null) return;
      skills = new Dictionary<string, Skill>();
      foreach (Skill skill in Resources.LoadAll<Skill>(""))
      {
          skills[skill.GetItemID()] = skill;
      }
}

I’m no way near the level of comprehending all of this, but it does work inside the Inspector as I showed you and it’s mostly re-using your code so it should be OKAY hopefully. Feel free to lecture me if what I’m doing is idiotic haha.

Okay so this is all I’ve been able to do… I implemented the interface into SkillTree and set up a property / serializefield for condition on the Skill class itself. (As shown on the image)

I have implemented the Evaluate member but at this point I’m not entirely sure how to do a proper check because with the way it works it just unlocks any skill regardless if I’m negating it or don’t have any skill unlocked.

Anyways here is the code so far… (not much because this part still isn’t quite there yet for me to fulfill on my own.)

Skilltree.cs

public void CanBeUnlocked(int id_skill)
{
     if (skillsDictionary.TryGetValue(id_skill, out inspectedSkill))
     {
         IsSkillUnlocked(id_skill);
     }
}

public bool IsSkillUnlocked(int id_skill)
{
     return inspectedSkill.Condition.Check(GetComponents<IPredicateEvaluator>());
}
public bool? Evaluate(EPredicate predicate, List<string> parameters)
{
    if (predicate == EPredicate.HasSkill)
    {
          if (inspectedSkill.RequiredPointsToUnlock <= availablePoints)
          {
               UnlockSkill(inspectedSkill.SkillID);
          }
    }
    return null;
}

The only difference is I’m no longer checking for any Dependencies. UnlockSkill() is still the same from the first post so really there’s just one less check I’m going through to unlock a skill currently.

public bool HasSkill(int id_skill)
{
     if(skillDictionary.ContainsKey(id_skill))
     {
          return skillDictionary[id.skill].Unlocked;
      }
      return false;
}
public bool? Evaluate(EPredicate predicate, List<string> parameters)
{
      if(predicate==EPredicate.HasSkill)
      {
            if(int.TryParse(parameters[0], out int skill_ID)
            {
                 return HasSkill(skill_ID);
            }
            return false;
      }
      return null;
}

Hey Brian first of all, thanks for the quick reply.

I have your Evaluate inside my script and also the HasSkill but I’m just a bit confused where it all pieces together at the moment. I tried for a while before coming back here - one thing I’ve noticed is that my Evaluate is never getting called - I put a debug.log and haven’t seen it fire off once no matter what I did.

I think with the Evaluate I’m a bit confused with that concept still I guess. How exactly are they suppose to be called, the evaluate is from Condition.Check() method right? I did that but nothing happens so I removed the IsSkillUnlocked() method that was executing that code. Maybe I still need it… dunno.

Edit: And Yes I have IPredicateEvaluator interface on SkillTree.cs :slight_smile:

here are all my methods besides BuildSkillList.

        public void CanBeUnlocked(int id_skill)
        {
            if (skillsDictionary.TryGetValue(id_skill, out inspectedSkill))
            {
                HasSkill(id_skill);
            }
        }

        public bool HasSkill(int id_skill)
        {
            if (skillsDictionary.ContainsKey(id_skill))
            {
                return skillsDictionary[id_skill].Unlocked;
            }
            return false;
        }

        public bool UnlockSkill(int id_Skill)
        {
            // The skill exists.
            if (skillsDictionary.TryGetValue(id_Skill, out inspectedSkill))
            {
                // Have enough points and the skill level isn't maxed out.
                if (inspectedSkill.RequiredPointsToUnlock <= availablePoints && inspectedSkill.SkillLevel < inspectedSkill.SkillCap)
                {
                    availablePoints -= inspectedSkill.RequiredPointsToUnlock;

                    // Return whatever the smallest number is for our level.
                    inspectedSkill.SkillLevel = Mathf.Min(inspectedSkill.SkillLevel += 1, inspectedSkill.SkillCap);

                    // We replace the entry on the dictionary with the new one (already unlocked).
                    skillsDictionary.Remove(id_Skill);
                    skillsDictionary.Add(id_Skill, inspectedSkill);

                    // First time unlocking slot this into first found empty ActionSlot.
                    if (!inspectedSkill.Unlocked)
                    {
                        inspectedSkill.Unlocked = true;
                        Inventory inventory = FindObjectOfType<Inventory>();
                        InventoryItem abilityItem = inspectedSkill as InventoryItem;
                        inventory.AddToFirstEmptySlot(abilityItem, 1);
                        return true;
                    }

                    // We've successfully unlocked the skill after it's already been unlocked for the first time.
                    return true;
                }
                else
                {
                    // The skill can't be unlocked. Not enough points
                    return false;
                }
            }
            else
            {
                // The skill doesn't exist
                return false;
            }
        }

        public bool? Evaluate(EPredicate predicate, List<string> parameters)
        {
            if (predicate == EPredicate.HasSkill)
            {
                if (int.TryParse(parameters[0], out int skill_id))
                {
                    Debug.Log($"Did this get called?");
                    return HasSkill(skill_id);
                }
                return false;
            }
            return null;
        }

Put a Debug before the int.TryParse() statement as well. My best guess is that the skill_id is not parsing properly as an integer.

if(predicate == EPredicate.HasSkill)
{
     Debug.Log($"If(HasSkill({parameters[0]}))");
     if(int.Try...

Again, quick response, appreciate that a lot.

Here’s the Debug.Log after clicking on the skill that has a Condition set up.

If(HasSkill(84692a0d-a4b4-4497-ab1d-c6f539db340c
UnityEngine.Debug:Log (object)
Game.Abilities.SkillTree:Evaluate (Game.Utils.EPredicate,System.Collections.Generic.List`1<string>) (at Assets/Scripts/Abilities/SkillTree.cs:113)
Game.Utils.Condition/Predicate:Check (System.Collections.Generic.IEnumerable`1<Game.Utils.IPredicateEvaluator>) (at Assets/Scripts/Quests/Condition.cs:56)
Game.Utils.Condition/Disjunction:Check (System.Collections.Generic.IEnumerable`1<Game.Utils.IPredicateEvaluator>) (at Assets/Scripts/Quests/Condition.cs:35)
Game.Utils.Condition:Check (System.Collections.Generic.IEnumerable`1<Game.Utils.IPredicateEvaluator>) (at Assets/Scripts/Quests/Condition.cs:17)
Game.Abilities.SkillTree:IsSkillUnlocked (int) (at Assets/Scripts/Abilities/SkillTree.cs:54)
Game.Abilities.SkillTree:CanBeUnlocked (int) (at Assets/Scripts/Abilities/SkillTree.cs:48)
UnityEngine.EventSystems.EventSystem:Update () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:501)

the string “84692a0d-a4b4-4497-ab1d-c6f539db340c” is the correct skill I’ve got inside my conditional requirement.

AH, I must have misinterpreted the code from earlier, I thought the skill_id was an integer key into the skillsDictionary…

Re-reading through your code, I actually see a structural issue with the skills… you’re storing the Unlocked in the Skill : Ability : InventoryItem : ScriptableObject, but you need to know that while changes to a ScriptableObject via code in the Editor are persistent, changes to a ScriptableObject in a built game are NOT persistent.

You’ll need to store the status of the skill separately, and of course the best place to do this is within the SkillTree via Capture/Restorestate().
I would keep a separate Dictionary to hold the state

Dictionary<int, bool> skillsAcquired = new Dictionary<int, bool>();

When a skill is unlocked, set it true in the skillsAquired Dictionary. Use this Dictionary whenever you need to know if a skill is unlocked.

Now we need to convert from the UUID of the Skill to the int index into the two Dictionaries…

void HasSkill(string id)
{
     InventoryItem item = InventoryItem.GetFromID(id);
     if(item && item is Skill skill)
     {
          foreach(Skill testSkill in skillsDictionary.Values)
          {
               if(testSkill == skill) return skillsAcquired.ContainsKey(skill.SkillID);
          }
          return false;
     }

Then your EvaluateMethod can use the ID of the skill (skill.GetID())

I have got the capture / restore state I just never shared it - not out of malice but because I forgot. It saves and loads the data successfully in the Editor, if I unlocked a skill, then hit load when it wasn’t saved it goes back to false so I know it should work.

I haven’t tried it in a build just yet, but I’ll adjust the code when I do test it out there later if my set up for the capture state / restore state doesn’t persist as you mentioned

Right now I’m confused, and in my confusion have made you confused so I’m sorry about that. You’ve been a great help always and even now I’m closer to a solution thanks to you.

I wanted to comment that I am using integers for the Dictionary.

Could it be an issue from the PredicatePropertyDrawer? Could it be that the issue lies in the code I replicated inside the Editor script? Since that’s how I am selecting a Skill based on its name for example in the image above you’ll see “Has Skill: Skill Required: Poison Shot” because I simply duplicated and switched a few things that you were using for the InventoryItem predicate.

I’m wondering if the Editor Code I copy pasted and put together isn’t supporting the functionality from the condition inside the editor properly cause… i’ll be the first to let you know I’m not entirely sure if its even supportable. Haha anyways… unfortunately I’m a bit stuck and you’ve already been a big help with things, sorry again for the confusion.

I suspect that you set the Property Drawer up to treat the Skill like the InventoryItems are treated, using the ID of the Skill (the InventoryItem based ID). Since you know you’re dealing with skills, you could key off of the skill_ID instead. That was actually my assumption before, because you’re using the skill_ID everywhere.

I’m sorry if I’ve added noise and confusion to the process. The setup you have is sound, except for storing the Unlocked in the ScriptableObject. (Here’s why: Behind the scenes when the game is built into a player, all of the ScriptableObjects are bundled into the Asset Bundle, which is a big compressed blob of read only data. You can change Unlocked in the built game, and that will last until you quit the game and go back in, at which point the SOs are read back from that read onl Asset Bundle.

1 Like

Oh no, you’ve done nothing of the sort. If anything, you’ve been pulling me out of the hole I dug myself into. You’ve been a big help Brian.

I think the confusion for me is how Evaluate is called. When I originally set up my methods I could tell what was doing what, but when I first attempted to use the Conditions and set up the method for it I was thinking “What is calling this?” Because the way it worked was like this.

CanBeUnlocked() goes through the Dictionary list looking for the event’s integer. In this case for my first skill is ‘0’. When the dictionary finds that ‘0’ exists, it puts it out as a skill ‘inspectedSkill’.

Next we go to IsSkillUnlocked() which takes in the same integer so ‘0’ and puts it out as inspectedSkill and then goes through the SkillDependencies(id_skill) method. The point originally for this method was to iterate over the ‘0’ out inspectedSkill’s dependencies and that would dictate whether I could proceed to Unlock as true or false.

If it was false, it’s because ‘0’ out inspectedSkill had a dependency that was still checked as locked. Opposite for true which would then allow me to execute the Unlock(id_skill) function. That would then check if we had enough points and if it’s the first time unlocking that inspectedSkill then we would add it as an inventoryitem and use the AddToFirstEmptySlot() to show it off in the action slot.

BUT now I’m no longer using this method. Right now I’m having an issue figuring out what is suppose to be calling Unlock(id_skill) and how the Evaluate is fired off or how HasSkill is used in the chain of methods. And because I’m confused it’s got a bit difficult for me to understand the logic in the code and how they should be executed and in what order. But I’m still trying to be optimistic… :slight_smile:

         void BuildAbilityList()
        {
            if (skills != null) return;
            skills = new Dictionary<int, Skill>();
            foreach (Skill skill in Resources.LoadAll<Skill>(""))
            {
                skills[skill.SkillID] = skill;
            }
        }

        void DrawAbilityList(Rect position, SerializedProperty element)
        {
            BuildAbilityList();
            List<int> ids = skills.Keys.ToList();
            List<string> displayNames = new List<string>();
            foreach (int id in ids)
            {
                displayNames.Add(skills[id].GetDisplayName());
            }
            int index = ids.IndexOf(element.intValue);
            EditorGUI.BeginProperty(position, new GUIContent("Skills"), element);
            int newIndex = EditorGUI.Popup(position, "Skill Required:", index, displayNames.ToArray());
            if (newIndex != index)
            {
                element.stringValue = ids[newIndex].ToString();
            }
        }

Hey Brian sorry for the flood of posts here. I changed the predicate dictionary from <string, Skill> to <int, Skill> in the PropertyDrawer.cs – After taking a break from the computer I re-read some of your posts and realised you thought I was using an integer for the Dictionary and then it clicked that you thought I was using an <integer, skill> key, value inside the PropertyDrawer.

Well I did my best to switch it to an integer value… but now back in Editor it’ll throw me an error saying that ‘the type’ isn’t supported. I’m wondering if there’s something in my predicate drawer that you can spot I’m doing incorrectly. ^.^ Attached an image to help illustrate.

int index = ids.IndexOf(element.intValue);

That’s the code it’s pointing me too from the error.

Try changing this to:

element.intValue = ids[newIndex];

I should also point out that you need to end the property. In Editor code, every Beginxxxx must have an Endxxx, so at the end of the DrawAbilityList method:

EditorGUI.EndProperty();
1 Like

Didn’t fix the error unfortunately. I could compile the script but whenever my cursor hovered over the HasSkill then it would still give me the same error of type doesn’t support an integer value.

I’m thinking if I could attempt to implement the approach the course did for the Traits? Make an enum script and then check whether the enum is unlocked? What do you think? Or would my best bet would be the go back to the <string, skill> inside the Predicate and discard all integer inside SkillTree for a string and check simnilarly to the items.

I’m taking today off for the holiday, but here’s a quick thought…
The skills already have a unique string that’s auto assigned to them. Why not use the itemID as the keys in your Dictionary?

Of course, at this point, we have a very long thread in which the original code has been altered at multiple steps. It might not be a bad idea to post the scripts again, but in the state that they are now, and tomorrow I’ll run a full logic trace.

Greetings, I was working around the clock for hours testing different stuff to get the functionality I wanted.

Regrettably, I have scrapped the Evaluate for my Skills as I didn’t create the PredicatePropertyDrawer and that stuff isn’t familiar enough for me to be messing about with it. I did try though and that’s what counts in my mind. I also tried using the GetFromID string but I didn’t have much luck, but maybe in the future Brian will do some kind of Skills Predicate system and I’ll definitely adapt to that.

So in the mean time, I just did something different & it’s currently working better than what I originally produced. Here’s SkillTree.cs in it’s entirety. You’ll see the new method and some of the older ones removed.

SkillTree.cs

using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using Game.Inventories;
using Game.Saving;

namespace Game.Abilities
{
    [System.Serializable]
    public class SkillTree : MonoBehaviour, ISaveable
    {
        [SerializeField] List<Skill> skillsList = new List<Skill>();
        Dictionary<int, Skill> skillsDictionary = new Dictionary<int, Skill>(); 

        // State
        [SerializeField] int availablePoints;
        Skill inspectedSkill;

        void Awake()
        {
            if (skillsList == null)
            {
                Debug.LogError($"Skill Library list is null. Populate to remove this error");
            }

            BuildSkillList();
        }

        void BuildSkillList()
        {
            foreach (Skill skill in skillsList)
            {
                if (skillsDictionary.ContainsValue(skill)) continue;

                skillsDictionary.Add(skill.SkillID, skill);
            }
        }

        public int GetAvailablePoints()
        {
            return availablePoints;
        }

        public void CanBeUnlocked(int id_skill)
        {
            if (skillsDictionary.TryGetValue(id_skill, out inspectedSkill))
            {
                IsSkillUnlocked(id_skill);
            }
        }

        // NEW METHOD!
        bool IsSkillUnlocked(int id_skill)
        {
            // Creates a list of all skillConfigs Skill Dependencies for this particular inspectedSkill.
            List<Skill> skills = skillsDictionary[id_skill].SkillConfig.skillDependencies.ToList();

            // If inspectedSkill has no dependencies attempt to unlock the inspectedSkill immediately.
            if (skills.Count <= 0)
            {
                return UnlockSkill(id_skill);
            }

            // If inspectedSkill has dependencies, iterate over all found inside skills List.
            foreach (Skill skill in skills)
            {
                // If the list finds ALL dependencies and they're unlocked then we proceed to unlock our skill.
                if (skills.Contains(skill) && skill.Unlocked)
                {
                    return UnlockSkill(id_skill);
                }

                // Any or all of the dependencies were locked or the inspectedSkill dependency didn't exist.
                return false;
            }

            // If we didn't have enough points to spend or the inspectedSkill wasn't found.
            return false;
        }

        bool UnlockSkill(int id_Skill)
        { 
            // The skill exists.
            if (skillsDictionary.TryGetValue(id_Skill, out inspectedSkill))
            {
                // Have enough points and the skill level isn't maxed out.
                if (inspectedSkill.RequiredPointsToUnlock <= availablePoints &&
                    inspectedSkill.SkillLevel < inspectedSkill.SkillCap)
                {
                    availablePoints -= inspectedSkill.RequiredPointsToUnlock;

                    // Return whatever the smallest number is for our level.
                    inspectedSkill.SkillLevel = Mathf.Min(inspectedSkill.SkillLevel += 1, inspectedSkill.SkillCap);

                    // We replace the entry on the dictionary with the new one (already unlocked).
                    skillsDictionary.Remove(id_Skill);
                    skillsDictionary.Add(id_Skill, inspectedSkill);

                    // First time unlocking slot this into first found empty ActionSlot.
                    if (!inspectedSkill.Unlocked)
                    {
                        inspectedSkill.Unlocked = true;
                        Inventory inventory = FindObjectOfType<Inventory>();
                        InventoryItem abilityItem = inspectedSkill as InventoryItem;
                        inventory.AddToFirstEmptySlot(abilityItem, 1);
                        return true;
                    }

                    // We've successfully unlocked the skill after it's already been unlocked for the first time.
                    return true;
                }
                else
                {
                    // The skill can't be unlocked. Not enough points
                    return false;
                }
            }
            else
            {
                // The skill doesn't exist
                return false;
            }
            
        }

        [System.Serializable]
        struct SaveData
        {
            public List<bool> skillUnlocked;
            public List<int> cost;
            public List<int> skillID;
            public List<int> levelCap;
            public List<int> level;
            public int points;
        }

        public object CaptureState()
        {
            SaveData data = new SaveData();

            data.points = availablePoints;

            data.cost = new List<int>();
            data.skillID = new List<int>();
            data.level = new List<int>();
            data.levelCap = new List<int>();
            data.skillUnlocked = new List<bool>();
            
            for (int i = 0; i < skillsList.Count; i++)
            {
                data.cost.Add(skillsDictionary[i].RequiredPointsToUnlock);
                data.skillUnlocked.Add(skillsDictionary[i].Unlocked);
                data.skillID.Add(skillsDictionary[i].SkillID);
                data.level.Add(skillsDictionary[i].SkillLevel);
                data.levelCap.Add(skillsDictionary[i].SkillCap);
            }
            return data;
        }

        public void RestoreState(object state)
        {
            SaveData loadData = (SaveData)state;

            availablePoints = loadData.points;

            for (int i = 0; i < skillsList.Count; i++)
            {
                skillsDictionary[i].RequiredPointsToUnlock = loadData.cost[i];
                skillsDictionary[i].Unlocked = loadData.skillUnlocked[i];
                skillsDictionary[i].SkillID = loadData.skillID[i];
                skillsDictionary[i].SkillLevel = loadData.level[i];
                skillsDictionary[i].SkillCap = loadData.levelCap[i];
            }
        }
    }
}

And I made some changes in the Skill.cs as well.

using System;
using System.Collections.Generic;
using UnityEngine;

namespace Game.Abilities
{
    [System.Serializable]
    [CreateAssetMenu(fileName = "New Skill", menuName = "Scriptable Objects/Skill/Create New Skill")]
    public class Skill : Ability
    {
        [Serializable]
        public class SkillDependencies
        {
            public List<Skill> skillDependencies = new List<Skill>();
        }


        [Header("Skill Configurations")]
        #region Configurable Parameters
        /// <summary>
        /// Represents a unique identification, which is required to obtain data about any particular Skill.
        /// </summary>
        [SerializeField] int skillID;
        /// <summary>
        /// A Skill's current level. A level depicts how effective of an ability can become, maximizing it out allows the player to deal extraordinary damage or support.
        /// </summary>
        [Range(0, 100)][SerializeField] int skillLevel;
        /// <summary>
        /// The maximum amount of skill points that can be attained from any Skill for the player.
        /// </summary>
        [Range(1, 100)] [SerializeField] int skillCapLevel;
        /// <summary>
        /// A boolean variable for gatekeeping a Skill locked if it's prerequisites are not met.
        /// </summary>
        [SerializeField] bool skillUnlocked;
        /// <summary>
        /// Amount of player skill points required to unlock this Skill.
        /// </summary>
        [Range(1, 100)] [SerializeField] int skillRequiredPointsToUnlock;
        /// <summary>
        /// A collection of Skills to determine whether a Skill has it's prerequisites met before it can be unlocked.
        /// </summary>
        [SerializeField] SkillDependencies skillConfig;
        #endregion

        #region Properties
        public int SkillLevel { get { return skillLevel; } set { skillLevel = value; } }
        public int SkillCap { get { return skillCapLevel; } set { skillCapLevel = value; } }
        public int SkillID { get { return skillID; } set { skillID = value; } }
        public SkillDependencies SkillConfig { get { return skillConfig; } set { skillConfig = value; } }
        public bool Unlocked { get { return skillUnlocked; } set { skillUnlocked = value; } }
        public int RequiredPointsToUnlock { get { return skillRequiredPointsToUnlock; } set { skillRequiredPointsToUnlock = value; } }
        #endregion
    }
}

If a skill has a dependency you want to have, slot in the skill into the SkillConfig → SkillDependencies list and then if that skill is unlocked you’ll be able to unlock the skill depending on that prerequisite being satisfied. If you have multiple skills needing unlocking, if any one of them is still locked, it won’t unlock the new skill until ALL required skills are deemed unlocked.

I’m actually gonna work on a way to make a new prerequisite where it tests whether the skill has enough invested points before the new skill can be unlocked as well. I’ll post my results here later…

Which explains why I made the class SkillDependencies rather than a simple list since I can add more configurable information there later on… anyways we shall see. That’s for another time!

I still have a concern about Unlocked being in the Skill, as this won’t persist once you build your game out to a player.

Privacy & Terms