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.