Unlocking Abilities

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.

Edit: This is my current save set up, so in a build none of this will save at all or will it? I’m kind of a beginner with Scriptable Objects outside of what we were taught in the courses. You said read only so I’m guessing it means it won’t save the data correctly or at all. Is my set up redundant atm?

        [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];
            }
        }

Ok, that might actually work since you’ll be adjusting the copy in RestoreState.

1 Like

After seeing this comment I isolated a scene and built it out and tested it… It does appear to work. But I’ll keep an eye on it down the track as I expand things for the skill tree! :slight_smile:

I made the changes I described above earlier about also checking for a level condition of any skill. It works well for me – I’m not afraid to be critiqued or to be shown a better method of doing all of this, so any student or brian feel free anytime.

Here’s what it looks like on a Skill now from the Editor Inspector. (Down the bottom I have 2 skills that are being checked as well as both skills requiring a certain level.)

I have a list of Skill Configs that each take in a skill and a level required. So in order to unlock this skill, it takes my “Ability 0” to be at level 2, whilst my “Ability 2” needs to have 3 points invested before it can be unlocked.

So inside the Skill script I made these changes:

        [Serializable]
        public class SkillConfig
        {
            public Skill dependency;
            [Range(0, 100)] public int skillLevelRequired;
        }
      
       // To have access to the skill dependencies as a list.
       [SerializeField] List<SkillConfig> skillConfig = new List<SkillConfig>();
       // Property to gain access inside SkillTree.
       public List<SkillConfig> SkillConfigs { get { return skillConfig; } set { skillConfig = value; } }

Once that was all done inside the Skill script I made an adjustment to how we iterate over our dependencies and used Linq to check “All” of the skill inside our skillConfig depenedencies and skill requirements to see whether they were satisfied to unlock the new skill.

Here’s the updated IsSkillUnlocked() method to handle this:

        bool IsSkillUnlocked(int id_skill)
        {
            // Creates a list of all skillConfigs Skill conditions for this particular inspectedSkill.
            var skills = skillsDictionary[id_skill].SkillConfigs.ToList();

            // If inspectedSkill has no dependencies or level requirements then we 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 (var skill in skills)
            {
                // If all SkillConfig prerequisites are met than proceed to unlock inspectedSkill.
                if (skills.All((skill) => skill.dependency.Unlocked && skill.dependency.SkillLevel >= skill.skillLevelRequired))
                {
                    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;
        }

I’m quite happy I am able to accomplish something like this.

Brian… you replied a lot and at times I felt that I was frustrating you so I apologize if I did. If I didn’t, thanks for sticking with me and lending me your thoughts on my whole process. This will be my last reply for now… any other students feel free to add more to this skill system if you wish and share it here!

I’m happy to help where I can. You’re definitely on the right track here! Well done!

1 Like

Privacy & Terms