Implementation Differences of UE4 Behavior Trees vs "Standard" Behavior Trees

This section of the documentation is intended for people who are familiar with Behavior Trees generally and would like to dive into the UE4 implementation as quickly as possible.

For those who haven’t used behavior trees before, you may find some of the explanation here confusing. You may want to read the walkthrough first, and you may also just want to skip to the next section to learn more about behavior trees generally. You can return to this section if you’re interested in what we’ve done differently than others.

There are three critical ways in which the UE4 implementation of Behavior Trees differs from “standard” behavior trees:

UE4 Behavior Trees are event-driven

“Event-driven” behavior trees avoid doing lots of work every frame. Instead of constantly checking whether any relevant change has occurred, the behavior trees just passively listen for “events” which can trigger changes in the tree.

Having an event-driven architecture grants improvements to both performance and debugging. However, to take the most advantage of these improvements, you will need to understand the other differences to our trees (below) and structure your behavior trees appropriately.

Performance Improvements: Since the code doesn’t have to iterate through the entire tree every tick, performance is much better! Conceptually, instead of constantly asking “Are we there yet?”, we can just rest until we’re prodded and told “We’re there!”

*Better debugging: * When stepping forward and backward through the behavior tree’s execution history to visually debug the behavior, it’s ideal to make the history show relevant changes and NOT show irrelevant ones. In the event-driven implementation, it’s not necessary to filter out irrelevant steps that iterated over the tree and chose the same behavior as before, because that additional iteration never had to happen in the first place! Instead, only changes to the location of execution in the tree or to blackboard values matter, and it’s easy to just show those differences. (I will link to the document on debugging here as soon as it’s ready!)

Conditionals are not leaf nodes

In the standard model for behavior trees, conditionals are “Task” leaf nodes, which simply don’t do anything other than succeed or fail. Although nothing prevents you from making traditional conditional tasks, it’s highly recommended that you use our Decorator system for conditionals instead.

Making conditionals be decorators rather than tasks has a couple of significant advantages.

First, conditional decorators make the behavior tree UI more intuitive and easier to read. Since conditionals are at the root of the sub-tree they’re controlling, you can immediately see what part of the tree is “closed off” if the conditionals aren’t met. Also, since all leaves are action tasks, it’s easier to see what actual actions are being ordered by the tree. In a traditional model, conditionals would be among the leaves, so you’d have to spend more time figuring out which leaves are conditionals and which leaves are actions.
(I intend to add a screenshot to illustrate this… congratulations, you’re reading the docs as they’re still in development, so no frills yet. ;p)

Another advantage of conditional decorators is that it’s easy to make those decorators act as observers (waiting for events) at critical nodes in the tree. This feature is critical to gaining full advantage from the event-driven nature of the trees. (A document about the special properties of our Conditional Decorators will be linked from here soon.)

Special handling for concurrent behaviors

“Standard” behavior trees often use a “Parallel” composite node to handle concurrent behaviors. The Parallel node begins execution on all of its children simultaneously. Special rules determine how to act if one or more of those child trees finish (depending on the desired behavior).

NOTE: “Parallel” nodes are not necessarily multi-threading (executing tasks at “truly” the same time). They’re just a way to conceptually perform several tasks at once. Often they still run on the same thread and begin in some sequence. That sequence should be irrelevant since they will all happen in the same frame, but it’s still sometimes important.

Instead of complex “Parallel” nodes, UE4 Behavior Trees use “Simple Parallel” nodes and our own special node type which we call “Services” to accomplish the same sorts of behaviors. See the next post in this thread for exactly what we use.

Why not use “Parallel” nodes?

  1. Parallel nodes can be very confusing, even for relatively simple behaviors.
  • *]Effectively Parallel nodes are simultaneously running a bunch of separate sub-trees, but any or all of those sub-trees may need to abort if one of them fails. Or they may “succeed” when the others finish (whether successful or failing). Or… etc.! Parallel behaviors can be confusing even in simple cases, and with the number of options potentially available, it can become highly confusing.
  1. Parallel nodes make it harder to optimize performance, especially in terms of making event-driven trees.

What does UE4 use instead of “Parallel” nodes?

There are three types of nodes which provide the functionality that would normally come from Parallel nodes:

“Simple Parallel” nodes
Simple Parallel nodes allow only two children: one which must be a single task node (with optional decorators), and the other of which can be a complete subtree.

You can think of the Simple Parallel node as “While doing A, do B as well.” For example, “While attacking the enemy, move toward the enemy.” Basically, A is a primary task, and B is a secondary or filler task while waiting for A to complete.

