Making a RTS game #52: Implementing a technology tree 3/3 (Unity/C#)

Let’s continue our RTS and keep working on our technology trees!

This article is also available on Medium.

In the last two episodes, we talked about technology trees and we implemented a basic system that allows us to build a tree out of nodes, display it in our game UI with a nice positioning algorithm and then research new techs that are stored along with the rest of the game session data.

However, for now, editing the overall hierarchy of our tech tree isn’t very easy: we need to manually select each tech node Scriptable Object, update its values and optionally its children, to create the right connections, all this while keeping a mental representation of the current shape of the tree.

This is clearly not ideal for game designers!

So, today, we’re going to wrap this up by creating a Unity editor tool to make the edition of our tech trees easier 🙂

Note: this tutorial is fairly long compared to others, and it is not absolutely essential. But it does introduce some interesting concepts in terms of tooling, and I think it’s a nice way of ending our adventure together, by focusing more on the dev and design of your game than direct in-game features 🙂

Here is a quick demo of what we’ll have by the end:

Important note: during this tutorial, we’ll consider that the “root” node of the tree is uniquely identified by its specific code, root. There are other ways we could determine which node is root (with a isRoot boolean flag, for example) that you might prefer – don’t hesitate to experiment and pick one that you like!

A quick overview

Before we dive into the code itself, let’s just take a minute to discuss what we are actually going to do. As you can see from the demo above, what I want is to create a new dockable window for my Unity workspace, just like the inspector or the “Game” tab, that contains a visual representation of my tech tree as well as some parametrisation tools in a sidebar.

Since all of this lives in edit mode, we’ll obviously need to rely a lot on the UnityEditor package (just like we did a long time ago for our custom game parameters Inspectors) – and we’ll be displaying our elements via the IMGUI, or Immediate Mode GUI.

The overall idea is to:

  • create our own (dockable) editor window by making our C# class inherit from the EditorWindow built-in class
  • “replace” the default Inspector for our tech node Scriptable Object assets so that they open this window, instead
  • populate this window with the nodes currently in the tree (and their relationships) when we open it
  • allow the user to add/remove nodes and add/remove relationships
  • allow the user to edit the parameters of the nodes (except for the root) in a sidebar resizable panel
  • (allow the user to move around with a grid to better visualise the nodes and their dependencies)

Note: the grid will just be a visual aid, we won’t talk about snapping or grid-based coordinate systems in this article! 😉

Ok, that’s quite a lot, so let’s get to it!

Disclaimer: a good chunk of this tool comes from this blog post on Gram Games’s website – big thanks to them for sharing all this nice info on the basics of making a node-based graph tool in Unity. However, I won’t be going through the creation of this tech tree editor in the exact same order as they do, because we already have some data structures we need to re-integrate so some parts are a bit more involved than in their implementation 😉

How to make our own editor window in Unity?

The first step is to create this new window. This can be done fairly easily by creating a new C# script and placing it in the “Editor” folder of our project. Remember, this is the directory that contains all “pure-editor” scripts, i.e. scripts that will not get compiled when you build your game and can use the Unity editor API to interact with your dev workspace.

So let’s create a new class called TechnologyTreeWindow:

It inherits from the EditorWindow class, and by adding a little [MenuItem] attribute, we’ve made it accessible from the top-menu, like this:

Adding the grid, the resizable side panel and the basic movements

Now, we need to add our background grid so that we have a bit of a playground to work with! As explained at the end of the Gram’s post, the idea is to store the current drag and offset of the grid and then re-apply it when we draw the background in our OnGUI() method:

To actually draw this grid, we can rely on the Unity’s built-in Handles tool that makes it easy to draw and colour lines:

And the _drag is updated based on our mouse events, in our _ProcessEvents() function:

At that point, we have a nice infinite draggable grid that automatically updates as we click and drag (note that I changed the colours compared to the script so it’s more visible in the video!):

Now, time to add our resizable sidebar 🙂

The idea is basically to keep track of the sidebar width ratio compared to the editor window’s total width, and draw the panel with the right dimensions. Again, taking our cue from Gram’s blog, we can refer to another article where they talk about resizable panels to implement our own on the left side of the window.

To choose how our sidebar background and resizing bar look like, we can use a GUIStyle: those are Unity’s way of defining the visual parameters of a GUI element, such as the background image for example.

Once defined, those can be passed directly to GUI boxes or areas to apply this style upon render in our _DrawSidePanel() method:

