Hello and welcome to the second part of this tutorial series about creating a skill tree system for your game in Unity. In this tutorial, we are going to learn to create an editor tool for the management of our skill tree. After this tutorial, you will have a tool for managing your skill tree as cool as the one seen in the video below.
In order to make this post shorter and cleaner I am going to use the awesome tutorial from Gram Games about creating a node editor on Unity, and add the custom functionality over their editor tool. I truly recommend to take a look at their tutorial, it is really good 🙂  Especially if it is your first approach to creating editor code for Unity.
- Let’s create a skill tree – Part I (Horizon: Zero Dawn)
- Let’s create a skill tree – Part II (Making a node editor)
- Let’s create a skill tree – Part III (Adding the skill tree to our game)
The starting point
Let’s start by making a quick introduction to the code provided by Gram Games in their tutorial.
The Node class is the one with all the functionality to manage each node. With it, we can draw the node, move it over the custom window or even delete it.
1 2 3 4 5 6 7 8 9 10 |
using System; using UnityEditor; using UnityEngine; public class Node { public Rect rect; public string title; public bool isDragged; public bool isSelected; |
The ConnectionPoint class allows us to define our points to connect different nodes.
1 2 3 4 5 6 7 8 9 10 |
using System; using UnityEngine; public enum ConnectionPointType { In, Out } public class ConnectionPoint { public Rect rect; public ConnectionPointType type; |
The class Connection allows us to define the connection between two nodes and draws it in our custom editor window.
1 2 3 4 5 6 7 8 9 10 |
using System; using UnityEditor; using UnityEngine; public class Connection { public ConnectionPoint inPoint; public ConnectionPoint outPoint; public Action<Connection> OnClickRemoveConnection; |
And here it comes the class that makes all the magic, the class NodeBasedEditor. This class inherits from EditorWindow and creates an editor window that gives the order to all of our previous classes to being drawn. It also manages events for creating and deleting nodes and connections.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using UnityEngine; using UnityEditor; using System.Collections.Generic; public class NodeBasedEditor : EditorWindow { private List<Node> nodes; private List<Connection> connections; private GUIStyle nodeStyle; private GUIStyle selectedNodeStyle; private GUIStyle inPointStyle; private GUIStyle outPointStyle; private ConnectionPoint selectedInPoint; private ConnectionPoint selectedOutPoint; private Vector2 offset; private Vector2 drag; |
The objectives
This is a basic node editor and we need to add functionality in order to make it useful to manage our skill tree.
This is the things that we are going to add to this node editor:
- A title for each node: We are going to use the ID of each skill.
- A boolean field for setting if the skill is unlocked or not.
- An integer field for setting the cost of the skill.
- Generate the JSON file with the skill tree created in the node editor.
- Make the node editor persistent. Functions to clear, load and save the state of the node editor.
Let’s get to it
Let’s start by adding a reference to a skill from each one of the nodes. You can see the changes marked in yellow:
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 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
using System; using UnityEditor; using UnityEngine; using System.Text; public class Node { public Rect rect; public string title; public bool isDragged; public bool isSelected; // Rect for the title of the node public Rect rectID; // Two Rect for the unlock field (1 for the label and other for the checkbox) public Rect rectUnlockLabel; public Rect rectUnlocked; // Two Rect for the cost field (1 for the label and other for the text field) public Rect rectCostLabel; public Rect rectCost; public ConnectionPoint inPoint; public ConnectionPoint outPoint; public GUIStyle style; public GUIStyle defaultNodeStyle; public GUIStyle selectedNodeStyle; // GUI Style for the title public GUIStyle styleID; // GUI Style for the fields public GUIStyle styleField; public Action<Node> OnRemoveNode; // Skill linked with the node public Skill skill; // Bool for checking if the node is whether unlocked or not private bool unlocked = false; // StringBuilder to create the node's title private StringBuilder nodeTitle; public Node(Vector2 position, float width, float height, GUIStyle nodeStyle, GUIStyle selectedStyle, GUIStyle inPointStyle, GUIStyle outPointStyle, Action<ConnectionPoint> OnClickInPoint, Action<ConnectionPoint> OnClickOutPoint, Action<Node> OnClickRemoveNode, int id, bool unlocked, int cost, int[] dependencies) { rect = new Rect(position.x, position.y, width, height); style = nodeStyle; inPoint = new ConnectionPoint(this, ConnectionPointType.In, inPointStyle, OnClickInPoint); outPoint = new ConnectionPoint(this, ConnectionPointType.Out, outPointStyle, OnClickOutPoint); defaultNodeStyle = nodeStyle; selectedNodeStyle = selectedStyle; OnRemoveNode = OnClickRemoveNode; // Create new Rect and GUIStyle for our title and custom fields float rowHeight = height / 7; rectID = new Rect(position.x, position.y + rowHeight, width, rowHeight); styleID = new GUIStyle(); styleID.alignment = TextAnchor.UpperCenter; rectUnlocked = new Rect(position.x + width / 2, position.y + 3 * rowHeight, width / 2, rowHeight); rectUnlockLabel = new Rect(position.x, position.y + 3 * rowHeight, width / 2, rowHeight); styleField = new GUIStyle(); styleField.alignment = TextAnchor.UpperRight; rectCostLabel = new Rect(position.x, position.y + 4 * rowHeight, width / 2, rowHeight); rectCost = new Rect(position.x + width / 2, position.y + 4 * rowHeight, 20, rowHeight); this.unlocked = unlocked; // We create the skill with current node info skill = new Skill(); skill.id_Skill = id; skill.unlocked = unlocked; skill.cost = cost; skill.skill_Dependencies = dependencies; // Create string with ID info nodeTitle = new StringBuilder(); nodeTitle.Append("ID: "); nodeTitle.Append(id); } public void Drag(Vector2 delta) { rect.position += delta; rectID.position += delta; rectUnlocked.position += delta; rectUnlockLabel.position += delta; rectCost.position += delta; rectCostLabel.position += delta; } public void MoveTo(Vector2 pos) { rect.position = pos; rectID.position = pos; rectUnlocked.position = pos; rectUnlockLabel.position = pos; rectCost.position = pos; rectCostLabel.position = pos; } public void Draw() { inPoint.Draw(); outPoint.Draw(); GUI.Box(rect, title, style); // Print the title GUI.Label(rectID, nodeTitle.ToString(), styleID); // Print the unlock field GUI.Label(rectUnlockLabel, "Unlocked: ", styleField); if (GUI.Toggle(rectUnlocked, unlocked, "")) unlocked = true; else unlocked = false; skill.unlocked = unlocked; // Print the cost field GUI.Label(rectCostLabel, "Cost: ", styleField); skill.cost = int.Parse(GUI.TextField(rectCost, skill.cost.ToString())); } public bool ProcessEvents(Event e) { switch (e.type) { case EventType.MouseDown: if (e.button == 0) { if (rect.Contains(e.mousePosition)) { isDragged = true; GUI.changed = true; isSelected = true; style = selectedNodeStyle; } else { GUI.changed = true; isSelected = false; style = defaultNodeStyle; } } if (e.button == 1 && isSelected && rect.Contains(e.mousePosition)) { ProcessContextMenu(); e.Use(); } break; case EventType.MouseUp: isDragged = false; break; case EventType.MouseDrag: if (e.button == 0 && isDragged) { Drag(e.delta); e.Use(); return true; } break; } return false; } private void ProcessContextMenu() { GenericMenu genericMenu = new GenericMenu(); genericMenu.AddItem(new GUIContent("Remove node"), false, OnClickRemoveNode); genericMenu.ShowAsContext(); } private void OnClickRemoveNode() { if (OnRemoveNode != null) { OnRemoveNode(this); } } } |
Now we should edit how we create the node in the NodeBasedEditor class with the new info for the node.
1 2 3 4 5 6 7 8 9 10 11 12 |
private void OnClickAddNode(Vector2 mousePosition) { if (nodes == null) { nodes = new List<Node>(); } // We create the node with the default info for the node. nodes.Add(new Node(mousePosition, 200, 100, nodeStyle, selectedNodeStyle, inPointStyle, outPointStyle, OnClickInPoint, OnClickOutPoint, OnClickRemoveNode, 0, false, 0, null)); } |
Ok, so now, we can create new nodes and draw info like the ID, if it’s unlocked or not and the cost of that skill. But we need to get the data from our new window and apply it to our previous skill tree system, right? Let’s see how we can accomplish that.
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 |
// Save data from the window to the skill tree private void SaveSkillTree() { if (nodes.Count > 0) { // We fill with as many skills as nodes we have skillTree.skilltree = new Skill[nodes.Count]; int[] dependencies; List<int> dependenciesList = new List<int>(); // Iterate over all of the nodes. Populating the skills with the node info for (int i = 0; i < nodes.Count; ++i) { if (connections != null) { List<Connection> connectionsToRemove = new List<Connection>(); List<ConnectionPoint> connectionsPointsToCheck = new List<ConnectionPoint>(); for (int j = 0; j < connections.Count; j++) { if (connections[j].inPoint == nodes[i].inPoint) { for (int k = 0; k < nodes.Count; ++k) { if (connections[j].outPoint == nodes[k].outPoint) { dependenciesList.Add(k); break; } } connectionsToRemove.Add(connections[j]); connectionsPointsToCheck.Add(connections[j].outPoint); } } } dependencies = dependenciesList.ToArray(); dependenciesList.Clear(); skillTree.skilltree[i] = nodes[i].skill; skillTree.skilltree[i].skill_Dependencies = dependencies; } string json = JsonUtility.ToJson(skillTree); string path = null; path = "Assets/SkillTree/Data/skilltree.json"; // Finally, we write the JSON string with the SkillTree data in our file using (FileStream fs = new FileStream(path, FileMode.Create)) { using (StreamWriter writer = new StreamWriter(fs)) { writer.Write(json); } } UnityEditor.AssetDatabase.Refresh(); } } |
We must include a button for running this SaveSkillTree method. And let’s think about what are we going to need in the future. I think we are totally going to need a button for loading the SkillTree from the file as well. And why not a function for wiping all the data from our editor window, just in case we want to start the SkillTree from the scratch or load the saved SkillTree and remove all of our current data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Function for clearing data from the editor window private void ClearNodes() { nodeCount = 0; if (nodes != null && nodes.Count > 0) { Node node; while (nodes.Count > 0) { node = nodes[0]; OnClickRemoveNode(node); } } } |
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 |
private void LoadNodes() { ClearNodes(); Skill[] _skillTree; List<Skill> originNode = new List<Skill>(); skillDictionary = new Dictionary<int, Skill>(); string path = "Assets/SkillTree/Data/skilltree.json"; string dataAsJson; Vector2 pos = Vector2.zero; 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 skillData = JsonUtility.FromJson<SkillTree>(dataAsJson); // Store the SkillTree as an array of Skill _skillTree = new Skill[skillData.skilltree.Length]; _skillTree = skillData.skilltree; // Create nodes for (int i = 0; i < _skillTree.Length; ++i) { if (nodes == null) { nodes = new List<Node>(); } nodes.Add(new Node(Vector2.zero, 200, 100, nodeStyle, selectedNodeStyle, inPointStyle, outPointStyle, OnClickInPoint, OnClickOutPoint, OnClickRemoveNode, _skillTree[i].id_Skill, _skillTree[i].unlocked, _skillTree[i].cost, _skillTree[i].skill_Dependencies)); ++nodeCount; if (_skillTree[i].skill_Dependencies.Length == 0) { originNode.Add(_skillTree[i]); } skillDictionary.Add(_skillTree[i].id_Skill, _skillTree[i]); } Skill outSkill; Node outNode; // Create connections for (int i = 0; i < nodes.Count; ++i) { for (int j = 0; j < nodes[i].skill.skill_Dependencies.Length; ++j) { if (skillDictionary.TryGetValue(nodes[i].skill.skill_Dependencies[j], out outSkill)) { for (int k = 0; k < nodes.Count; ++k) { if (nodes[k].skill.id_Skill == outSkill.id_Skill) { outNode = nodes[k]; OnClickOutPoint(outNode.outPoint); break; } } OnClickInPoint(nodes[i].inPoint); } } } } } |
Now we need to print the buttons that allow you to use these methods. We can do it running this new method in every OnGUI call.
1 2 3 4 5 6 7 8 9 10 |
// Draw our new buttons for managing the skill tree private void DrawButtons() { if (GUI.Button(rectButtonClear, "Clear")) ClearNodes(); if (GUI.Button(rectButtonSave, "Save")) SaveSkillTree(); if (GUI.Button(rectButtonLoad, "Load")) LoadNodes(); } |
Oh, and don’t forget to create those Rect that we are using in the previous function for printing our buttons.
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 |
private void OnEnable() { // Create the skilltree skillTree = new SkillTree(); nodeStyle = new GUIStyle(); nodeStyle.normal.background = EditorGUIUtility.Load("builtin skins/lightskin/images/node5.png") as Texture2D; nodeStyle.border = new RectOffset(12, 12, 12, 12); selectedNodeStyle = new GUIStyle(); selectedNodeStyle.normal.background = EditorGUIUtility.Load("builtin skins/darkskin/images/node5 on.png") as Texture2D; selectedNodeStyle.border = new RectOffset(12, 12, 12, 12); inPointStyle = new GUIStyle(); inPointStyle.normal.background = Resources.Load("green") as Texture2D; inPointStyle.active.background = EditorGUIUtility.Load("builtin skins/darkskin/images/btn left on.png") as Texture2D; inPointStyle.border = new RectOffset(4, 4, 12, 12); outPointStyle = new GUIStyle(); outPointStyle.normal.background = Resources.Load("red") as Texture2D; outPointStyle.active.background = EditorGUIUtility.Load("builtin skins/darkskin/images/btn right on.png") as Texture2D; outPointStyle.border = new RectOffset(4, 4, 12, 12); // Create buttons for clear, save and load rectButtonClear = new Rect(new Vector2(10, 10), new Vector2(60,20)); rectButtonSave = new Rect(new Vector2(80, 10), new Vector2(60, 20)); rectButtonLoad = new Rect(new Vector2(150, 10), new Vector2(60, 20)); // Initialize nodes with saved data LoadNodes(); } |
Oops, did you see what happened here? As we didn’t have information about the node we didn’t know where to draw each of the nodes when loading. Maybe we should save the position of the nodes too…
For that, we can define new classes for managing data from the node. We are only going to need an ID for each one of the nodes and a Vector2 with the position that must be saved.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using UnityEngine; [System.Serializable] public class NodeData { public int id_Node; public Vector2 position; } [System.Serializable] public class NodeDataCollection { public NodeData[] nodeDataCollection; } |
We need a new method for saving to a new file. Also, we must modify the LoadNodes function for adding the nodes to the correct position previously saved.
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 |
// Save data from the nodes (position in our custom editor window) private void SaveNodes() { NodeDataCollection nodeData = new NodeDataCollection(); nodeData.nodeDataCollection = new NodeData[nodes.Count]; for (int i = 0; i < nodes.Count; ++i) { nodeData.nodeDataCollection[i] = new NodeData(); nodeData.nodeDataCollection[i].id_Node = nodes[i].skill.id_Skill; nodeData.nodeDataCollection[i].position = nodes[i].rect.position; } string json = JsonUtility.ToJson(nodeData); string path = "Assets/SkillTree/Data/nodeData.json"; using (FileStream fs = new FileStream(path, FileMode.Create)) { using (StreamWriter writer = new StreamWriter(fs)) { writer.Write(json); } } UnityEditor.AssetDatabase.Refresh(); } |
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 |
private void LoadNodes() { ClearNodes(); string path = "Assets/SkillTree/Data/nodeData.json"; string dataAsJson; NodeDataCollection loadedData; 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 loadedData = JsonUtility.FromJson<NodeDataCollection>(dataAsJson); Skill[] _skillTree; List<Skill> originNode = new List<Skill>(); skillDictionary = new Dictionary<int, Skill>(); path = "Assets/SkillTree/Data/skilltree.json"; Vector2 pos = Vector2.zero; 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 skillData = JsonUtility.FromJson<SkillTree>(dataAsJson); // Store the SkillTree as an array of Skill _skillTree = new Skill[skillData.skilltree.Length]; _skillTree = skillData.skilltree; // Create nodes for (int i = 0; i < _skillTree.Length; ++i) { for (int j = 0; j < loadedData.nodeDataCollection.Length; ++j) { if (loadedData.nodeDataCollection[j].id_Node == _skillTree[i].id_Skill) { pos = loadedData.nodeDataCollection[j].position; break; } } LoadSkillCreateNode(_skillTree[i], pos); if (_skillTree[i].skill_Dependencies.Length == 0) { originNode.Add(_skillTree[i]); } skillDictionary.Add(_skillTree[i].id_Skill, _skillTree[i]); } Skill outSkill; Node outNode; // Create connections for (int i = 0; i < nodes.Count; ++i) { for (int j = 0; j < nodes[i].skill.skill_Dependencies.Length; ++j) { if (skillDictionary.TryGetValue(nodes[i].skill.skill_Dependencies[j], out outSkill)) { for (int k = 0; k < nodes.Count; ++k) { if (nodes[k].skill.id_Skill == outSkill.id_Skill) { outNode = nodes[k]; OnClickOutPoint(outNode.outPoint); break; } } OnClickInPoint(nodes[i].inPoint); } } } } else { Debug.LogError("Cannot load game data!"); } } } private void LoadSkillCreateNode(Skill skill, Vector2 position) { if (nodes == null) { nodes = new List<Node>(); } nodes.Add(new Node(position, 200, 100, nodeStyle, selectedNodeStyle, inPointStyle, outPointStyle, OnClickInPoint, OnClickOutPoint, OnClickRemoveNode, skill.id_Skill, skill.unlocked, skill.cost, skill.skill_Dependencies)); ++nodeCount; } |
And that should be all 🙂 I hope I didn’t mess with the code nor the writing of this article. Just in case, I’m going to put all of the scripts in here, remember you can double-click on it for enabling the full code and that you can copy it to your clipboard.
You can also download an unitypackage from here. There you can find the folder structure with all of the scripts, two images that I’m using for the connection points in the node, and the code for the SkillTree of the previous post.
1 2 3 4 5 6 7 8 9 10 |
using System; using UnityEditor; using UnityEngine; using System.Text; public class Node { public Rect rect; public string title; public bool isDragged; |
1 2 3 4 5 6 7 8 9 10 |
using UnityEngine; using UnityEditor; using System.Collections.Generic; using System.IO; public class NodeBasedEditor : EditorWindow { private List<Node> nodes; private List<Connection> connections; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using UnityEngine; [System.Serializable] public class NodeData { public int id_Node; public Vector2 position; } [System.Serializable] public class NodeDataCollection { public NodeData[] nodeDataCollection; } |
At the end…
So here we are, finally, with our custom editor window for editing our skill tree file. Now it’s easier to manage the creation and modification of our SkillTree but the player isn’t able to use it yet! Seems like that’s our next step down the road…
On the next tutorial of this series, we are going to make the player able to use our SkillTree and save player’s progress. So it seems like I’m about to make a basic game, any thoughts or suggestions?
I hope you liked this tutorial and I’m looking forward to seeing you again on the next post 😉 As always feel free to comment or to ask about anything.
Do you have any ideas for writing articles? That’s where I constantly battle and I just wind up
looking empty display for very long time.
Hello Darrel,
Same thing happens to me. I left the blog for a year without any new posts. Do you have any subject that you would like me to talk about?
For me, the connection point in unity is invisible. I can still connect things but it might be confusing for my team when they look at my project.
Is there a way to colour them
Hello Benj,
What Unity version are you using? I made this with Unity 2017 in mind, so I’m not sure if it’s still working on new Unity versions.