Today, we’ll continue working on the behaviour trees for the units of our RTS!
This article is also available on Medium.
In the last episode, we introduced the concept of behaviour tree (BT) and we saw how this tool can help us model complex AI systems like the one for our RTS units. We also compared it to another task switching structure, the finite state machine.
Now, it’s time to actually get to coding and see how BTs can be implemented in Unity. And to begin with, let’s start by creating a little toolbox of basic BT objects to then choose from easily.
Preparing the behaviour tree utilities
So, for now, we’re not going to implement the actual behaviour trees of our RTS units yet… but we’ll make sure that we have everything ready for the next section!
We are going to code up the base objects of a BT, the
Node and the
Tree class; the usual composites: the
Selector and the
Parallel; and the usual decorators: the
Inverter and the
We’re also going to take advantage of one of C# utils: the namespaces. By declaring all of our classes in a custom
BehaviorTree namespace, we’ll make it easier to import afterwards in our RTS scripts (and even easier to export to other projects if we need this basic lib elsewhere!).
Alright – the first thing we need to define is the base element of our tree: the node. A node is a unit in this big logic system, it is the building block for the rest.
We’ve said it before: nodes have children (zero or more). Conversely, they usually also have a parent: if the node A has two child nodes B and C, then B and C have the node A as parent. That’s pretty straight-forward but it will be important for us to be able to navigate through our graph in both directions (upwards, going through the parents; and downwards, going through the children). Of course, the root node of the tree has no parent (its parent is null).
Nodes also have a state that can be “running”, “success” or “failure”. We’ve already seen in previous tutorials how C# enums can help us with this kind of pre-determined list of options; so we’re going to define an enum for the nodes state with those three possible values, and each node will have a little variable to store its current state.
Then, we’ll have to prepare our data context. We will use C# dictionaries for this – we’ll have strings as keys and objects as values. Remember how the
object variable type is a lazy C# type that can hold any variable and can then be casted to the original type to retrieve the initial data? This will allow us to easily store any kind of data in our context and reference it by a unique name… just like a basic variable in a C# program! Don’t forget, though: this context is local to the subtree that spawns from this node, meaning that:
- ancestors of this node (i.e. nodes that are higher in the graph, closer to the root than this node) will not have access to it
- descendants of this node (i.e. children but also grand-children, grand-grand-children and so on) will have access to it
Making sure that the data context is local is important to keep our nice BT properties, and in particular the atomicity / reusability of nodes as subtrees. If all branches could potentially depend on any other branch in the tree, we’d lose this nice decoupling we strive for.
Important note: this does not exclude the root from having data that is shared throughout the graph (and we’ll actually see very soon that we will have data shared in the graph in our behaviour trees)! But it does not impose it and therefore saves the decoupling property of our tree 😉
Finally, nodes will have a set of various functions to help us build and run the tree: the required
Execute() method that each node will override to specify its own behaviour, naturally; but also some util functions to add, remove or set children; some accessors to relevant variables; and the methods to set, get or clear a data by key in the node’s data context. Those last functions will potentially need to climb up the tree (recursively), parent after parent, until they’ve reached an ancestor that has the given key, or until they’ve reached the root.
Okay. That’s a lot of info.
Hopefully it’s still pretty clear and the following code, that basically translates this bunch of ideas in C#, will be understandable enough 😉
You can see that this class is defined in the
BehaviorTree namespace, so it’s “isolated” from the rest of our RTS scripts but it can be very easily imported with the
using BehaviorTree; line 🙂
Actually, the tree will be much quicker to code! The only variable a tree needs is the reference to its root node; from there, the root itself will be able to access all of its children, and grand-children… so it will allow for a traversal of the entire graph.
But we can’t actually define a root at that point, since we’re still at an abstract level that doesn’t have any real node! We will only define actual nodes and relationships in our derived classes, the ones that implement real entity behaviours and that we will instantiate on our unit. At that point, the only thing the
Tree class knows is that it can have a root. So how can we get a reference to this soon-to-be-but-not-yet-created root?
To solve this problem, we’ll rely on an abstract method:
SetupTree(). Remember the idea of an abstract method is to define a prototype with no implementation – it specifically stipulates that the thing you declared hasn’t been completely defined and that it still misses some logic. This basically forces the child classes to define it afterwards and inject their custom logic. It’s thus a way of making sure a method exists on all derived classes while not providing any logic to share between them (contrary to virtual methods).
And because it contains an abstract method, our
Tree class will necessarily be abstract too: this means that we won’t be able to instantiate this class itself but only the classes we derive from it.
Thanks to that dynamic getter, we’ll fill in the root node when we first create our
Tree-derived class instance and we’ll then be able to call its
Execute() method in the
Update() entry point to make the tree “tick”.
All in all, this gives us the following script:
The composite nodes
Now that we have the core objects for our BTs, let’s prepare a few util node types to create structures and logic in our trees!
All of those nodes will of course inherit from our
Node class. Like their base class, they will define a default empty constructor and another one that accepts a list of precomputed children; and they will override the
Execute() method to implement their specific behaviour.
We’ll start with composites – you’ll recall that those are the nodes with one or more children that run all of those children in a given order, and the state of which depends on the states of those children.
Selector is like a logical OR: it iterates through its children and stops as soon as one has succeeded; if they all failed, then the
Selector node itself returns a failure too:
Parallel (with a “one” success policy)
Parallel processes all of its children at the same time and only “computes” its success/failure state at the end of all child executions. Here, I’m going to choose a “one” success policy; in other words, I’ll say that my
Parallel node succeeds if at least one of its child nodes has succeeded:
Sequence is like a logical AND: it iterates through its children and only succeeds if all children succeeded:
Note: I’ve added a little
boolean variable to easily make a Sequence node go through its children at random; we won’t use it for now but it can be useful 🙂
The decorator nodes
Similarly, we can also define the common decorators we talked about earlier: the inverter and the timer. Here, we want to transform the result of a single child node, or repeat / delay its execution.
Inverter is like a logical NOT: it transforms a failed child into a success and vice-versa:
Timer re-processes its child node again and again every X seconds. But we’ll need to add a bonus feature to this node: the ability to run a specific action when the child node is re-processed. Indeed, because the
Timer node will basically run indefinitely on its own, it would be nice to be able to catch each “timer reset” event and react on it (for example: to refresh the UI after we’ve produced some resources…).
To do that, we’ll add in a little delegate variable that will allow us to easily pass in any zero-parameters void function we want as the callback for this “timer reset” event.
Designing our behaviour tree
Now that we know how BTs work and that we have a basic BT toolbox, let’s see how we can use them to model our building and character units behaviour as a BT!
We saw before what our units have to do – it’s time to translate these items into an actual tree graph. We are going to have one graph for buildings, and one graph for characters.
We’ll start with the building units. For those, we want to continuously produce resources on the one hand, and optionally attack enemies in range if there are any. Also, we produce resources every 3 seconds and we attack according to the building’s attack rate. So this gives us the following diagram:
Just to get used to BTs, let’s go through this easy-enough graph and see how it works and implements the desired behaviour.
The parallel composite at the top will run the three branches at the same time – we’ll call this node over and over gain during our
Update() function to have the tree “tick”.
First, let’s take a look at the right branch since we’re familiar with the production resource system. This subtree works based on a
- the leftmost “UnitIsMine” node is a simple check for whether the entity running this behaviour tree is owned by us or not: if the owner of the unit matches our player index, then the check will pass and we’ll return a “success”; else, we’ll return a “failure” which will interrupt the
Sequenceand prevent it from running the right sub-branch
- but if we do own the unit, we’ll execute the timer decorator that will regularly call its child node; and this child node will produce resources according to the system we put in place previously
Now, the left branch. Similarly, this subtree starts with a sequence so it will only continue executing if all children succeed. Those children are first a check for whether there is an enemy in the attack range of our unit, and second the timed-attack branch. In other words, if the check passes, the sequence will continue and move on to actually running the (timer + attack) subtree; and this subtree has the same structure as the one for resource production.
The middle branch only contains one node: a check for whether there is an enemy in the FOV range. If there is one, then this enemy is marked as the
currentTarget (and this info is stored in the data context of the parent node, in this case the root node, to make it accessible to all sibling subtrees).
currentTarget data slot is then read and used by the “attack sequence” in the left branch.
Note that, for buildings that cannot attack (because they have an
attackDamage of zero), only the right branch is really necessary. We could add a check to ignore them, but we will simply not create the left-most branches when we first initialise the BT if the unit can’t attack:
Similarly, we could remove the right-most branch if we do not own the unit, but this would make for some pretty empty trees at time, and I’d rather show you a simple case with a few basic nodes! 😉
Character units are going to have a more complex behaviour tree because they need to handle the additional “follow” and “move to destination” or “move to target” actions.
But this tree can still be cut down into smaller chunk to study it bit by bit, and it simply reuses the flow-control nodes we prepared earlier and some self-explanatory checks or actions:
You see that this tree uses the same
currentTarget data slot as the ones for buildings but also another,
destinationPoint, to store the exact
Vector3 position that corresponds to the point the player right-clicked on.
Also, notice how this “move to destination” branch is on the left: this means that it will have the highest priority and that this action will supersede all others; if there is no registered destination point, the AI will fallback to checking for a close target: if it has one, it will try and attack or else follow the target to get closer. The character unit will also continuously check for enemy units in its FOV range to update the
currentTarget data slot.
Since there is no condition-free branch with an action leaf, if the unit has no destination nor current target, it will simply stay still on the ground, in “idle mode”.
Note: we won’t add a resource production logic to our character units here but you see that it could very easily be implemented just by injecting a
Parallel node as the new root and putting our resource production subtree underneath 😉
Rewriting our current unit behaviour as behaviour trees
Before we implement the entire behaviour trees of our buildings and characters, we’ll start by preparing our classes and “rewrite” our current behaviour as behaviour trees. Meaning that, by the end of that article, we should have everything working exactly the same…
… but with behaviour trees under the hood 😉
Once again, remember that it’s one of the benefits of behaviour trees: you can start with a very basic model and refine it iteratively by adding branches here and there to extend the logic!
Rewriting our buildings behaviour
Alright, let’s start with our buildings. Since, for now, we don’t want to implement any new feature, we’ll only need to work on producing resources at a regular rate. So we will basically work on the right subtree of our buildings BT and create a simpler behaviour tree that looks like this:
The timer node is a flow-control node that we already have in our toolbox; so the only nodes we need to prepare for that behaviour tree are the “UnitIsMine” check and the “ProduceResources” task.
We’ll start by implementing the “UnitIsMine” check – it is a basic if-like node that will return “success” if the unit is owned by us or “failure” otherwise. I will code up this node in a
CheckUnitIsMine.cs C# script: I am explicitly prefixing my classes with the “Task” or “Check” word so that I immediately know what kind of node I am working on.
To get the index of the player that owns the unit, we’ll need to access the
Unit instance matching this entity. We’ll access it through the
UnitManager component that’s on it – we’ll just pass the reference to this manager in the node’s constructor:
At the very top, I am importing our brand new
BehaviorTree package so that I can derive my class from the
Node class. Also: because it’s a check node, it can only return “success” or “failure” and it is never in “running” mode.
Now, let’s create another C# script called
TaskProduceResources.cs. This script is again really basic: it simply gets a reference to the
Unit instance, like the previous one, and calls its
ProduceResources() method, then it returns a success.
Also, don’t forget: this node that doesn’t care about the 3 seconds-production timer: this will be handled by its parent! So, in our action node, we just tell the unit to do one “round” of production and we’re good:
Now, I can use this new node type as well as the timer utility to create a custom behaviour tree (BT) for my building units, in a
BuildingBT C# class that inherits from the base
The idea is just to override my
SetupTree() method, fill it with the logic that builds our tree and have it return the root node reference:
Then, we need to update the
- make the
producingRatepublic so we can access it in our Singleton instance from here
- and more importantly remove the resource production logic from it so it doesn’t collide with our new local-controlled production
It’s essential to avoid having multiple objects in your project compete to do the same thing: you run the risk of having them overwrite or counteract each other’s result, get unexpected reactions and just overall get a really inefficient system!
We’ve removed all the variables that were linked to this production system. This means we also have to remove the logic that added our units to the
ownedProducingUnits upon placement (because this variable doesn’t exist anymore):
And that’s it! Our building units are now fully in charge of their resource production and they will do so at a regular rate determined by the
producingRate variable in our
Rewriting our characters behaviour
For our character units, we’ll have a bit more work to do. To reproduce the behaviour we currently have, we need to isolate the left-most subtrees of our complete BT and re-implement our “destination point definition logic”:
This tree is a bit more complex than the previous one. We already have the
Sequence nodes, and we’ve also implemented the
CheckUnitIsMine node in the previous section; but we still have to prepare the remaining nodes:
TaskTrySetDestinationwill re-inject the logic of ground raycasting and right-clicking currently in the
GameManagerin our behaviour tree; it will also be a nice opportunity to see how to access and update a node’s data context
CheckHasDestinationwill be a simple if-like node that checks if the
destinationPointslot is currently filled
- if this check successes, then we’ll execute the
TaskMoveToDestinationnode that reads the
destinationPointvalue and performs various actions depending on it
The TaskTrySetDestination node
To begin with, let’s work on transferring our destination point definition logic from our
GameManager to our character unit behaviour tree. Remember that, for now, we have this piece of code in the
We want to rewrite this as part of the logic of a node of our behaviour tree, so in particular we want to make it work for a single entity. Instead of iterating through the list of all selected units, we need to check if the unit we are currently computing the behaviour for is selected or not.
To do this, we need to add a little flag that stores whether a unit is currently selected in our
UnitManager class (and that will be automatically inherited by our derived
CharacterManager). We didn’t need it before because this script was doing everything on its own, but now that we have several classes that contribute to defining the actions of a character unit, we have to create means of communication between them.
So let’s add a little boolean variable in our
UnitManager and its matching accessor, and update the flag during our select/deselect process:
Ok – now, we can easily check for the unit’s selection state and re-inject the raycasting and right-clicking logic.
This gives us the following code for our brand new
TaskTrySetDestination BT node:
The thing I want to focus on here is the call to the
SetData() function. We said it before: we want to use the data context of our current tree branch to easily exchange info between its nodes. Here, our node has to “output” data to this context (in the
destinationPoint slot): it needs to store the
Vector3 position that the player right-clicked on in the dictionary so that other nodes in the branch can access it afterwards.
SetData() function simply sets a given key-value pair in the
_dataContext dictionary variable of a node. But be careful! We don’t set the data in our node itself but in its grand-parent so that the sibling subtrees can access this piece of data too.
And like before with our building resource production logic: don’t forget to remove the redundant unit movement logic from the
The CheckHasDestination node
This node will be way simpler: here, we just need to perform a basic check to see if our
destinationPoint data slot is filled or not.
We’ll retrieve back the value we stored in the
destinationPoint data slot by using the
GetData() method. Since we coded up this function to recursively move up the tree until it’s either found the key or found the root, we don’t need to worry which node we call it on: it will automatically restore the value if it is present in this branch of the behaviour tree!
The TaskMoveToDestination node
Finally, let’s use all of this info we stored to actually perform an action and make our character unit move towards the destination point we set.
We’ve seen before that this node needs to read our
destinationPoint data slot and then use this value in different ways. More precisely, we’ll want to do one of three things:
- if the current agent’s destination point is different from
destinationPoint, we’ll define the
destinationPointas the new target destination for the unit’s NavMeshAgent
- if we see that the agent’s reached it destination, we’ll clear the
destinationPointdata slot to cancel further movement
- else, we won’t do anything (the agent is already on the right course)
We already know how to load up data from a node’s context, so this routine is overall quite straight-forward to translate to C# code:
ClearData() method searches for the given string key recursively and moves up the tree until it’s either found the key and cleared it from the dictionary or reached the root.
Using the nodes in a behaviour tree
Let’s wrap this up by implementing the
CharacterBT class that will define the structure of the character unit’s behaviour tree. Just like the
BuildingBT, we’ll simply override the
SetupTree() method to build our tree and return the root:
Checking that everything still works 🙂
That’s it! We’re ready to test our modifications. Don’t forget: at that point, we’ve only reproduced our previous behaviour so, if all goes according to plan, we should have the exact same game as before 😉
First: remember to add the
CharacterBT script to your character unit prefab(s) and the
BuildingBT script to your building character prefab(s): for now, I have two buildings (the House and the Tower) and one character (the Soldier).
Now, start your game: you should be able to produce a unit, select it and have it move around to various points on the map just like before! Also, buildings that you own will regularly produce new resources.
Alright, we now have a better understanding of how to actually use behaviour trees (BTs) in practice! In this tutorial, we prepared a little toolbox of BT utilities and we used it to rewrite the current behaviour of our building and character units as behaviour trees.
Next time, we’ll wrap up this part of the series on BTs by adding our additional units “skills”: the ability to spot, optionally follow and ultimately attack enemy units that are in close-enough range!