The position variable is a Rect that is automatically defined because our TechnologyTreeWindow inherits from the EditorWindow class and that gives us the current (X,Y)-position and size of the window in the Unity editor workspace. For the GUI styles, you have several ways of populating them and getting the built-in Unity images, but here I’m using a really cool tool script that creates a new window in the editor to list all the available textures and images, and get their proper names.

Finally, we just need to implement the interaction so that, if we start to drag and we’re hovering the resizer, we resize the sidebar panel instead of moving the grid:

We now have a nice sidebar on the left of our tech tree window that we can adjust to show more or less of our grid (and we still have our click-and-drag interaction from before if we’re not hovering the resizer) 🙂

Linking this window to our Scriptable Objects

But of course, we want to populate this window with some data, now! More precisely, we want to consider all of the tech node Scriptable Objects in a directory and show them as a tree in our window, starting from the root.

To do this, I’ve decided to:

  • “cut” my default TechnologyNodeData Inspector by creating my own TechnologyTreeNodeEditor class
  • have this class open the tech tree window if I’m selecting the root…
  • … or just display an info message telling the user to select root otherwise!

That’s because our current structure only stores “local” relationships (i.e. direct parent-to-children), so we can’t just pick any node and hope to rebuild the entire tree: in order to get our full data, we must start from the root.

But anyway – all of this is done in our new TechnologyTreeNodeEditor C# class, like this:

Except that you can see when we click on the “Open Editor” button, we are supposed to open the window and pass it some data – which is not yet implemented in our TechnologyTreeWindow

So let’s update our window script and have it accept incoming data by creating an override for the ShowTreeWindow() static method. While we’re at it, we can even update our previous version of the ShowTreeWindow() to try and auto-load the data if we are currently selecting the root node:

Now, if we select our various tech node Scriptable Object assets, depending on whether it’s the root or not, we either show the message or open the tech tree window:

Important note: during dev, as we’ll keep on recompiling our scripts, the window will be cleared of its data. So you’ll note to click the “Open Editor” button again to re-populate the window all throughout your tests.

Displaying the nodes

You probably guessed it: the CreateGraph() method is going to be one of the core parts of our window script – it’s where we’ll read the structure of the tree, instantiate the visual IMGUI elements for each data node in the tree and, ultimately, the connections between them.

Showing the nodes

For now, we’ll start with the nodes themselves. It’s a bit like what we did last time to display the tech tree in the in-game UI: we’ll need to associate our data nodes with actual GUI elements that have a position and a size on screen.

These visual representations will be instances of another C# class: the TechnologyTreeEditorNode (be careful, it’s not the same as the TechnologyTreeNodeEditor we just implemented!):

Note: again, we’re using a GUIStyle to get a nice visual render of our node 😉

Now that we have this class, we can:

  • iterate through our tree recursively (starting from the root TechnologyNodeData instance we were given) and associate a GUI element to each
  • go through this list of GUI elements and draw them (using their Draw() method) during the OnGUI() phase

And tadaa! We now have nodes on our graph! 🙂

Dragging the nodes

The whole point of this tool is to make it easier to edit our graph, so we definitely need to be able to move and reorder our nodes quickly. Again, we are going to use the click-and-drag interaction; except that this time, we’ll want to process the mouse events on our nodes.

This will be done in another function that we call in our OnGUI() method, _ProcessNodeEvents():

And this method in turn simply calls a ProcessEvents() method on the visual GUI node itself where we check to see whether the mouse is inside the rect of this node, and if need be update the node’s position:

Selecting the nodes

Next to-do on our list is being able to select those nodes – which will have two consequences:

  • the style of the node will change: we’ll update our GUIStyle variable and switch to the “selected” style so the user can quickly catch which node is currently selected
  • we’ll record this selected node so that, later on, we can edit its parameters in the sidebar

I want the node to be selected when I click on it, and deselected if I click anywhere on the background grid. So to properly react to this “on click” event, I’ll need to pass some additional info to my TechnologyTreeEditorNode when I first create it, in the form of an Action (i.e. a callable function). Then I can just call this method in my TechnologyTreeEditorNode class if I’m clicking on the node and selecting it (along with changing the GUI style).

Finally, back in my TechnologyTreeWindow, I can show very basic info on the selected node in my sidebar, if I have one, or else a message to tell the user to select a node in the grid:

Here’s the result in the tool:

Adding the connections

Ok – we can now visualise, select and drag our nodes easily! Now, of course, we want to show (and edit) the connections between those nodes so that we don’t have to manually fill our children fields anymore 🙂

Preparing the C# classes

To represent these connections in our GUI graph, we are going to use two extra classes: Connection and ConnectionPoint. The ConnectionPoints will be small buttons above and beneath our nodes that we can click to create new edges in the graph, and the Connection will be the actual line between two nodes that are parent and child. Don’t forget we should also have a little square in the middle of the edge to destroy it quickly:

The two classes are pretty straight-forward, they just define some Draw() method and various accessors to the different GUI elements, the node data instances or GUI styles and callback actions:

Note: if you want a more sophisticated render with nice bezier curves, you can use the Handles.DrawBezier() utility as shown in the Gram’s blog post 🙂

Drawing the connections between the nodes

Now that we’ve prepared our two classes, we just need to use them:

  • in the TechnologyTreeEditorNode when we create the object and when we draw it:
  • in the TechnologyTreeWindow when we first create the graph, and then when we draw it (note that we need to wait for all the nodes to be processed before creating the connections or we might have some null references for the links, so we do a second loop):

And since all that is IMGUI, and we continuously redraw the elements on the screen, the edges will directly follow the nodes as we drag them around!

Creating a new connection

At the moment, we’ve prepared an empty callback for the “on connection point click” event. In fact, this function needs to handle a few cases:

  • I’m clicking an “in”/”out” point and I had not clicked any other point before: I select this “in” or “out” point and store it in a private variable to be able to create a connection in the future
  • I’m clicking an “in” (resp. “out”) point and I had clicked on an “in” (resp. “out”) point before: I “erase” the previous selection and replace it
  • I’m clicking an “in” (resp. “out”) point and I had clicked on an “out” (resp. “in”) point before: I link the two nodes attached to these connection points

This is done with just a bunch of if-checks and variable re-assigns, like this:

To create a new connection, we simply access the actual data Scriptable Objects assets and update their children list accordingly, then create the GUI element and add it to our list of _connections:

However, since we’re editing Scriptable Objects on-the-fly, we want to make sure that they are properly saved and re-serialised by Unity. To force this, we can use the EditorUtility and AssetDatabase packages to access the node asset, mark it as “dirty” and then ask for an immediate save of the assets:

And finally, we can even draw a temporary connection to give the user a clear feedback of the fact he’s currently preparing a new edge:

Removing a connection

Destroying an edge between two nodes is even simpler: we have a quick reference to its source and target nodes, so we just have to update the children and “dirty” the source asset again, and we’re done!

Testing it out!

If you go back to your editor, you’ll see you can now see the connections between the node, click on the connection points to create new ones or click on the square on an edge to destroy it:

It’s important to note that because all the graph data is held in the root, nodes that are not connected to the tree will not be reloaded if you re-open the window (remember we go through the children list to populate the graph, so unlinked nodes will just be ignored)…

To fix this, we can simply re-read our Scriptable Objects directory at the end of the CreateGraph() method and check for nodes that would have been “forgotten” by our recursive process (just be careful cause the listing will return both the assets and their meta file, so you have to filter out the .meta files):

Handling the contextual menus

The last two actions we want for our grid tool are the ability to add and to remove a node. There are three things to note about those actions:

  • they need to update the GUI visuals but also the assets in our project folder so that the associated Scriptable Objects are indeed created or destroyed
  • I want to access them by using the mouse right-click and popping up a little contextual menu: if I’m hovering the grid, I’ll get the option to add a node; if I’m hovering a node, I’ll get the option to remove this node
  • removing a node will of course also remove all its connections in the graph

Creating a new node

To have a contextual menu pop if we right-click the grid, we simply need to extend our _ProcessEvents() method so that it also checks for right-clicks with e.button == 1:

And Unity has us covered for creating little menus easily, since we just need to call the GenericMenu() constructor and pass it the various options of the menu along with their callback:

To actually add a node, we want to create an asset with an arbitrary “untitled” code (plus an auto-incremented index) and some default values.

So, first, let’s add an Initialize() method to our TechnologyNodeData class to auto-set some base values:

Now, we can implement our _OnClickAddNode() function in the TechnologyTreeWindow that creates and sets up the new assets, insures it is serialised and saved, and then adds the associated GUI element to the graph:

Removing a node

We can use a similar logic to get a contextual menu directly on a node – in the TechnologyTreeEditorNode, we can upgrade the ProcessEvents() method to check for right-clicks and open a GenericMenu() if need be:

Then, our _OnClickRemoveNode() callback action has to check for the node’s connections, unlink all those edges, remove the GUI element and remove the actual Scriptable Object asset from the folder:

Now, you can add new nodes to your graph, connect them, remove them, etc.! You can see in my project assets, below the window, that new Scriptable Objects are being created or destroyed as I add and remove nodes to the graph:

Editing the values in the sidebar

