Subclassing behavior tree

What’s the correct workflow for when you would want to do the equivalent of subclassing a behavior tree? Let’s say you have multiple characters and you want them all to exhibit the same high-level behavior, i.e. hiding, attacking, escaping, but the actual internals of how you want the character to perform these actions would depend on the pawn class and their capabilities?

I see that there is a “Run Behavior” node, but that doesn’t allow me to specify a behavior with any sort of variable. Is there anything better than duplicating the parent behavior tree and replacing the subtrees manually for each pawn class? Do I have to do some sort of dynamic swapping of behavior trees like have a task node that switches the behavior tree at that point to some other tree that’s stored in a class member variable and then back to the common root behavior tree when that sub-behavior tree completes?

Just wondering what the best practice is here because it’s unclear.

If it’s a case of the ‘what’ and ‘when’ of the behaviour being the same and only the ‘how’ which differs, you should be able to implement this with a single behaviour tree. The idea generally is that the BT only defines high level behaviour anyway. Your BT decorators and tasks would just invoke functions on your AI character class (or indirectly through AI controller class). These functions could be virtual and each different character class could override the implementation details.

If you have significant differences in what behaviours the different characters should exhibit, it is not so easy. Obviously they’ll need sections of behaviour tree nodes specific to them. I would guess the Run Behaviour task was designed to allow reuse of functionality so you don’t have to duplicate large parts of the tree, but I haven’t made use of it yet so can’t help you there.

The issue is that the Run Behavior task does not accept a variable, and so in order to have the same tree that runs different tasks in that node, I would need to duplicate the whole behavior tree.

I use blackboard only for the state variables, locations etc which can appear in the engine-provided decorators/tasks. The parenting of the blackboards goes hand in hand with the BT subtrees.

All other variables go inside the controller (or the pawn), whose inheritance works in the usual way (no copies needed.) If you don’t want to use a global list of states, you could keep also the state variables inside the controller and write custom decorators/tasks.

Because the inheritances of the blackboards and other components run in opposite directions, their depths have to be the same and each “layer” of blackboards has to contain everything needed for a given “layer” of BT’s/controllers.

I was able to perform this task by creating:

  • Two AI classes. BaseDroneBrain and TriangleDroneBrain.

  • Two Behavioral Trees. BaseDroneBehavior and TriangleDroneBehavior

BaseDroneBrain

Holds all of the common functions and events, but does not set the behavior tree.

TriangleDroneBrain

Child Blueprint of BaseDroneBrain

BaseDroneBehavior

The trick is to not assign the BTS_BaseDroneService to this Behavior Tree. Simply carry on with your selector or sequence as if it was being performed somewhere else, because it is.

TriangleDroneBehavior

Here we have to do two things in addition to whatever unique functionality you want for this Behavior Tree.

  • First, Use the BTS_BaseDroneService
  • Second, Use the Run Behavior node to call the BaseDroneBehavior Behavior Tree

In this implementation strategy I am using Run Behavior effectively as a call to the parent.

Arbitrarily, I split my BTT_FollowTarget and BTT_CheckRoute into the two different trees for testing purposes. Ideally you could have a blueprint value or some other boolean technique in place to tell the BaseDroneBehavior Behavioral Tree to silence various subroutines in order to avoid conflicting behaviors. This example is a bit simplistic, but my approach appears to be working well so far.

This is brilliant. Thank you very much for the detailed solution.

Faith in humanity restored.

EDIT:
It feels like you can effectively “override” a parent’s state behavior by putting it on the left of the “run parent behavior tree”. And you might may even be able to run the “parent” tree by calling it in the overriden child class behavior if you want to keep its effects.

EDIT2: It does not help me though, I need to make a modular behavior tree system and for this I need to store behavior trees in variables and run them dynamically. I could do that in a giant tree that switches child behavior tree depending on the variable I set on the enemy.

Here is what I did (using C++ but keeping the behavior tree built in UE editor):

The point is not to subclass a behavior tree, but the tasks/services/decorators inside it. Subclassing a behavior tree does not make much sense as a behavior tree is basically and algorithm, and we don’t subclass an algorithm, but some of its components. And if you consider a whole tree as a node, they subclassing it means replacing it (replacing the algorithm it implements), in such a case you only need to use a different tree and not subclass it.

I am using a specific AI controller class per character. Let’s say we have a MonsterAIController base class and a TRexAIController subclass of MonsterAIController. The main behavior tree will be implemented for MonsterAIController. I am creating a base class for all tasks named TaskBase, subclass of BTTaskNode with a DoExecuteTask function (you’ll see why later). Each AI class is associated to a Task Executor class, we have MonsterTaskExecutor and TRexTaskExecutor. Now say we have a task JumpTask we want to implement in a generic way for monsters but want to have some specificities for TRex. There will be a MonsterJumpTask and TRexJumpTask, both subclasses of TaskBase, itself a subclass of BTTaskNode. In the Unreal Editor behavior tree, I will insert a MonsterJumpTask. However, when the behavior tree will execute this task, here is what will happen:

  • MonsterJumpTask::ExecuteTask will get its AI owner, and from there the TaskExecutor instance. If we are on a monster, this will return MonsterTaskExecutor, if we are on a TRex this will return TRexTaskExecutor.

  • MonsterJumpTask::ExecuteTask will now call the TaskExecutor’s ExecuteJumpTask. In the case of MonsterTaskExecutor, this will call DoExecuteTask (remember, our new Execute function in TaskBase) of MonsterJumpTask. In the case of TRexTaskExecutor, the executor will first create an instance of TRexJumpTask if none exist, then call DoExecuteTask on it. The executor will keep a reference to that instance so doesn’t have to create it again every time. The DoExecuteTask is the code that actually peforms the task, ExecuteTask (called by UE) is only an indirection to call DoExecuteTask for the proper task class.

And there we go, by running the behavior tree at the Monster level we got a task executed at the TRex level for the case the monster is actually a TRex (determined by its AI class and task executor class). The same principle can be applied to other functions than executing a task, such as aborting a task. A similar concept can also be applied to services and decorators.

There are a few drawbacks with this approach:

  • the TaskExecutor needs to know about all the possible tasks it can execute, which is a bit heavy if lots of tasks are being used. If C++ had Reflection implemented that would be made simpler, but this is not yet the case, maybe in C++20.

  • the task subclasses are acutally not part of the behavior tree, they are delegates to tasks of the tree (in our example tasks at the Monster level). So their lifecycle has to me handled manually.