Making a RTS game #24: Implementing behaviour trees for our units 2/3 (Unity/C#)

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 Sequence, the Selector and the Parallel; and the usual decorators: the Inverter and the Timer.

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!).

The Node class

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 3 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 sorts 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 🙂

The Tree class

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(). 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 – remember 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

The 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)

The 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

Finally, the 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

The Inverter is like a logical NOT: it transforms a failed child into a success and vice-versa:

Timer

The 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.

Buildings

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 Sequence:

  • 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 Sequence and 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).

This 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:

Characters

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 corresponding 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 “rewriting” 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 iteratively refine it 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 Tree class:

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 GameManager to:

  • make the producingRate public 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 GameManager.

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 Parallel and Sequence nodes, and we’ve also implemented the CheckUnitIsMine node in the previous section; but we still have to prepare the remaining nodes:

  • the TaskTrySetDestination will re-inject the logic of ground raycasting and right-clicking currently in the GameManager in our behaviour tree; it will also be a nice opportunity to see how to access and update a node’s data context
  • the CheckHasDestination will be a simple if-like node that checks if the destinationPoint slot is currently filled
  • if this check successes, then we’ll execute the TaskMoveToDestination node that reads the destinationPoint value 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 GameManager class:

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.

This 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 GameManager! 😉

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 3 things:

  1. if the current agent’s destination point is different from destinationPoint, we’ll define the destinationPoint as the new target destination for the unit’s NavMeshAgent
  2. if we see that the agent’s reached it destination, we’ll clear the destinationPoint data slot to cancel further movement
  3. 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:

Similarly to GetData(), our 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 2 buildings (the House and the Tower) and 1 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.

Conclusion

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!

Leave a Reply

Your email address will not be published. Required fields are marked *