We’re nearly there! The final step is to use our side panel to show the tech node properties and edit them – we need to re-create this because we don’t have our default Inspector anymore, but also we want to have a bit more control…

In particular:

  • we shouldn’t be able to edit the root node
  • we don’t want to show the children field because this is updated via the edges creation and deletion in the grid
  • when we edit the name of the tech node asset, the associated GUI element needs to update its title too
  • finally, I’d like the asset to be automatically named after the node’s code, by turning the snake_case into PascalCase (so for example attack_booster_2 will turn into AttackBooster2)

All of these are obviously personal preferences and you can absolutely decide to ignore some of these for your own implementation 😉

Most of this will be about updating our _DrawSidePanel() method so that it shows different amount of info depending on the selected node (if there is one), and properly updates the data associated with this node as you change the values in the inputs.

We saw in an earlier episode that we can automate quite a lot of input display thanks to Unity’s very useful built-in PropertyField: this input automatically takes the most optimal form for the type of variable it is showing, and it directly updates the serialised object you are editing.

So a first naive implementation of our sidebar GUI could be:

Now, to limit this to the non-root nodes, we just have to add a little if-check at the beginning:

And the real trick is for the code and displayName fields, because those need to do special things when they are updated.

For the displayName, we want it to be linked to the _title of the node GUI element – so we can actually use a custom setter in our TechnologyTreeEditorNode on the Title property and rather modify this one to update the Scriptable Object under the hood:

This way, all we have to do is, if the field to display an input for is named “displayName”, we show a text field input and use it to update the Title property of the GUI node (which, in turn, will update the displayName of the associated Scriptable Object):

For the code, we want to update the value in the Scriptable Object but also the name of the Scriptable Object asset itself, in the project’s folder. To avoid slowing everything down, however, it’s better to wait for the user to actually press ‘Enter’ or defocus the input (and not update everything as soon as you typed a new character in the text field).

So we can use a DelayedTextField, and check to see if the value has changed before doing our modifications:

The modifications rely on the same utilities and packages we saw earlier, and they first update the node’s code, and then rename the asset with a little reformatting of the value.

With that done, we now have a really cool side panel to configure our nodes easily (again, take a look at the assets beneath to see how they automatically get renamed)!

Bonus: auto-creating a “root” node asset

At the moment, something’s a bit off with our tool: we’ve made it pretty impossible to create a root tech node! If it already exists, there won’t be any issue, but if you want to initialise a brand new tech tree, you might have a bit of a problem because you can’t edit its parameters.

To solve this issue, we can add another menu item to create a node and initialise it as a root – I’ll simply implement a static method in my TechnologyTreeNodeEditor, CreateRootNode():

Note: we could put this function in whichever class we prefer since it is static and actually called from the menu item; I just find it pretty logical to add it here 🙂

The _GetSelectedPathOrFallback() util function is a little snippet that allows us to (somewhat) robustly get the currently selected project folder. It’s not completely safe, which is why I have a fallback to the root Assets/ path but it’s usually able to guess the current folder based on the selected assets.

If you want, you can even add a “validator” on the menu item so that it is only active if there is not a root node already defined in this folder.

First, let’s a new static method to the TechnologyNodeData (when in the Unity editor) to easily try and retrieve the root node Scriptable Object asset in a given folder:

And then, just add a second static function in the TechnologyTreeNodeEditor class with the same menu path as CreateRootNode() but the validate property set to true:

Now, you have a new menu command you can click to create a new tech tree root in any folder, and that will be disabled if there is already one here:

Bonus: auto-selecting the associated “root” node asset from the Inspector

Based on this GetRootNodeInDirectory() utility, we can even improve our TechnologyNodeData custom Inspector so that, for non-root nodes, it retrieves the root node in the same folder (if there is one) and opens it directly, instead of having the user search for it:

Now, rather than having a plain message telling me to find the root node, I can click the “Open Editor” button from other nodes as well and they will crawl back to the top of the tree automatically to show me the entire tree:

Conclusion

And with that third and final episode of the tech tree sub-series, we’re now finished implementing the technology tree system for our RTS, plus a custom-made editor tool to make it easier to edit and re-configure for designers!

Next time, I will wrap up the “phase 1” of my RTS tutorial series with a little retrospective on the 52 articles I posted this year and in particular: various acknowledgements and thanks to dear followers who helped make this series better, what sharing these tutorials taught me, and what the future of this RTS tutorial is!

So stay tuned for the final episode of “phase 1” next Thursday, ’cause I have a little surprise for you… 🙂

Leave a Reply

Your email address will not be published.