I’m going to assume you’re familiar with behaviour trees already, however I would still consider this post as introductory in nature. I’ll probably post something more advanced soon (most likely something to do with efficiency, which when you’re talking about behaviour trees either means getting them to run fast (making them cache friendly and avoiding necessary computation), speeding up the creation of instances of a behaviour tree, or reducing the per instance memory cost each behaviour tree). But I digress.
So why use behaviour trees at all? Finite state machines are actually pretty decent (useful, well understood, and very importantly, intuitive). They have one critical weakness though. All the transitions between states are explicit. As the state machine grows these transitions become a huge tangle and changing the structure of the state machine becomes all but impossible (hierarchical finite state machines do mitigate some of this tangle reasonably well though). Enter behaviour trees, where the execution of a particular subtree is only dependent on the portion of the tree that leads directly to it. Making large scale changes to tree structure is still non-trivial, but it is at least doable.
Now what about those state transitions we got rid of? Was the gain in flexibility really free? The answer of course is no. First there is the very obvious performance cost that results from having to repeatedly validate the conditions for the current behaviours. Naively, every time the tree is refreshed you must recompute all your conditions to ensure that your current behaviours are still valid. This does have the nice side effect of making behaviours very reactive to changing conditions though. And you never have the problem of being stuck in a state because there wasn’t a valid transition out of it.
But is there another cost besides performance? One I’ve rarely heard mentioned is that behaviour tree logic can be somewhat unintuitive. This is due to the fact that the conditions that must be true for a behaviour to start must stay true for it to continue to run! This is completely different than state machines, where you only need the condition to transition to the state. State machine logic seems to map more naturally to most real world systems (where the conditions that trigger a state are different from the conditions that end it, as opposed to behaviour tree logic which only has one set of conditions that has to stay true for a behaviour to run).
Let’s think of a concrete example. Say an agent needs to be between 4 and 5 metres from their target to start a particular attack and this attack requires specialized AI to be running throughout the execution. Now of course the attack doesn’t stay within this initial boundary. If you’re dealing with a state machine that’s fine, all you needed were the starting conditions. But a behaviour node might need different conditions for starting and remaining active. Typically the conditions for the node need to include some kind of boolean OR wrapper node that holds both the initial conditions and the conditions required for it to remain active. This makes for messy behaviour trees that can be hard to understand and difficult for new users to create.
But one very simple way to deal with this problem is with a decorator called a Latch Decorator. This decorator doesn’t do anything if its child returns false (it just returns false itself), but once its child returns true it “latches on” and stays on. You can then group your conditions and attach them to this decorator and once the conditions are true to start your behaviour they will stay true. So when will it turn off you might ask? Well you can have it latch for a given number of seconds if you wish, but typically you just leave it on until the node resets once the beahviour that follows it is finished. The mechanism for this is implementation specific, but as an example the behaviour following it will eventually return false, this could cause the parent of this subtree to not evaluate it on the next update. Nodes typically detect this for the purposes of either shutting down or reinitializing when control flow returns to them.
Anyway, quite a lot of discussion for such a simple little node. But the devil is in the details isn’t it?