Let’s continue our RTS and keep working on our technology trees!
This article is also available on Medium.
Last week, we started to implement the last big feature of “phase 1” of our RTS game: the technology tree! We discussed the overall structure and we even created a simple example tech tree that was able to impact different other systems in our game: the purchase costs and the unit’s attack damage value.
But so far, we’ve had to do everything via our debug console, which is of course not the way to expose this info to the players! So, today, let’s continue working on our tech trees and see how to display them in the UI in a readable and intuitive manner. Then, we’ll also add a bit of logic to actually save and reload the current state of the tree so that, when we reload a game session, we get back all of our unlocked techs 🙂
Displaying our tech tree in the UI and enabling research
Creating the panel and toggling it
At the moment, our game scene UI already contains two panels: the settings panel and the menu panel. The tech tree panel will be very similar – it will also be opened via a button in the top bar, and it will also have a background overlay we can click to close it back:
The logic to use in our
UIManager is exactly the same as for the other panels:
Note: remember to assign the
ToggleTechnologyTreePanel() method to the on-click callback of the top bar button 😉
The thing now is that we want to (re-)instantiate the tree in the middle of the panel dynamically, so that it is up-to-date with the currently unlocked techs, and that it automatically shows all the tech nodes in a nice and clear way.
For the nodes, I’ll start with a very simple prefab that is just a hierarchy of images and text UI elements:
Now – we could naturally just list our available techs and their current status (unlocked, available or locked) with a basic vertical layout… but, it would make it quite hard for the player to properly understand the relationships between the different nodes!
Since we have a tree, we should instead display it as we normally do for trees, with little blocks for the nodes and connecting lines for the edges. So, in the end, I should have something that looks like this (here I’m showing a more complex example of a tech tree than the one we created last time so you see how it works on more prod-like data):
But this however poses two questions:
- how can we dynamically place our nodes so that, no matter what we put in our tree, we get a readable and balanced display? (here, for example, the fact that I have all my branches neatly separated, spaced out and nonetheless vertically aligned)
- how can we draw the edges dynamically, too, so that they properly join the blocks?
Well – let’s dive deeper and see how we can tackle those two issues! 🙂
Placing the nodes
For now, we’ll ignore the edges and just focus on the nodes. What we want to do here is iterate through our tech node instances and associate with each piece of data a UI element on the screen, but also compute the “best position” for it so that the tree is intuitive to read for the players.
For example, we’d like to put some space between the tech levels (i.e. the parents and their children), but we’d also like for the nodes to be well-spread horizontally, with the various tech branches neatly separated.
There are several possible algorithms to draw directed graphs, but one that often works great is the Reingold-Tilford algorithm. As explained in this really cool article by Rachel Lim, this algorithm allows you to draw balanced and clean trees with just a few steps.
Disclaimer: I’ve actually mostly re-adapted her C# implementation to fit my own objects, but the overall logic comes from this article, so kudos to her! 🙂
If you want an in-depth explanation of the algorithm and how the code is built gradually, you can refer to Rachel’s article. Here, I’ll just give you the different classes we need to add to our RTS project “as-is”, and a quick explanation of how they work.
The first class we need to prepare is the
TechnologyNodeVisualizer: this will be the UI visual representation of a
TechnologyNodeData instance. It has a reference to the node data in a
_node field, a bunch of variables and accessors for the drawing itself and a
Draw() function that actually instantiates a prefab at the computed position.
Note: to avoid reloading the same node UI prefab again and again, I used the Singleton pattern with a private accessor – this is a nice trick to reduce memory consumption and variable allocation 😉
This is the core element for our tree display – but as you can see, it doesn’t have any logic to actually compute its position. This is handled by another script, the
TechnologyTreeLayouter, which is my own re-adaptation of Rachel’s code. The class has a set of static functions (both public and private) that are used to compute the best positions for all of our nodes. I won’t copy it here since it’s pretty long, but as usual you can check it out on the Github of the project 🚀
Note: this file has some static variables at the top to define the size of the nodes on the screen, or the distance between them – feel free to adapt these to better fit your own UI! 🙂
Just as bird’s-eye view, however, it does expose three important functions that our other classes will call:
InitializeNodeVisualizer(): this computes an initial position for a node UI representation based on its depth (= its level) in the tree and the arbitrary node size we defined (the
NODE_HEIGHTvariables, plus the
ComputeInitialXPosition(): this does a first pass on all the nodes recursively (children first) and aligns all the nodes so that the parents are above their children, properly centred and spaced out
ComputeFinalXPosition(): this does a second pass on all the nodes recursively (children last) to re-center the entire layout and prevent the leftmost and rightmost branches from going outside the frame
Thanks to all of these, we can now create a third script, the
TechnologyTreeVisualizer, that creates the
TechnologyNodeVisualizer instances for all the nodes in our tree and places them with the layouter – this is done recursively by going down the tree and calling the various layout functions on the nodes and their children:
You’ll notice that I also need the parent UI element to put all of these contents in and the width of this parent so I can re-center the entire tree.
Finally, all that’s left to do is call this
DrawTree() function from our
UIManager when we open the panel – for now, I’ll only do it if I haven’t initialised the tech tree display yet:
If we run the game, we can now open the panel and tadaa! We get our little tech tree from last time with a nice tree-layout display! 🙂
Adding the edges
Step 2 is to add some edges between those nodes so that we get a clear understanding of the relationships in our tree!
Now, because we are using Unity’s new UI (canvas-based) system, with the canvases and stuff, it’s not going to be as easy as calling the
GUI.DrawTexture() Unity built-in or using a
LineRenderer component – those don’t work with this UI system. The question is: how can you easily render a 2D line with the Unity UI canvas system?
As superbly explained in yet another great video by Game Dev Guide, the answer is to create a custom UI renderer; this is done by implementing your own C# script and have it inherit from the
Graphic Unity built-in class – for example, in our case, we’ll make a
That’s fine and all, but how do we actually make a line in C# code?
Well – in Unity, UI canvas elements are just like the rest of your 3D and 2D objects in the scene: they are actual meshes that are rendered and drawn by the camera. This means that whenever the UI canvas is “dirtied” and asks for the UI elements inside its hierarchy to re-update, each element will compute and send its mesh data so that it can be properly re-drawn on the screen. Therefore, the idea of our custom renderer is to make a procedurally generated 2D geometry that renders the proper line UI element.
Because it is a
UILineRenderer class allows us to precisely tell how we want to compute its “on-screen mesh”. This mesh definition is done in the
OnPopulateMesh() method that we can override to fit our needs; here, I’ve adapted Matt’s code (which is a bit more complex to integrate a grid-based scale system), and so this gives me the following code:
And now, we can super easily convert any of our UI elements to draw lines on the screen easily, yay!
Note: my experiments show that Unity has some difficulties “initialising” the render from scratch – if you use an empty GameObject and add the
UILineRenderer to it, you won’t get a RectTransform, or default UI material to show it. So I find it easier to start off a “ready-made” UI element, like an Image, and replace the rendering component with my own 😉
Using this neat trick, we can create another prefab, this time for our tech tree edges, with the following setup:
Now, we can use it in our
TechnologyTreeLayouter, in our
DrawEdges() method. Basically, in this method, we just need to create:
- a vertical line between the parent and the middle-point above its children (blue)
- then a horizontal line that goes from the leftmost to the rightmost child X positions, in-between the two levels on the Y axis (red)
- and finally the vertical lines between this horizontal separator and the child node UI elements (green)
Again, you can find this method in the
TechnologyTreeLayouter.cs file on the Github 🚀
And of course, we call
DrawEdges() recursively so that the nodes keep on drawing edges all throughout the tree…
Finally, we can call this function in our
Note: make sure to call it before the nodes are drawn so that the edges UI elements are higher in the hierarchy and therefore drawn first, behind the nodes 😉
Now, if we open the panel again, we get very basic and readable lines for our tree edges!
Showing the node state
Another improvement we can make to this UI display is to show the player the current state of each tech node (that can be: unlocked, available or locked). Visually, this state will determine both:
- the colour of the node’s UI element border: unlocked nodes will be “golden”, available nodes will be “pinkish” and locked nodes will be blue
- the interactivity of these elements: all nodes will use a UI Button component but only the available nodes will be actually interactable
To do all of this, I updated my prefab a bit. Now, I have a button instead of a basic image, some wood texturing for kicks… and I also made sure that, when the node is not interactable, the button has a blueish tint:
This will make things easier for the “available” and “locked” states; however, if the node has been “unlocked”, it will make for a greenish colour because it will mix with the gold tint. So we have to insure the disabled colour is reset to a plain white in that case!
All of this colouring, interactivity checks and optional reset is done in the
Draw() method of our
Note: you can’t directly assign just a single button-state colour – you need to re-assign the entire
ColorBlock all at once; so the trick is usually to create a temporary copy, modify this copy and re-assign the copy to your button’s colours 😉
Unlocking nodes from the UI and re-integrating the research costs and time
Ok now – the whole point of this UI is to show the available and unlocked technologies… but also to allow the player to research and unlock new ones! So the final thing we need to do is actually connect a callback function to our node buttons so that, if we click one that is available, it starts the research phase and ultimately unlocks the tech.
That’s also a good time to re-integrate our research costs and time variables. When we click on a tech, if we can buy it, we want to start the research and unlock it only after the given amount of time.
So we can already upgrade our UI node prefab a bit to also add a progress bar beneath the image:
Here, you’ll notice that I’m once again using the “Filled” mode for my progress bar fill image so that I can easily update its “Fill Amount” parameter from my C# scripts.
For the techs costs, I’ll add a little container in the top-right corner of my panel – it’s invisible on its own, but it does have a LayoutElement component so that it can ignore the layout of its parent, and it uses a HorizontalLayoutGroup to automatically place its children.
In order to catch the “mouse enter” and “mouse exit” events, and to properly send the node data to our UI manager and populate these costs display, we’ll use the same technique as we did for the skill buttons and create a little class called
Don’t forget to actually add this script to your tech node UI element prefab:
Of course, we also want our
TechnologyNodeVisualizer class to initialise this script (note that if the node is already unlocked, we don’t need the script anymore so we can just destroy it):
And finally, in our
UIManager, we can register this event and use various callbacks to populate or clear the costs display – the logic is the same as for the info panel before 😉
Now, if we hover our node, we directly see their cost in the top-right corner:
The next step is to add the callback so that when we click the node of a tech we can afford, we start the research – again, we are simply going to emit an event when we click the button (we’ll assign it in our
TechnologyNodeVisualizer class, so that our
UIManager can react to it and start a coroutine:
This means however that we are going to need a reference to this progress bar UI element, in order to update it throughout the research phase; so let’s:
- add an accessor on our progress bar in the
- return some references to these elements (keyed by each node unique code) from our
TechnologyTreeVisualizerwhen we draw the tree:
Then, in the
UIManager, we can use all of these references and our new event to run a coroutine that consumes the required resources, updates the progress bar fill and, after it’s waited for the proper time, actually unlocks the tech:
Once we’re done, we’ll also redraw the tree so that it is up-to-date with the latest data state… and here we are! 🙂
Note: if you don’t want to show the “Root” node, you can simply do an early return in the
Draw() method and check its code against the
ROOT_NODE_CODE variable… or you could use another prefab to get a custom display for this node 🙂
Storing and reloading the tech tree
To wrap up this second part of the tech tree sub-series, let’s make sure that our current tech advancement is stored in our game session data; this way, if we reload a game, we’ll have all our unlocked nodes restored automatically (with the matching bonuses, of course).
Important note: we are going to modify our
GameData class, which means that previous saves will not be compatible with our new format anymore! You won’t be able to reload your old saves – these will get you null reference errors!
It’s in fact pretty easy to do – as we said last time, we just need to remember the unique codes of the unlocked nodes, and then read back those codes when we load the data. We already have all the static methods we need, so all we have to do is update our
And then update our
DataHandler to use this new array of codes:
And now, if we save our game and reload it later, we’ll get back all of our unlocked tech tree nodes 🙂
Today, we’ve finished our simple tech tree system and we now have a functional in-game mechanic! Players can see their current technologies, research new ones for some given amount of resources, and all this data is safely stored and reloaded from one play session to the other.
There are evidently lots of improvements or extra features we could add to this tech tree system. For example, it would be pretty quick to add “mutually exclusive branches”, just by having a list of “contradictory nodes” and checking to see if any are currently unlocked. But those are what make your game really unique, so I’d rather leave your imagination run free…
… and focus on a more widespread issue: the edition of this tech tree! 🙂
Indeed, from a designer’s perspective, editing the tech tree global structure (i.e. the Scriptable Objects we use as reference and load back when the game scene starts) is not very easy. Connecting all the children is a long and boring process when you have to manually check the lists… and you run the risk of making some mistakes because you have to keep a mental representation of your tree as you do it.
So, in the third and final part of this sub-series of tutorials on tech trees, we’ll see how we can make a dedicated Unity editor tool to facilitate the visualisation and edition of our tech trees out of play mode!