While there are some options as to how to handle the secondary “meanwhile” task (Task B), the node is relatively simple in concept compared to traditional Parallel nodes. Nonetheless, it supports much of the most common usage of Parallel nodes.

Simple Parallel nodes allow easy usage of some of our event-driven optimizations. Full Parallel nodes would be much more complex to optimize.

(I’ll add a screenshot of this ASAP. Sorry for the lack of fun images.)

“Services”
Services are special nodes associated with any composite node (Selector, Sequence, or Simple Parallel), which can register for callbacks every X seconds and perform updates of various sorts that need to occur periodically.

For example, a service can be used to determine which enemy is the best choice for the AI pawn to pursue while the pawn continues to act normally in its behavior tree toward its current enemy.

Services are active only as long as execution remains in the subtree rooted at the composite node with which the service is associated.

(I’ll be adding a screenshot here ASAP. Also, there will be more detailed information in a separate document about Services.)

Decorator “Observer Aborts” property

One common usage case for standard Parallel nodes is to constantly check conditions so that a task can abort if the conditions it requires becomes false. For example, if you have a cat that performs a sequence “Shake Rear End”, “Pounce”, you may want to give up immediately if the mouse escapes into its mouse hole. With “Parallel” nodes, you’d have a child that checks if the mouse can be pounced on, and then another child that’s the sequence to perform. Since our Behavior Trees are event-driven, we instead handle this by having our conditional decorators “observe” their values and abort when necessary. (In this example, you’d just have the “Mouse Can Be Pounced On?” decorator on the sequence itself, with “Observer Aborts” set to “Self”. )

(For more information, I’ll link to the “Observer Aborts” explanation documentation ASAP.)

Advantages of UE4’s Approach to Concurrent Behaviors

Clarity
Using Services and Simple Parallel nodes creates simple trees that are easier to understand.

Ease of Debugging
Clearer graphs are easier to debug. In addition, having fewer “simultaneous” execution paths is a huge boon to watching what’s actually happening in the graph.

Easier Optimizations
Event-driven graphs are easier to optimize if they don’t have a lot of subtrees “simultaneously” executing.

FAQ
Can you really do everything you can do with Parallel nodes?
We believe you can do everything necessary with the nodes we’re providing, with a better interface! Certainly the nodes above handle the most common cases. If we find any edge cases that can’t be handled or are less than ideal, we’ll consider additional fixes to handle those cases.

Are these the only differences between UE4 Behavior Trees and a “standard” behavior tree?
“Standard” is in quotes for a reason. :wink: There’s really no such thing as “standard”, so there could be any number of differences between UE4’s implementation and whatever implementation you’re most familiar with. If you’re familiar with an unusual implementation, it may have other critical differences, and there are likely more subtle differences regardless. Hopefully these notes give you an idea of the most important differences relevant to how you will need to build your trees. For more information about our special types of nodes, read the sections about those nodes. <LINK>

For more information, please see the hub for Behavior Tree Documentation.

Hello,

thank you for these really helpful explanations. I have a question about Parallel Nodes though :slight_smile:

Imagine a football game, and a bot who “has” the ball in it’s half of the field, and need to go on the other side to shoot eventually.

The easiest is to mimic what’s already in the shooter game : have a sequence which

  1. Find a position on the other side, close to the goal
  2. Move the bot on this position
  3. Make the ball shoot the ball

But the problem is that the bot is not the only character on the field. And it might want to:

  • Dribble any opponent who might come in front of him
  • Pass the ball to a team mate
  • Shoot before it reaches the position it first planned to go to, because for example the goal keeper can be lobbed
  • etc…

How would you handle such a scenario?

I can think of 2 ways :

Use a parallel node

I would have a sequence node which would:

  1. Find a position on the other side, close to the goal.
  2. Move the bot on this position using a Task in a Parallel node. The background node would be a selector with an infinite loop decorator which would first try to shoot if possible, then try to dribble if needed, or in the end pass the ball if a team mate is in a better position. When any of these tasks succeeds, this would change a flag in the Blackboard ( something like ItHasBall ), which would then instantly abort the parallel node.

Use intermediate pathfindings

I would have a sequence node, with an infinite loop decorator, which would:

  1. Find a position on the other side, close to the goal.
  2. Set the target position 1 meter away, in the direction of the previous found position.
  3. Move the bot 1 meter toward this position.
  4. Use a sequence node which would try to do one of the actions I spoke of above

I could also use decorators, but ATM I don’t think that would the best solution. This would require to implement as many decorators as actions I would like my bots to do + adding some data in the blackbord, which I think can be avoided with a better BT structure.

What do you think of all that?

Thanks :slight_smile: