In this first tutorial, we are going to learn how to make a skill tree. This skill tree will be similar to the one in Horizon: Zero Dawn. Over this series of tutorial, we are going to create the skill tree data structure, to learn how to use it, and to create a tool which will allow us to get a better control over the skill tree creation and modification. At the end of this tutorial, you should be able to make something like this:

First of all, let’s start by analyzing the skill tree from Horizon: Zero Dawn.

As you can see in the video above:

Each skill has two states: unlocked or locked.

Almost every skill depends on another skill. Once you unlock one skill, you would be able to unlock the following one.

Every skill has a cost.

In addition to that, we need to differentiate every skill. For that, we can use an ID as you would do in a database.

Creating the Skill and SkillTree classes

In order to accomplish the requirements explained above, we will need an integer that will work as our ID, a boolean to know whether the skill is already unlocked or not, a cost and an array of skill IDs that we are going to use as dependencies. So our Skill class should be something like this:

Skill.cs [System.Serializable] public class Skill { public int id_Skill; public int[] skill_Dependencies; public bool unlocked; public int cost; } 1 2 3 4 5 6 7 8 [ System . Serializable ] public class Skill { public int id_Skill ; public int [ ] skill_Dependencies ; public bool unlocked ; public int cost ; }

Furthermore, we will need a new class for managing a bunch of these skills. For this, we will define the class SkillTree:

SkillTree.cs using System.Collections.Generic; [System.Serializable] public class SkillTree { public Skill[] skilltree; } 1 2 3 4 5 6 using System . Collections . Generic ; [ System . Serializable ] public class SkillTree { public Skill [ ] skilltree ; }

Reading the skill tree

Of course, we will need to read and write the skill tree data into a file. For this, we will create a new class called SkillTreeReader and read from and write to it in a JSON file.

