May unstable before version 1.0.0!
A lightweight behavior tree library that separates data and behavior.
Requires at least C++20.
Copy away bt.h
and bt.cc
in the Source
folder.
-
Nodes store no entity-relate data states, behaviors and data are separated.
Suitable for: multiple entities sharing a same set of behaviors.
-
Builds a behavior tree in tree structure codes, concise and expressive, and supports to extend the builder.
-
Built-in multiple decorators, and supports custom decorator definition.
-
Supports composite nodes with priority child nodes, stateful compositors, and random selector.
-
Also supports continuous memory fixed sized tree blob.
Code overview to structure a behavior tree in C++:
- Horizontally from left to right represents the relationship from parent node to child node.
- Vertically, the sibling relationship, from top to bottom, prioritizes from high to low (by default).
- In a depth-first way, prioritize recursively ticking all descendant nodes.
- Behaviors and entity-related data are separated, the tree stores behaviors and structure of them, the blob stores entity-related stateful data.
In the behaviors module (system):
// Build the tree.
bt::Tree root;
root
.Sequence()
._().If<A>()
._()._().Action<B>()
._().Selector()
._()._().Action<C>()
._()._().Parallel()
._()._()._().Action<D>()
._()._()._().Action<E>()
.End()
;
In the entities module:
struct Entity {
// A TreeBlob holds all the entity-related stateful data.
bt::DynamicTreeBlob blob;
// or use a fixed size tree blob, will be embeded into the entity struct.
// at most 8 nodes x 64 bytes/per node, 2d fixed size array
bt::FixedTreeBlob<8, 64> blob;
};
In the tick loop:
bt::Context ctx;
// In the ticking loop.
while(...) {
// for each blob
for (auto& e : entities) {
// Bind the data blob for some entity.
root.BindTreeBlob(e.blob);
++ctx.seq;
root.Tick(ctx)
// Unbind the data blob
root.UnbindTreeBlob();
}
}
Reference:
- Build
- Status
- Node Classification
- TreeBlob
- Leaf Nodes:
- Composite Nodes:
- Decorators
- Sub Tree
- Tick Context
- Others:
-
Build a tree: [↑]:
- The function
_()
increases the indent level by1
. - The function
End()
should be called after the tree's build done.
For example, the following tree:
root
contains a single child, which is aSequence
node.- The
Sequence
node contains2
children:- The first one is a decorator
ConditionalRunNode
, and it contains a single action nodeB
. Once the conditionA
is satisfied, theB
is fired. - The second child is an action node
C
.
- The first one is a decorator
- And finally, don't forget the
End()
.
root .Sequence() ._().If<A>() ._()._().Action<B>() ._().Action<C>() .End();
It's important to note that the behavior tree stores only tree structure information, without any entity related states and data.
- The function
-
Execution status enums [↑]:
bt::Status::RUNNING bt::Status::FAILURE bt::Status::SUCCESS
-
Classification of nodes : [↑]
Node Base class of all kinds of nodes. | InternalNode Contains one or more children. | | SingleNode Contains exactly a single child. | | | RootNode The BT tree root. | | | DecoratorNode Decorates its child node. | | CompositeNode Combining behaviors for multiple nodes. | | | SequenceNode Run children nodes sequentially until all SUCCESS or one FAILURE. | | | SelectorNode Run children nodes sequentially until one SUCCESS or all FAILURE. | | | ParallelNode Run children nodes parallelly, SUCCESS if all SUCCESS, otherwise FAILURE. | LeafNode Contains no children. | | ActionNode Executes a specific task/action. | | ConditionNode Tests a specific condition.
-
TreeBlob [↑]
A TreeBlob stores the entity-related states data for all nodes in a tree.
One bt tree and one entity instance correspond to a TreeBlob instance.
There are two kinds of tree blobs:
-
bt::DynamicTreeBlob
contains a vector of dynamically allocated unique pointers to node blobs. -
bt::FixedTreeBlob
contains a fixed size 2d array.// NumNodes is the max number of nodes to store. // MaxSizeNodeBlob is the max value of the sizes of node blobs to store. bt::FixedTreeBlob<NumNodes, MaxSizeNodeBlob> blob;
FixedTreeBlob
performs a bit faster than theDynamicTreeBlob
.These two template parameters can be obtained through the interface
root.NumNodes()
andMaxSizeNodeBlob()
. This requires compiling the built behavior tree first, executing it, outputting this information, and then filling it in the code that defines these FixedTreeBlobs in the entity.
To declare a stateful bt node on top of tree blob, checkout the following node-blob section.
-
-
Action [↑]
Define a class that inherits from
bt::ActionNode
, and implement theUpdate
method:class A : public bt::ActionNode { public: // TODO: Implements this bt::Status Update(const bt::Context& ctx) override { } // The name of this Action. std::string_view Name() const override { return "A"; } };
To use a
Action
:.Action<A>()
To define a stateful action node, that is the node depends on entity-related stateful data. We can define a
NodeBlob
struct at first: [↑]:struct ANodeBlob : bt::NodeBlob { // data fields storing entity related data. // It's recommended to set a initial value for each field. };
And then overrides the interface
GetNodeBlob
:class A : public bt::ActionNode { public: // Every stateful Node class should declare its own Blob type member. using Blob = ANodeBlob; // Should override this method, returns a pointer to the base node blob type. // getNodeBlob is a method provided by bt library, defined in class `Node`. NodeBlob* GetNodeBlob() const override { return getNodeBlob<ANodeBlob>(); } // Use getNodeBlob<ANodeBlob>() to access the pointer to this's node's data blob. bt::Status Update(const bt::Context& ctx) override { ANodeBlob* b = getNodeBlob<ANodeBlob>(); b->data = 1; // example } };
For other node types, are all the same way to define entity-related stateful node classes.
-
Condition [↑]
A
Condition
is a leaf node without children, it succeeds only if theCheck()
method returnstrue
.And there's no
RUNNING
status for a condition node.To implement a "static condition", just define a class derived from class
bt::ConditionNode
, and implements theCheck
method:class C : public bt::ConditionNode { public: // TODO: Implements this bool Check(const bt::Context& ctx) override { return true; } std::string_view Name() const override { return "C"; } };
Example to use a static condition:
root .Sequence() ._().Condition<C>() // Condition is a leaf node. ._().Action<A>()
We can also make a
Condition
directly from a lambda function dynamically:root .Sequence() ._().Condition([=](const Context& ctx) { return false; }) ._().Action<A>() ;
-
Sequence [↑]
A
SequenceNode
executes its child nodes sequentially, succeeding only if all children succeed. It behaves akin to the logical AND operation, especially for theCondition
children nodes.// If A, B, C are all SUCCESS, the sequence node goes SUCCESS, otherwise FAILURE. root .Sequence() ._().Action<A>() ._().Action<B>() ._().Action<C>() ;
-
Selector [↑]
A
SelectorNode
executes its child nodes sequentially, one by one. It succeeds if any child succeeds and fails only if all children fail. It behaves similarly to the logical OR operation// If A, B, C are all FAILURE, the selector node goes FAILURE, otherwise SUCCESS. root .Selector() ._().Action<A>() ._().Action<B>() ._().Action<C>() ;
-
Parallel [↑]
A
ParallelNode
achieves success when all of its child nodes succeed. It will execute all of its children "simultaneously", until some child fails. In detail, it executes all children, then aggregates the results of the child nodes' execution to get theParallelNode
's status.// A, B, C are executed parallelly. // If all children SUCCESS, the parallel node goes SUCCESS, otherwise FAILURE. root .Parallel() ._().Action<A>() ._().Action<B>() ._().Action<C>() ;
-
RandomSelector [↑]
RandomSelector will randomly select a child node to execute until it encounters a successful one.
If its children nodes implement the function
Priority()
, it will follow a weighted random approach, that is to say, the node with a greater weight will be easier to be selected.One use of randomly selecting nodes is to make the AI's behavior less rigid.
root .RandomSelector() ._().Action<A>() ._().Action<B>() ._().Action<C>() ;
-
Priority [↑]
By default, children nodes are equal in weight, that is, their priorities are equal (all are defaulted to 1). For composite nodes, their child nodes are examined from top to bottom.
However, in order to support the "dynamic priority" feature, for example, the behavior of each child node has a dynamic scoring mechanism, and the child node with the highest score should be selected for execution each tick, for such cases, the class
Node
supports overriding aPriority
function.class A : public bt::ActionNode { public: unsigned int Priority(const bt::Context& ctx) const override { // TODO, returns a number > 0 } };
Child nodes with higher priority will be considered firstly. If they're equal, then in order, the one above will take precedence.
It's recommended to implement this function fast enough, since it will be called on each tick. For instance, we may not need to do the calculation on every tick if it's complex. Another optimization is to separate calculation from queries, for example, pre-cache the result somewhere on the blackboard, and just ask it from memory here.
All composite nodes, including stateful ones, will respect to its children's
Priority()
functions. -
Stateful Nodes [↑]
The 4 composite nodes all support stateful ticking:
StatefulSequence
,StatefulSelector
,StatefulRandomSelector
andStatefulParallel
.The word "stateful" means that skipping the children already succeeded (failed for selectors), instead of ticking every child.
// For instance, the following A will be skipped by Tick() once it // turns SUCCESS, only B will got future ticks. root .StatefulSequence() ._().Action<A>() ._().Action<B>()
Another example:
// the following A will be skipped by Tick() once it // turns FAILURE, only B will got future ticks. root .StatefulSelector() ._().Action<A>() ._().Action<B>()
The stateful data for stateful compositors are all stored in their
NodeBlob
structs. -
Switch/Case
are just syntax sugar based onSelector
andIf
: [↑]// Only one case will success, or all fail. // Cases will be tested sequentially from top to bottom, one fails and then another. .Switch() ._().Case<ConditionX>() ._()._().Action<TaskX>() ._().Case<ConditionY>() ._()._().Action<TaskY>()
-
Decorators [↑]
-
If
executes its child node only if given condition turnstrue
. [↑].If<SomeCondition>() ._().Action<Task>()
-
Invert()
inverts its child node's execution status. [↑].Invert() ._().Action<Task>() // how it inverts: // RUNNING => RUNNING // SUCCESS => FAILURE // FAILURE => SUCCESS
-
Repeat(n)
(aliasLoop
) repeats its child node' execution for exactlyn
times, it fails immediately if its child fails. [↑]We can name the process a "round", that from the start (turns
RUNNING
) to the termination (turnsSUCCESS
orFAILURE
). TheRepeat(n)
node performsn
rounds of its child's execution.// Repeat action A three times. .Repeat(3) ._().Action<A>()
-
Timeout
executes its child node with a time duration limitation, fails on timeout. [↑]using namespace std::chrono_literals; .Timeout(3000ms) ._().Action<Task>()
-
Delay
waits for a certain time duration before executing its child node. [↑]using namespace std::chrono_literals; .Delay(1000ms) ._().Action<Task>()
-
Retry
retries its child node on failure, for a maximum ofn
times, with a giveninterval
. [↑]The following code retry the
Task
on failure for at most 3 times, the retry interval is1000ms
:It immediately returns
SUCCESS
if the child succeeds.using namespace std::chrono_literals; .Retry(3, 1000ms) ._().Action<Task>()
-
ForceSuccess
executes its child node and checks its status, returnsRUNNING
if the decorated node isRUNNING
, otherwise always returnsSUCCESS
. [↑].ForceSuccess() ._().Actino<Task>()
ForceFailure
returnsRUNNING
if the decorated node isRUNNING
, otherwise always returnsFAILURE
. -
Custom Decorator [↑]
To implement a custom decorator, just inherits from
bt::DecoratorNode
:class CustomDecorator : public bt::DecoratorNode { public: std::string_view Name() const override { return "CustomDecorator"; } bt::Status Update(const bt::Context& ctx) override { // TODO: run the child node. // child->Tick(ctx); } };
It's a common case that a decorator node need to manage some stateful data, if they are entity-related, we can define a
NodeBlob
for this decorator class to work together, checkout the section above: stateful action node.
-
-
Sub Tree [↑]
A behavior tree can be used as another behavior tree's child:
bt::Tree root, subtree; root .Sequence() ._().Action<A>() ._().Subtree<A>(std::move(subtree));
Once a subtree is
moved
onto another tree, the subtree itself is completely useless, all its resources are belong to the parent tree.If you want to clone a tree for multiple instances, for duplicating behaviors purpose, you could make a factory function:
auto Walk = [&](int poi) { bt::Tree subtree("Walk"); // clang-format off subtree .Sequence() ._().Action<Movement>(poi) ._().Action<Standby>() .End() ; // clang-format on return subtree; }; root. .RandomSelector() ._().Subtree(Walk(point1)) ._().Subtree(Walk(point2)) .End();
-
The tick
Context
[↑]struct Context { ull seq; // ticking seq number. std::chrono::nanoseconds delta; // delta time since last tick, to current tick. std::any data; // user data. }
A main purpose of the
Context
struct is, able to access enviroment/world data in the behavior classes. -
Hook Methods [↑]
For each
Node
, there are three hook functions:class MyNode : public Node { public: // Will be called on the node's first run of a round. // Each time the node changes from other status to RUNNING. virtual void OnEnter(const Context& ctx){}; // Will be called on the node's last run of a round. // Each time the node goes to FAILURE/SUCCESS. virtual void OnTerminate(const Context& ctx, Status status){}; // Hook function to be called on this node's build is finished. virtual void OnBuild() {} }
-
Visualization [↑]
There's a simple real time behavior tree visualization function implemented in
bt.cc
.It simply displays the tree structure and execution status on the console. The nodes colored in green are those currently executing and are synchronized with the latest tick cycles.
Example code to call the
Visualize
function:// In the tick loop ++ctx.seq; root.Tick(ctx) root.Visualize(ctx.seq)
-
Blackboard ? [↑]
In fact, if there's no need for non-programmer usage, behavior trees and blackboards don't require a serialization mechanism. In such cases, using a plain
struct
as the blackboard is a simple and fast approach.When there's one behavior tree for multiple entities, blackboard could be a "world" instance or a reference accessable to entities in the world.
struct Blackboard { World* world; }; // Pass a pointer to blackboard to the tick context. auto bb = std::make_shared<Blackboard>(); bt::Context ctx(bb);
In the
Update()
function:bt::Status Update(const bt::Context& ctx) override { auto bb = std::any_cast<std::shared_ptr<Blackboard>>(ctx.data); // TODO Access bb->field. }
-
Ticker Loop [↑]
There's a simple builtin ticker loop implemented in
bt.cc
, to use it:root.TickForever(ctx, 100ms);
-
Custom Builder [↑]
// Supposes that we need to add a custom decorator. class MyCustomMethodNode : public bt::DecoratorNode { public: MyCustomMethodNode(std::string_view& name, ..) : bt::DecoratorNode(name) {} // Implements the Update function. bt::Status Update(const bt::Context& ctx) override { // Propagates ticking to the child. // child->Tick(ctx) return status; } }; // Make a custom Tree class. class MyTree : public bt::RootNode, public bt::Builder<MyTree> { public: // Bind the builder to this tree inside the construct function. MyTree(std::string_view name = "Root") : bt::RootNode(name), Builder() { bindRoot(*this); } // Implements the custom builder method. // C is the method to creates a custom Node to attach to the tree. auto& MyCustomMethod(...) { return C<MyCustomMethodNode>(...); } }; MyTree root; root .MyCustomMethod(...) ;
-
Working with Signals/Events [↑]
It's a common case to emit and receive signals in a behavior tree. But signal (or event) handling is a complex stuff, I don't want to couple with it in this small library.
General ideas to introduce signal handling into bt.cc is:
- Creates a custom decorator node, supposed named
OnSignalNode
. - Creates a custom builder class, and add a method named
OnSignal
. - The
OnSignal
decorator propagates the tick down to its child only if the corresponding signal fired. - The data passing along with the fired signal, can be placed onto the blackboard temporary.
- You can move the OnSignal node as high as possible to make the entire behavior tree more event-driven.
Here's an example in detail to combine my tiny signal library blinker.h with bt.cc, please checkout the code example in folder example/onsignal.
root .Parallel() ._().Action<C>() ._().OnSignal("a.*") // once signal not matched here,ticking stop to propagate downward ._()._().Parallel() ._()._()._().OnSignal("a.a") ._()._()._()._().Action<A>() ._()._()._().OnSignal("a.b") ._()._()._()._().Action<B>() .End() ;
- Creates a custom decorator node, supposed named
-
Tree traversal [↑]
There's a simple method to traversal a bt tree in a depth-first way, example code:
// Called before touching a `node`, the ptr is the unique_ptr instance holding this node. // Notes that ptr is nullptr for case `node equals root`. bt::TraversalCallback preOrder = [&](bt:Node& node, bt::Ptr<bt::Node>& ptr) { // TODO }; // Called after the `node` and all of its children are touched. // ptr is the same meaning as previous preOrder callback. bt::TraversalCallback postOrder = [&](bt::Node& node, bt::Ptr<bt::Node>& ptr) { // TODO }; // And, we can use bt::NullTraversalCallback for empty callback. root.Traverse(preOrder, postOrder, NullNodePtr);
BSD.