Developing the AI for a simulation game, my friend and I noticed that behavior trees tend very fast to become quite complex and unmanageable. We also noticed that we need to repeat several times the same nodes in the same order with different parameters, which leads to the necessity of several cut and paste operations that are not only boring, but also prone to error.
We started to look how we could reduce the extent of our behavior trees using the tools provided by the engine. The “Run Behavior” node is the nearest to what we were looking for, but it doesn’t allow to pass parameters and needs the same blackboard as the calling tree. This implies that to have the exact same behavior with 2 different objects (for example transporting an object from one place to another, verifying that the 2 places are free from other bots and eventually waiting in a queue), we should write 2 different “Run Behavior” nodes. In a complex world with a lot objects to manage and different actions to accomplish this isn’t an optimal solution.
So, we came out with the idea of developing a new node which is basically a “Run Behavior” with parameters; we call it “Call Behavior”. The behavior tree asset that you call through the “Call Behavior” node has a different blackboard than the calling tree. This allows to re-use these behavior trees in different places, exactly like functions in programming languages.
How to use
Each behavior tree intended for being called from a Call Behavior node (we like to call them behavior functions) has it’s own blackboard. The elements of this blackboard that are parameters must be named with “PARAM_” as prefix.
When calling the behavior function from a behavior tree or another behavior function the call behavior node allows to specify the actual value of the parameters, as an array.
A service that calls the PopBlackboard function is necessary at the top of the behavior function.
An example project can be found here
To try Example 1 change Run Behavior asset parameter in Bot blueprint to Example1BT.
This simple example shows basics of “Call Behavior”. It also illustrates that variables assigned in a called behavior will affect external observers.
The example is composed of a selector, left branch of which will print an int stored in BB only if this int is not zero and right branch sets this int using a behavior function.
The condition on the left branch will abort the lower priority branches.
Right branch calls a behavior function that put a number in its parameter, triggering of the abort of itself and the execution of the left branch of the selector.
The execution will do nothing spectacular, just print “42” to the log (But 42 is the answer to all questions, isn’t it?).
To try Example 2 change Run Behavior asset parameter in Bot blueprint to Example2BT.
In this example the bot brings boxes to player. The example uses a behavior function TransportBF which makes two calls to another function called MoveBF.
The C++ Code
You can download the code from github
We added three classes to our code:
- BTTask_CallBehavior inheriting from BTTask_RunBehavior
In MyAIController class we override the RunBehaviorTree function and add a new function MyUseBlackboard. Actually, all we would need is to override the UseBlackboard function, but this unfortunately isn’t virtual. That’s why the RunBehaviorTree function is exactly the same as the one in the superclass with the only difference of calling MyUseBlackboard instead of UseBlackboard.
The MyUseBlackboard function initializes the BlackboardComponent to MyBlackboardComponent instead of the default one.
More interesting is the MyBlackboardComponent class.
Here we add a few functions
bool PushBlackboard(UBlackboardData* NewAsset, TArray &Params);
This function is very similar to the InitializeBlackboard function.
- We make a copy of the blackboard to push called NewAssetCopy; this avoids overwriting of the blackboard asset by different instances of the same Call Behavior;
- We save the old blackboard asset in the BBStack array;
- We assign the blackboard asset to NewAssetCopy and initialize it. The new blackboard asset uses the old blackboard as parent. ValueOffsets and ValueMemory are extended in order to contain the new blackboard.
- The ValueOffsets of parameters are pointed to the ValueMemory of the actual arguments.
- Finally, the parameters keys receive the observers that notify the argument observers in case of change.
UFUNCTION(BlueprintCallable, Category = BehaviorTree) void PopBlackboard();
This function called by a service when exiting the Call Behavior node restores Blackboard Data and observers.
In the BTTask_CallBehavior class we add a new array
UPROPERTY(Category = Blackboard, EditAnywhere) TArray Params;
that allows us to pass the parameters to the node, then override the InitializeFromAsset and the ExecuteTask functions in order to use the PushBlackboard function of MyBlackboardComponent.
Improvement and Testing
- Unfortunately some functions in the engine aren’t virtual, so the solution isn’t the most elegant ever, however it works.
- We didn’t test it with SimpleParallel because we don’t use it.
- Synchronized keys in behavior functions are not supported.
- The blackboard of a behavior function cannot have a parent.
- You have to be careful specifying the parameters as array, there is no type or number check.
- The blackboard debugger doesn’t work inside a behavior function, that means you can’t see values of keys, but you can see executions and put breakpoints.
- Call behavior can trigger “duplicate keys” warnings while running. You may want to disable it by adding this in your DefaultEngine.ini :[Core.Log] LogBehaviorTree=Error