string path = "Assets/SkillTree/Data/skilltree.json"; string dataAsJson; if (File.Exists(path)) { // Read the json from the file into a string dataAsJson = File.ReadAllText(path); // Pass the json to JsonUtility, and tell it to create a SkillTree object from it SkillTree loadedData = JsonUtility.FromJson<SkillTree>(dataAsJson); // Store the SkillTree as an array of Skill _skillTree = new Skill[loadedData.skilltree.Length]; _skillTree = loadedData.skilltree; // Populate a dictionary with the skill id and the skill data itself for (int i = 0; i < _skillTree.Length; ++i) { _skills.Add(_skillTree[i].id_Skill, _skillTree[i]); } } else { Debug.LogError("Cannot load game data!"); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 string path = "Assets/SkillTree/Data/skilltree.json" ; string dataAsJson ; if ( File . Exists ( path ) ) { // Read the json from the file into a string dataAsJson = File . ReadAllText ( path ) ; // Pass the json to JsonUtility, and tell it to create a SkillTree object from it SkillTree loadedData = JsonUtility . FromJson < SkillTree > ( dataAsJson ) ; // Store the SkillTree as an array of Skill _skillTree = new Skill [ loadedData . skilltree . Length ] ; _skillTree = loadedData . skilltree ; // Populate a dictionary with the skill id and the skill data itself for ( int i = 0 ; i < _skillTree . Length ; ++ i ) { _skills . Add ( _skillTree [ i ] . id_Skill , _skillTree [ i ] ) ; } } else { Debug . LogError ( "Cannot load game data!" ) ; }

In order to make it easier to look for any skill, after reading the data, we populate a dictionary where we will be able to make a search by ID.

But… how should we define our skill tree? This is not the coolest thing to do, but we will get to that on the second part of this tutorial. By now, the file should look something like this:

{"skilltree":[ {"id_Skill":0,"skill_Dependencies":[],"unlocked":true,"cost":0}, {"id_Skill":1,"skill_Dependencies":[0],"unlocked":false,"cost":0}, {"id_Skill":2,"skill_Dependencies":[0],"unlocked":false,"cost":2} ]} 1 2 3 4 5 { "skilltree" : [ { "id_Skill" : 0 , "skill_Dependencies" : [ ] , "unlocked" : true , "cost" : 0 } , { "id_Skill" : 1 , "skill_Dependencies" : [ 0 ] , "unlocked" : false , "cost" : 0 } , { "id_Skill" : 2 , "skill_Dependencies" : [ 0 ] , "unlocked" : false , "cost" : 2 } ] }

You can copy/paste this JSON file for having the complete skill tree structure like the one seen in Horizon: Zero Dawn. You should place this file in the /Assets/SkillTree/Data folder.

{ "skilltree": [ {"id_Skill": 0,"skill_Dependencies": [],"unlocked": true,"cost": 0}, {"id_Skill": 1,"skill_Dependencies": [0],"unlocked": false,"cost": 1}, {"id_Skill": 3,"skill_Dependencies": [0],"unlocked": false,"cost": 1}, {"id_Skill": 5,"skill_Dependencies": [0],"unlocked": false,"cost": 1}, {"id_Skill": 2,"skill_Dependencies": [0],"unlocked": false,"cost": 1}, {"id_Skill": 4,"skill_Dependencies": [0],"unlocked": false,"cost": 1}, {"id_Skill": 6,"skill_Dependencies": [0],"unlocked": false,"cost": 1}, {"id_Skill": 7,"skill_Dependencies": [0],"unlocked": false,"cost": 1}, {"id_Skill": 8,"skill_Dependencies": [0],"unlocked": false,"cost": 1}, {"id_Skill": 9,"skill_Dependencies": [0],"unlocked": false,"cost": 1}, {"id_Skill": 10,"skill_Dependencies": [1],"unlocked": false,"cost": 2}, {"id_Skill": 11,"skill_Dependencies": [2],"unlocked": false,"cost": 2}, {"id_Skill": 12,"skill_Dependencies": [3],"unlocked": false,"cost": 2}, {"id_Skill": 13,"skill_Dependencies": [4],"unlocked": false,"cost": 2}, {"id_Skill": 14,"skill_Dependencies": [5],"unlocked": false,"cost": 2}, {"id_Skill": 15,"skill_Dependencies": [6],"unlocked": false,"cost": 2}, {"id_Skill": 16,"skill_Dependencies": [7],"unlocked": false,"cost": 2}, {"id_Skill": 17,"skill_Dependencies": [8],"unlocked": false,"cost": 2}, {"id_Skill": 18,"skill_Dependencies": [9],"unlocked": false,"cost": 2}, {"id_Skill": 19,"skill_Dependencies": [10],"unlocked": false,"cost": 3}, {"id_Skill": 20,"skill_Dependencies": [10],"unlocked": false,"cost": 3}, {"id_Skill": 21,"skill_Dependencies": [12],"unlocked": false,"cost": 3}, {"id_Skill": 22,"skill_Dependencies": [13],"unlocked": false,"cost": 3}, {"id_Skill": 23,"skill_Dependencies": [13],"unlocked": false,"cost": 3}, {"id_Skill": 24,"skill_Dependencies": [15],"unlocked": false,"cost": 3}, {"id_Skill": 25,"skill_Dependencies": [16],"unlocked": false,"cost": 3}, {"id_Skill": 26,"skill_Dependencies": [18],"unlocked": false,"cost": 3}, {"id_Skill": 27,"skill_Dependencies": [18],"unlocked": false,"cost": 3}, {"id_Skill": 28,"skill_Dependencies": [19],"unlocked": false,"cost": 3}, {"id_Skill": 29,"skill_Dependencies": [20],"unlocked": false,"cost": 3}, {"id_Skill": 30,"skill_Dependencies": [21],"unlocked": false,"cost": 3}, {"id_Skill": 31,"skill_Dependencies": [22],"unlocked": false,"cost": 3}, {"id_Skill": 32,"skill_Dependencies": [23],"unlocked": false,"cost": 3}, {"id_Skill": 33,"skill_Dependencies": [24],"unlocked": false,"cost": 3}, {"id_Skill": 34,"skill_Dependencies": [25],"unlocked": false,"cost": 3}, {"id_Skill": 35,"skill_Dependencies": [26],"unlocked": false,"cost": 3}, {"id_Skill": 36,"skill_Dependencies": [27],"unlocked": false,"cost": 3} ]} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 { "skilltree" : [ { "id_Skill" : 0 , "skill_Dependencies" : [ ] , "unlocked" : true , "cost" : 0 } , { "id_Skill" : 1 , "skill_Dependencies" : [ 0 ] , "unlocked" : false , "cost" : 1 } , { "id_Skill" : 3 , "skill_Dependencies" : [ 0 ] , "unlocked" : false , "cost" : 1 } , { "id_Skill" : 5 , "skill_Dependencies" : [ 0 ] , "unlocked" : false , "cost" : 1 } , { "id_Skill" : 2 , "skill_Dependencies" : [ 0 ] , "unlocked" : false , "cost" : 1 } , { "id_Skill" : 4 , "skill_Dependencies" : [ 0 ] , "unlocked" : false , "cost" : 1 } , { "id_Skill" : 6 , "skill_Dependencies" : [ 0 ] , "unlocked" : false , "cost" : 1 } , { "id_Skill" : 7 , "skill_Dependencies" : [ 0 ] , "unlocked" : false , "cost" : 1 } , { "id_Skill" : 8 , "skill_Dependencies" : [ 0 ] , "unlocked" : false , "cost" : 1 } , { "id_Skill" : 9 , "skill_Dependencies" : [ 0 ] , "unlocked" : false , "cost" : 1 } , { "id_Skill" : 10 , "skill_Dependencies" : [ 1 ] , "unlocked" : false , "cost" : 2 } , { "id_Skill" : 11 , "skill_Dependencies" : [ 2 ] , "unlocked" : false , "cost" : 2 } , { "id_Skill" : 12 , "skill_Dependencies" : [ 3 ] , "unlocked" : false , "cost" : 2 } , { "id_Skill" : 13 , "skill_Dependencies" : [ 4 ] , "unlocked" : false , "cost" : 2 } , { "id_Skill" : 14 , "skill_Dependencies" : [ 5 ] , "unlocked" : false , "cost" : 2 } , { "id_Skill" : 15 , "skill_Dependencies" : [ 6 ] , "unlocked" : false , "cost" : 2 } , { "id_Skill" : 16 , "skill_Dependencies" : [ 7 ] , "unlocked" : false , "cost" : 2 } , { "id_Skill" : 17 , "skill_Dependencies" : [ 8 ] , "unlocked" : false , "cost" : 2 } , { "id_Skill" : 18 , "skill_Dependencies" : [ 9 ] , "unlocked" : false , "cost" : 2 } , { "id_Skill" : 19 , "skill_Dependencies" : [ 10 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 20 , "skill_Dependencies" : [ 10 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 21 , "skill_Dependencies" : [ 12 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 22 , "skill_Dependencies" : [ 13 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 23 , "skill_Dependencies" : [ 13 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 24 , "skill_Dependencies" : [ 15 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 25 , "skill_Dependencies" : [ 16 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 26 , "skill_Dependencies" : [ 18 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 27 , "skill_Dependencies" : [ 18 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 28 , "skill_Dependencies" : [ 19 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 29 , "skill_Dependencies" : [ 20 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 30 , "skill_Dependencies" : [ 21 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 31 , "skill_Dependencies" : [ 22 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 32 , "skill_Dependencies" : [ 23 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 33 , "skill_Dependencies" : [ 24 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 34 , "skill_Dependencies" : [ 25 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 35 , "skill_Dependencies" : [ 26 ] , "unlocked" : false , "cost" : 3 } , { "id_Skill" : 36 , "skill_Dependencies" : [ 27 ] , "unlocked" : false , "cost" : 3 } ] }

Using the skill tree

The three main functions that we are going to use within the skill tree are the following ones:

Knowing if a skill is unlocked

Knowing if a skill could be unlocked (should satisfy dependencies and cost)

Unlocking a skill

So the class SkillTreeReader will look like this:

SkillTreeReader.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using System.IO; public class SkillTreeReader : MonoBehaviour { private static SkillTreeReader _instance; public static SkillTreeReader Instance { get { return _instance; } set { } } // Array with all the skills in our skilltree private Skill[] _skillTree; // Dictionary with the skills in our skilltree private Dictionary<int, Skill> _skills; // Variable for caching the currently being inspected skill private Skill _skillInspected; public int availablePoints = 100; void Awake() { if(_instance == null) { _instance = this; DontDestroyOnLoad(this.gameObject); SetUpSkillTree(); } else { Destroy(this.gameObject); } } // Use this for initialization of the skill tree void SetUpSkillTree () { _skills = new Dictionary<int, Skill>(); LoadSkillTree(); } // Update is called once per frame void Update () { } public void LoadSkillTree() { string path = "Assets/SkillTree/Data/skilltree.json"; string dataAsJson; if (File.Exists(path)) { // Read the json from the file into a string dataAsJson = File.ReadAllText(path); // Pass the json to JsonUtility, and tell it to create a SkillTree object from it SkillTree loadedData = JsonUtility.FromJson<SkillTree>(dataAsJson); // Store the SkillTree as an array of Skill _skillTree = new Skill[loadedData.skilltree.Length]; _skillTree = loadedData.skilltree; // Populate a dictionary with the skill id and the skill data itself for (int i = 0; i < _skillTree.Length; ++i) { _skills.Add(_skillTree[i].id_Skill, _skillTree[i]); } } else { Debug.LogError("Cannot load game data!"); } } public bool IsSkillUnlocked(int id_skill) { if (_skills.TryGetValue(id_skill, out _skillInspected)) { return _skillInspected.unlocked; } else { return false; } } public bool CanSkillBeUnlocked(int id_skill) { bool canUnlock = true; if(_skills.TryGetValue(id_skill, out _skillInspected)) // The skill exists { if(_skillInspected.cost <= availablePoints) // Enough points available { int[] dependencies = _skillInspected.skill_Dependencies; for (int i = 0; i < dependencies.Length; ++i) { if (_skills.TryGetValue(dependencies[i], out _skillInspected)) { if (!_skillInspected.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; } return canUnlock; } public bool UnlockSkill(int id_Skill) { if(_skills.TryGetValue(id_Skill, out _skillInspected)) { if (_skillInspected.cost <= availablePoints) { availablePoints -= _skillInspected.cost; _skillInspected.unlocked = true; // We replace the entry on the dictionary with the new one (already unlocked) _skills.Remove(id_Skill); _skills.Add(id_Skill, _skillInspected); return true; } else { return false; // The skill can't be unlocked. Not enough points } } else { return false; // The skill doesn't exist } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 using System . Collections ; using System . Collections . Generic ; using UnityEngine ; using System . IO ; public class SkillTreeReader : MonoBehaviour { private static SkillTreeReader _instance ; public static SkillTreeReader Instance { get { return _instance ; } set { } } // Array with all the skills in our skilltree private Skill [ ] _skillTree ; // Dictionary with the skills in our skilltree private Dictionary < int , Skill > _skills ; // Variable for caching the currently being inspected skill private Skill _skillInspected ; public int availablePoints = 100 ; void Awake ( ) { if ( _instance == null ) { _instance = this ; DontDestroyOnLoad ( this . gameObject ) ; SetUpSkillTree ( ) ; } else { Destroy ( this . gameObject ) ; } } // Use this for initialization of the skill tree void SetUpSkillTree ( ) { _skills = new Dictionary < int , Skill > ( ) ; LoadSkillTree ( ) ; } // Update is called once per frame void Update ( ) { } public void LoadSkillTree ( ) { string path = "Assets/SkillTree/Data/skilltree.json" ; string dataAsJson ; if ( File . Exists ( path ) ) { // Read the json from the file into a string dataAsJson = File . ReadAllText ( path ) ; // Pass the json to JsonUtility, and tell it to create a SkillTree object from it SkillTree loadedData = JsonUtility . FromJson < SkillTree > ( dataAsJson ) ; // Store the SkillTree as an array of Skill _skillTree = new Skill [ loadedData . skilltree . Length ] ; _skillTree = loadedData . skilltree ; // Populate a dictionary with the skill id and the skill data itself for ( int i = 0 ; i < _skillTree . Length ; ++ i ) { _skills . Add ( _skillTree [ i ] . id_Skill , _skillTree [ i ] ) ; } } else { Debug . LogError ( "Cannot load game data!" ) ; } } public bool IsSkillUnlocked ( int id_skill ) { if ( _skills . TryGetValue ( id_skill , out _skillInspected ) ) { return _skillInspected . unlocked ; } else { return false ; } } public bool CanSkillBeUnlocked ( int id_skill ) { bool canUnlock = true ; if ( _skills . TryGetValue ( id_skill , out _skillInspected ) ) // The skill exists { if ( _skillInspected . cost <= availablePoints ) // Enough points available { int [ ] dependencies = _skillInspected . skill_Dependencies ; for ( int i = 0 ; i < dependencies . Length ; ++ i ) { if ( _skills . TryGetValue ( dependencies [ i ] , out _skillInspected ) ) { if ( ! _skillInspected . 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 ; } return canUnlock ; } public bool UnlockSkill ( int id_Skill ) { if ( _skills . TryGetValue ( id_Skill , out _skillInspected ) ) { if ( _skillInspected . cost <= availablePoints ) { availablePoints -= _skillInspected . cost ; _skillInspected . unlocked = true ; // We replace the entry on the dictionary with the new one (already unlocked) _skills . Remove ( id_Skill ) ; _skills . Add ( id_Skill , _skillInspected ) ; return true ; } else { return false ; // The skill can't be unlocked. Not enough points } } else { return false ; // The skill doesn't exist } } }

We can use the function IsSkillUnlocked(int id_skill) if we want to know whether the skill is already unlocked or not. The function CanSkillBeUnlocked(int id_skill) is used for knowing if all the dependencies (node dependencies and cost) are satisfied. And the function UnlockSkill(int id_Skill) is used for setting a skill as unlocked (if it’s possible) and returns true if all went OK or false if there were any errors.

Now is up to you to replicate the skill tree on Horizon: Zero Dawn. I made it by creating a simple canvas with a bunch of buttons using these functions above. Don’t forget to comment any doubt or suggestion and feel free to share your creations in the comments section or on Twitter (@AntonioClavain).

Conclusion

This is all for this first tutorial. In the next posts, we are going to improve the method for creating the JSON file by making a node editor for the Unity Editor. We are also going to create a simple gameplay example using a skill tree and make it possible to save the progression of the player.

I hope this tutorial was engaging and useful for you guys. I just want to remind you that I am open to any suggestions, questions, and comments.

See you soon! 🙂