Let’s keep working on our RTS and implement some technology trees!
This article is also available on Medium.
As I’ve hinted at in the last episodes, I’m slowly closing this “phase 1” of the Unity RTS tutorial… but don’t worry, I still have some surprises for you, and I will of course continue to discuss Unity, RTS and programming tips in the future 😉
However, there is one last big feature I want to add to our RTS “game skeleton”: the technology tree!
Because this system will be a bit complex, we will build it gradually over the three episodes to come:
- today, we’ll see what game technology trees are, how we are going to represent ours and how we can use our new structure to create a very basic tech tree
- next time, we’ll talk about how to display the tree in our UI, integrate the research costs and time and how to save/load it to retrieve the unlocked techs when we reload a game session
- and finally, we’ll put on our “tool dev” hat and work on an editor tool to edit our tech trees more easily
Ready to dive into this big adventure? Then, let’s go! 🙂
What are tech trees?
Lots of strategy games have some kind of global upgrade system that boosts various “skills” or other global variables: the speed of your units, how much resources you produce each turn, whether you have some extra special units, etc.
Oftentimes, these upgrades are shown as a directed acyclic graph, or a tree. You have some “primary”, low-level techs that you already unlocked or can unlock for a small cost, and then as you go down the tree you get more and more advanced (i.e. powerful) techs that require the previous ones to be unlocked and have a higher cost.
For example, the Civilization series of video games is well-known for offering large and detailed tech trees with multiple levels…
Also, a core idea is that you always get the same “form” of tree from one game session to another, but that the specifics of which tech is unlocked or not depend on each session.
An overview of our tech tree structure
Nodes, edges, root…
As one might expect, the technology tree will, obviously, be… a tree data structure! So it will rely on the fundamental principles of the tree structures:
- at the top, we’ll have the root
- then a hierarchy of nodes: each node can have zero or more children, and those children can themselves have children, and so on…
- these relationships between the nodes are called edges and in our case they will represent the tech dependencies (which tech(s) has(ve) to be “unlocked” to give access to this one)
- the nodes at the very bottom that have no children are called leaves
But contrary to our behaviour tree that was pretty complex and had this execution logic based on specifically-typed “flow nodes” (like the Sequence, the Selector, the Inverter, etc.), our technology tree will be much simpler.
The node class
So much so that we will only need one class to represent all the nodes and tree: a new Scriptable Object type, called the
TechnologyNodeData! This core element will be repeated again and again to get all of our nodes, and it will contain some children and parent relationships to store the edges between our nodes; but all of these can be contained into this single class 🙂
Each technology tree node will have a few characteristics:
- its unique code: a unique identifier used internally to access, store or reference the nodes in our scripts
- its display name: so that we can show the node in the tree in a pretty way in our UI tech tree panel
- its research cost and time: when we want to unlock a new tech, we’ll have to “research” it which will consume both some resources and some time
Note that to make it easier, and because it wouldn’t show any new mechanics, I’ll use my classic in-game resources (gold, wood, stone) but that you could of course add special “research points” or “skill points” resources for this 😉
It will also have a list of children where we’ll be able to reference other
TechnologyNodeData instances to gradually create the edges in our tree and the hierarchy. We will also store the parent of the nodes (mainly for drawing purposes) but we of course don’t want the devs and designers to have to fill both children and parents – it would be a pain and could lead it to some error; therefore, we will auto-compute the parents based on the children when a game (re)starts and we first list and reference of all the technology nodes.
Finally, each node will be in one of the three following states:
- unlocked: the technology has been researched and is already active
- available: the technology can be research, if you have the required resources
- locked: the technology cannot yet be reached (its parent is not unlocked)
These can be represented just by adding an
_unlocked boolean flag to our
TechnologyNodeData class, since the locked state can actually be a check on the parent’s
The nice thing with using Scriptable Objects here is that it will make it pretty straight-forward to edit our technology tree, add new techs or remove them, update their research requirements or even change the edges and the unlock paths… as we’ll see very soon, we’ll just have to update a few assets in our project’s folder to completely change our tech tree! Also, like we did previously with our
CharacterData instances, it will be easy to load all the nodes from our Resources upon start.
Important note: something tricky with that data representation however, especially for beginner programmers, is that we don’t differentiate between “tree” and “node” anymore. The “tree” is simply the root node 😉
There is, however, a little caveat with all of this.
On the one hand, we have the tree architecture itself that is the same from one game session to another (it is chosen by the game designers and is part of “the game mechanics”). It is loaded from our Scriptable Objects.
But on the other hand, we’ll also need to know which technology nodes were researched and unlocked during this specific game session.
From a dev point of view, it’s actually really easy: we’ll simply have to store the unique codes of the nodes with their
_unlocked flag set to
true in our recently created
GameData class when we save the game, and then quickly loop through all when we reload the data to re-assign the unlocked nodes.
But! This means that if we change the structure of the tree too much, and in particular if we remove nodes that were referenced in it, we’ll have small inconsistencies between the stored data and the current one. Even if we’ll make sure to ignore the missing keys, it’s important to note that this system will not guarantee a perfect match between previously saved game sessions and your latest tech tree structure…
It is not absolutely bad per se, but you should be aware of that and try to avoid changing too much your tree structure once it’s “fixed” because you’ll probably have to re-create new game sessions if you do.
I especially point this out in the context of DLCs and other add-ons. If you create a video game and expect to see it flourish, you’ll most likely need to add new content regularly so that players don’t get tired of the vanilla experience. These additions can vary a lot, but you need to think of them well ahead and in particular spot all the possible “breaking points”, where changes could prevent retro-compatibility… because when they download your new extensions, players should not suddenly lose all their previously saved games! 😉
Preparing the node class
Alright, with all that said, time to get to coding! To begin with, let’s prepare this
TechnologyNodeData class. Since we won’t be storing it directly, we don’t need to inherit from our custom binary serialisable classes, and we can simply use the Unity
ScriptableObject class as parent:
The properties I define here are simply the various characteristics I mentioned before – for the research cost, we do the same as with our units and use a list of
Those are the public fields that will be exposed and customisable in the editor, thanks to Unity’s built-in Inspector for Scriptable Objects. Let’s also add two private fields that will be used internally by our scripts: the
_unlocked flag and the
_parent auto-computed reference (plus their associated public getters).
Those two properties will not be visible and it’s actually great because we don’t want the designers to modify them directly – this could lead to small mistakes, inconsistencies and bugs 😉
So – at that point, we are ready to add a bit of behaviour to these nodes. I want to be able to do three things:
- load all my technology node Scriptable Objects and store them (keyed by their unique code), like I did previously for my
- get the list of currently unlocked nodes
- conversely, set the currently unlocked nodes from a list of unique codes
All of these functions should act on all node instances, they are pretty “global”: let’s make them static, this way they will be very easy to call from anywhere (without needing to create any actual instance of
Ok, there are several things to comment on here.
First, about the
- we list all of our
TechnologyNodeDatainstances from the given Scriptable Objects directory and assign them to their unique codes in a Dictionary – nothing fancy here, but just make sure to put the Scriptable Objects in the right folder, or change the path used here!
- as we read the instances, we prepare a mapping of each child to its parent, by unique code… but we don’t fill our
_parentfield yet because we can’t insure at that point that the
TechologyNodeDatainstance matching the parent’s code has already been loaded: that’s why we do it in another loop afterwards, once all nodes have been loaded and referenced
- when we load the node instances, we also force their
_parentfields back to the default
false; this is a safeguard to avoid “keeping data” between play sessions: because the Unity Editor automatically saves Scriptable Objects (serialisable private fields included) but we can’t access the private fields to reset them by hand, we need to make sure they are reset when the game first starts
- finally, we auto-unlock the root node of the tree (that has a specific “root” code) because this node is purely structural and doesn’t really correspond to any real tech research or bonuses
GetUnlockedNodeCodes() method should be fairly clear: we just iterate over all our nodes and check for their
_unlocked flag, then return the result as an array of unique node codes.
SetUnlockedNodes(), we take the list of unique node codes and, if this node exists in our current mapping, then we try to unlock it (the unlock process is aborted if the node is already unlocked).
This unlock process is done in the
Unlock() method which is, this time, an instance method and not a static one.
At the moment, however, “unlocking” simply means changing the value of the
_unlocked flag… but how can we actually impact our game session with this tech tree? To do this, I’ve decided to implement a basic system of “actioners”.
The “actioners” are the real logic of the tech tree, they are what determine what happens when a tech is researched and unlocked. They are callback functions that are associated with your node unique codes and trigger whenever a specific node is unlocked.
Depending on the techs you want your tree to implement, these callbacks can vary a lot. In the following sections, we’ll see how to do some attack boosters and cost reducers, but you could think of tech nodes that give you access to a new type of unit, that grant some overkill power, that weaken enemy units…
Despite having very different consequences for the player, all those behaviours can roughly be factorised as: “storing somewhere modifiers and/or flags, to later re-use or check for in our code”.
For example, as we’ll see soon, boosting the attacks of all units can be done by keeping a little multiplier somewhere and applying it whenever you create a new unit (and optionally retro-actively to all your already spawned units). Granting the player a new unit type could be about toggling a flag on or off and showing or hiding a matching “character instantiate skill” accordingly.
In other words, my “actioners” will be quite short and basic functions that update some modifier and flag mappings, and optionally apply those to the current game session if you need retro-active effects:
I’ve also defined some simple functions to access my “actioners” and modifiers while checking for keys and optionally returning default values if the key doesn’t exist. This is particularly valuable for the modifiers since, at the beginning, we won’t have any modifiers defined… and so we’ll consider they’re all equal to 1, so that they don’t impact our computations!
This system of “actioners” allows me to easily define the behaviour of my tech nodes, just by adding some callbacks associated to the right code.
Implementing a basic tech tree with five nodes
To better understand how all of this works, let’s create a very simple tech tree with only five nodes:
Here, the player automatically unlocks the root and can then choose to unlock the “Attack Booster” or the “Buy Cost Reducer” (or both). Remember that the root is automatically unlocked and that the second “rank” of techs cannot be reached until the first “rank” has been unlocked.
This tree can be represented with our data structures by creating five Scriptable Objects of the
TechnologyNodeData type and filling their info with some example values:
Note: we won’t be using most of these properties in this first part, but you can prepare them for next time! 🙂
As you can see, I have defined my relationships (my edges) via the
children property: for example, by setting the “Attack Booster” and “Buy Cost Reducer” Scriptable Object assets as children of the “Root” Scriptable Object, I effectively create the edges between these three nodes.
Now, we need to code our “actioners” so that upon unlocking those tech nodes actually impact the game session!
Buy Cost Reducer
First, let’s take care of our “Buy Cost Reducer”, since it’s a bit simpler 🙂
The idea here is just to store a float modifier that will then be applied whenever we display the purchase cost of a unit or actually buy it:
Now that we have this modifier, we can use it in our
UIManager when we hover a skill button, if the skill is meant to instantiate (i.e. buy) a unit:
And of course, in our
Unit class, we also need to take this modifier into account when we
Place() the unit:
This way, by default, we’ll multiply the values by 1 (if the tech is inactive) but as soon as we actually enable the “Buy Cost Reducer”, the UI info and the resources decrease will be properly updated.
For the level 2 of the “Buy Cost Reducer”, it’s basically the same thing, just with a smaller modifier so that the costs are cut down even more when it’s active; also, as you can see, we overwrite our previous modifiers so that the code we wrote to use this modifier stays the same, and we don’t stack the levels in a weird way…
For the “Attack Booster”, we need a slightly longer “actioner”; indeed, we also want this tech to retro-actively update the attack of all the units you already spawned (contrary to the “Cost Reducer” that can only affect future purchases).
In other words, we want to:
- store a modifier for the new units that we’ll create in the future (so that they can “boost their attack”)
- but also loop through our current units and update their attack with this boost
Luckily, remember that we recently implemented a static list that allows us to easily retrieve all the units currently owned by a player! 🙂
So our “actioner” is in fact pretty straight-forward to implement:
And then, we just need to read this modifier when we create our new units and add our
SetAttackDamage() method in the
Unit class to properly recompute the level up data when we re-assign the attack damage value:
Similarly to the “Buy Cost Reducer” tech, the level 2 of the “Attack Booster” works exactly like level 1 but with a bigger modifier:
Testing our tech nodes
The problem is that, for now, we don’t have any UI or equivalent to unlock our techs; so, how can we actually test that our modifiers work? Once again, time to use our magic debug console! 🙂
All we have to do is add a new “unlock_tech” command that takes in the code of the node to unlock and, if it exists, unlocks it (plus the parser for debug commands with a single string parameter since we haven’t implemented it yet):
Note that because we’ve coded up some checks in our
Unlock() function, we should not be able to unlock unreachable nodes in our tree… but I won’t bother actually insuring this because I consider this to be a “quick and dirty” console 😉
Also, for now, we ignore the research costs and time, and we just do a “direct unlock” process. We’ll integrate back the research-related variables in the next part.
Anyway! With that new command in our console, we can run our game and test our new tech nodes; for example, you can check out your “unmodified” costs for buying a Worker unit, then direct-unlock the “Buy Cost Reducer” tech and see that your purchase costs have changed:
In this episode, we’ve introduced the concept of tech tree into our RTS game and we’ve even prepared a very basic example to use to directly impact various variables in our other mechanics.
Next week, we’ll continue improving this tech tree and see how to display it in the UI, and how to store the data so that we can load it back afterwards 🙂