Download

I gave up on behavior trees

I’ve been using behavior trees in my game for about a year. They’ve worked pretty well for me, but there have always been little quirks with the AI which pops up, and these are frustrating. With 4.12, a few things changed to add to the frustration. I’ll try to detail them here:

Argument 1:
-The AIController MoveTo() command or MoveToActorOrLocation() will not work as you’d expect. If your behavior tree needs to interrupt a movement task, one of these nodes will allow you to interrupt the logic, but the other will not (the simple version). This means that if your character is moving towards a location and a higher priority task arrives, he’ll continue moving until he arrives, then he’ll evaluate the behavior tree.

-If you put in a bunch of conditional decorators into your behavior tree, sometimes you’ll get false positives and vice versa. The behavior tree could have a selector node, one of the left nodes gets pass on all conditions, but a node to the right gets run instead.

-Aborting lower priority nodes, self nodes, or both doesn’t always work.

Conclusion: Behavior trees aren’t 100% reliable. 98% of the time they are. I’m humble enough to consider the possibility that it’s user error on my part, however.

Argument 2:
When you create your blueprint task nodes, you have to recreate a lot of boiler plate blueprint nodes. Usually the sequence is something like this:

  1. Create an “Begin Execute” node, then get the AI controller, check that its valid, cast it to your own AI controller, save a reference, get the controlled pawn, check that its valid, cast it to your controlled character class, save a reference to it, etc.
  2. Check the blackboard values, then change them and apply behavior specific actions

Conclusion: There’s a lot of overhead costs with behavior tree tasks.

So… I’ve finally given up on behavior trees. Instead, I rebuilt my AI in C++. I pretty much recreated my behavior tree logic using a state machine within the AI controller class. Surprisingly, this only took me a full day to do and the code implementation is cleaner than the behavior tree. Instead of using a blackboard, I just created a custom struct which contained all of the key data my existing blackboard used. No more querying the blackboard for keys and key values :slight_smile: This makes serializing this data a lot faster and easier as well. Since I use a single master function to run my AI via a state machine, I can combine all of my tasks into one master function, so if any of my tasks need to know anything about the controlled pawn, or other common meta data, it’s already been collected and sanitized at the top of the master function. This reduces the code footprint significantly :slight_smile: I can read and write sequential code and debug it and get it to work 100% consistently, and slam it with various test cases, so there’s a huge reliability and confidence win here. Since the AI logic is all being done in an AI controller, I can create multiple behavior patterns by creating multiple master functions to run behaviors, and if I use a function pointer, switching to a completely different behavior is as simple as pointing the function pointer to a different behavior pattern. One tricky thing to note about this though: Your AI method will be running in a tick function, so if there’s something you want to be processed only once, you’ll have to design your state machine to handle that (such as creating an extra flag which gets toggled after initialization is done).

Anyways, I thought I’d share this alternative approach to AI incase anyone else wants to consider alternatives.

Just a little info on your point #2.

It seems like you are on 4.12, and on that version they added the “_AI” nodes (Execute_AI, receive_Tick_AI, etc).
Those nodes come with the Owner Controller and the Controlled Pawn. Helps reduce the boiler plate code you used to write by a significant margin.

Thanks for sharing, it’s always informative to see how others do. Especially with system that makes you raise an eyebrow yourself.

behaviour trees are a mess to debug and maintain

FWIW, I find it to be quite simple, after I spent a few months figuring out a lot of the more advanced bits that are not really mentioned in any of the tutorials out there.

One of the most important things I think to know about Behavior Trees, is that there is a node that you can use to chain off to a different Tree. This means that you can have Trees for specific functions that several different AIs can share, even if they have root trees that are quite different. It is a little bit of weirdness, since in Unreal almost everything else is based on OOP inheritance, and with Behavior Trees, if you want to extend a behavior, you need to do it with Compositing rather than Inheritance.

As well, if you have a task that requires any sort of state (such as placing the pawn into a “run” state that will only be valid for that section of the tree) you need to make sure that you have a way to clear that state out when done. This usually means that you need to make a single BTT that encapsulates the entire function, and has a proper reset On Abort. If you chain several tree bits together (ie, “Set Running”, “Chase Player”, “Set Walking”) if that node gets aborted, you’re going to remain in the Running state unless you also clear it in whatever new state you’re in.

Behavior Tree Services can be absolutely amazing in implementing performant AI that makes proper decisions. What I am working on right now, I have a single (implemented in C++ because it does a fair amount of decision calculations) Behavior Tree Service which runs every 0.1 sec, and implements checks for all of the parameters of the AIs possible special moves (ie, is it within distance of a target that can be attacked, is it within distance to know of something it should pickup, or investigate, is it on the same Z plane as the target it is hunting), and sets booleans on the Blackboard for “could the AI do this right now?”.

Then the tree itself just has a list of nodes that check each bool, from left to right, based on which should be a higher priority (ie, attacking a target close by is higher priority than picking up an item of interest), which then connects to a BTT that actually executes the task. Each BTT is pretty much self-contained, and follows all steps of the process for the task in question (ie, set running, move to the target, attack the target) and cleans up it’s state on completion or abort.

I unfortunately cannot show examples of what I am using, as it’s commercial product, but I hope this explanation helps someone.

It’s also pretty easy to debug, works same as the Blueprint debugger, although it can be a bit obnoxious trying to figure out the Blackboard variables sometimes.

1 Like

I did want to also add, that I think the most frustrating part of the BehaviorTree, is that keeping the Blackboard in sync with everything else is a bit on the difficult side – there’s not an obvious pattern for it. What I currently do is implement a Blueprint Interface that exposes a function to call for notifying the Controller of what is happening – let’s say the AI decides to use it’s one Special Move. I create a BPInterface that has a “DoSpecialMove” function (and maybe a “AbortSpecialMove”). The Controller and the Character then both implement that interface. The BehaviorTreeTask calls the Controller’s DoSpecialMove, the Controller calls the Character’s DoSpecialMove.

So, basically, the way I’m doing things right now, the Blackboard is telling the Tree if it can perform any particular task. The Tree decides which task to perform. The Task tells the Controller that it is presently performing this Task, and the Controller tells the Character that it is presently performing this Task. Any special logic that needs to happen in regards to the Controller or the Pawn entering these tasks can be handled there.

And now that I’ve explained what I’m doing, I have a better understanding of what I’m doing as well :smiley: