OpenSource-BehaviorTree.CPP_v4.9_Complete_Guide(LLM Generated)
OpenSource-BehaviorTree.CPP_v4.9_Complete_Guide(LLM Generated)
Version: 4.9.0 | Last updated: 2026-07 | License: MIT
Based on official documentation, Doxygen API reference, and source code verification (v4.9.0).
For official docs: https://www.behaviortree.dev/ | API: https://behaviortree.github.io/BehaviorTree.CPP/
Table of Contents
- What is a Behavior Tree?
- Core Concepts
- Quick Start
- Node Types and Class Hierarchy
- Control Nodes
- Decorator Nodes
- Built-in Action Nodes
- Blackboard and Port System
- XML Format
- Scripting Language
- Pre/Post Conditions
- Asynchronous Actions
- SubTrees
- Logging and Debugging
- Groot2 Integration
- Substitution Rules and Mock Testing
- Global Blackboard Pattern
- Registration Methods
- Migration from v3.x to v4.x
- Advanced Features
- Complete Tutorial Examples
- Best Practices
1. What is a Behavior Tree?
A Behavior Tree (BT) is a hierarchical node structure for organizing task switching in autonomous agents (robots, game AI). Compared to Finite State Machines (FSM):
| Aspect | Behavior Tree | FSM |
|---|---|---|
| Hierarchy | Native, composable subtrees | Manual state management |
| Reusability | Subtrees are reusable | States hard to reuse |
| Readability | Graphical, clear semantics | Transitions hard to follow |
| Reactivity | Built-in reactive nodes | Requires extra design |
| Debugging | Visual, recordable, replayable | Few standard tools |
2. Core Concepts
2.1 Tick Mechanism
The tree executes via tick signals propagated from root to leaves. Each tick:
- Root receives the tick signal
- Signal propagates down to leaf nodes
- Leaf nodes execute and return a status
- Status propagates up to determine next execution
2.2 Node Status
enum class NodeStatus {
IDLE = 0, // Not executed yet (never returned by user code)
RUNNING = 1, // Currently executing
SUCCESS = 2, // Completed successfully
FAILURE = 3, // Completed with failure
SKIPPED = 4 // Skipped (v4+, used by PreConditions)
};
Utility functions (defined in basic_types.h:42-50):
bool isStatusActive(const NodeStatus& status); // !IDLE && !SKIPPED
bool isStatusCompleted(const NodeStatus& status); // SUCCESS || FAILURE
Custom leaf nodes must NEVER return
IDLEorSKIPPED.SKIPPEDis reserved for PreConditions.
2.3 NodeType Enum
enum class NodeType {
UNDEFINED = 0,
ACTION,
CONDITION,
CONTROL,
DECORATOR,
SUBTREE
};
2.4 Service-Oriented Architecture
BT.CPP follows a service-oriented model: leaf nodes (actions/conditions) are client code that communicates with external server components (sensors, actuators, planners) via a middleware like ROS 2, shared memory, or custom IPC. The tree itself is a coordinator — it decides when and in what order to invoke services, but does not own the services themselves.
This separation of concerns means:
- Leaf nodes are thin wrappers around service calls
- The BT logic is entirely in XML, independent of service implementation
- Services can be developed, tested, and debugged in isolation
2.5 Synchronous vs Asynchronous
| Type | Behavior | Use Case |
|---|---|---|
| Synchronous | Blocks until done, returns SUCCESS/FAILURE | Quick checks, simple actions |
| Asynchronous | Returns RUNNING, resumed later, halt-able | Long operations, external service calls |
3. Quick Start
3.1 Building from Source
git clone https://github.com/BehaviorTree/BehaviorTree.CPP.git
cd BehaviorTree.CPP && mkdir build && cd build
cmake .. -DBTCPP_GROOT_INTERFACE=OFF -DBTCPP_SQLITE_LOGGING=OFF
cmake --build . --parallel
sudo cmake --install .
3.2 Hello World
#include <behaviortree_cpp/bt_factory.h>
BT::NodeStatus SayHello()
{
std::cout << "Hello World!\n";
return BT::NodeStatus::SUCCESS;
}
int main()
{
BT::BehaviorTreeFactory factory;
// Register a simple action using a function pointer
factory.registerSimpleAction("SayHello", std::bind(SayHello));
// Trees are defined at deployment-time with XML
std::string xml = R"(
<root BTCPP_format="4">
<BehaviorTree ID="MainTree">
<Sequence>
<SayHello />
</Sequence>
</BehaviorTree>
</root>
)";
auto tree = factory.createTreeFromText(xml);
tree.tickWhileRunning(); // v4+ recommended execution loop
return 0;
}
3.3 Execution Loop Methods
// v4+ recommended: internal loop with sleep (interruptible)
NodeStatus status = tree.tickWhileRunning(sleep_ms);
// Equivalent to v3.8 behavior: single tick, no internal loop
NodeStatus status = tree.tickExactlyOnce();
// Tick once, but re-tick if RUNNING with pending wake-up (no sleep)
NodeStatus status = tree.tickOnce();
Tree::sleep(timeout) can be interrupted by TreeNode::emitWakeUpSignal(), allowing immediate re-ticking. This is the event-driven mechanism: instead of busy-waiting, the tree sleeps and wakes up instantly when a running node signals a state change (e.g., a response arrives from an external server). Use emitWakeUpSignal() inside onRunning() of StatefulActionNode when new data is available.
// In a StatefulActionNode's onRunning():
if (response_received) {
return NodeStatus::SUCCESS;
}
emitWakeUpSignal(); // wake the tree immediately when more data expected
return NodeStatus::RUNNING;
Tree class API (defined in include/behaviortree_cpp/bt_factory.h):
// Lifecycle
void initialize(); // Create WakeUpSignal and bind to all nodes
void haltTree(); // Halt all nodes recursively
// Node lookup
template <typename NodeType = TreeNode>
std::vector<const TreeNode*> getNodesByPath(
StringView wildcard_filter) const; // Type+wildcard filtered search
// Introspection
uint16_t getUID(); // Incrementing unique numeric ID
std::shared_ptr<WakeUpSignal> wakeUpSignal() const; // Shared wake-up mechanism
Blackboard::Ptr rootBlackboard(); // Root blackboard (for @ prefix)
4. Node Types and Class Hierarchy
TreeNode
├── LeafNode
│ ├── ActionNodeBase
│ │ ├── SyncActionNode (tick() → SUCCESS/FAILURE synchronously)
│ │ │ └── EntryUpdatedAction (registered as WasEntryUpdated)
│ │ ├── StatefulActionNode (onStart/onRunning/onHalted lifecycle)
│ │ ├── ThreadedAction (tick() runs in a separate thread)
│ │ ├── CoroActionNode (coroutine-based async via setStatusRunningAndYield())
│ │ └── SimpleActionNode (wraps a std::function for convenience)
│ └── ConditionNode
│ └── SimpleConditionNode (wraps a std::function for convenience)
├── ControlNode (1..N children)
│ ├── SequenceNode (also AsyncSequence with make_async=true)
│ ├── SequenceWithMemory
│ ├── ReactiveSequence
│ ├── FallbackNode (also AsyncFallback with make_asynch=true)
│ ├── ReactiveFallback
│ ├── ParallelNode
│ ├── ParallelAllNode
│ ├── IfThenElseNode
│ ├── WhileDoElseNode
│ ├── SwitchNode (2..6)
│ ├── TryCatchNode
│ └── ManualSelectorNode
└── DecoratorNode (1 child)
├── InverterNode
├── ForceSuccessNode
├── ForceFailureNode
├── RepeatNode
├── RetryNode
├── KeepRunningUntilFailureNode
├── TimeoutNode
├── DelayNode
├── RunOnceNode
├── PreconditionNode
├── SubTreeNode
├── LoopNode<T> (template, replaces removed ConsumeQueue)
├── EntryUpdatedDecorator (SkipUnlessUpdated / WaitValueUpdate)
4.1 Constructor Patterns
All node types that declare ports MUST use this constructor:
MyNode(const std::string& name, const BT::NodeConfig& config);
Nodes without ports can use the shorter form:
MyNode(const std::string& name);
4.2 TreeNode Public API Reference
#include <behaviortree_cpp/tree_node.h>
| Method | Return Type | Description |
|---|---|---|
status() |
NodeStatus |
Current node status |
haltNode() |
void |
Interrupt execution of this node (calls halt(), then executes _onHalted post-condition script) |
isHalted() |
bool |
Whether the node was halted |
waitValidStatus() |
NodeStatus |
Block until status is RUNNING/SUCCESS/FAILURE |
name() |
const std::string& |
Instance name (not the type) |
registrationName() |
const std::string& |
Factory registration ID (type name, e.g. "MyAction") |
UID() |
uint16_t |
Unique numeric ID (depth-first traversal order) |
fullPath() |
const std::string& |
Hierarchical path: "subtree_name/node_name" |
config() |
const NodeConfig& |
Read-only node configuration (blackboard, ports, manifest) |
executeTick() |
NodeStatus |
Public tick entry point: checks pre-conditions (IDLE/SKIPPED only), runs pre-tick callback, calls tick() with exception wrapping + duration monitoring, checks post-conditions, runs post-tick callback, then setStatus() (SKIPPED preserves IDLE internally) |
tick() |
NodeStatus |
User-defined logic (protected, override this) |
getInput<T>(key, dest) |
Result |
Read port into existing variable |
getInput<T>(key) |
Expected<T> |
Read port returning optional value |
setOutput<T>(key, value) |
Result |
Write to output port |
getRawPortValue(key) |
StringView |
Raw port value without any type conversion |
getInputStamped<T>(key, dest) |
Expected<Timestamp> |
Read port with timestamp |
getInputStamped<T>(key) |
Expected<StampedValue<T>> |
Read port returning stamped value |
getLockedPortContent(key) |
AnyPtrLocked |
Thread-safe zero-copy port access |
emitWakeUpSignal() |
void |
Notify tree to tick again |
requiresWakeUp() |
bool |
Whether node needs wake-up signal |
subscribeToStatusChange(cb) |
StatusChangeSubscriber |
Subscribe to status transitions |
setPreTickFunction(cb) |
void |
Callback executed before tick |
setPostTickFunction(cb) |
void |
Callback executed after tick |
setTickMonitorCallback(cb) |
void |
Callback for tick duration monitoring |
Static utility methods:
static bool isBlackboardPointer(StringView str, StringView* stripped = nullptr);
static StringView stripBlackboardPointer(StringView str);
static Expected<StringView> getRemappedKey(StringView port_name, StringView remapped_port);
template <class DerivedT, typename... ExtraArgs>
static std::unique_ptr<TreeNode> Instantiate(const std::string& name,
const NodeConfig& config,
ExtraArgs... args);
isBlackboardPointer() checks if a string matches the {...} pattern. Instantiate<>() creates a node instance using either the full constructor (name, config, args...) or the name-only constructor (name) — detected at compile time.
Runtime port remapping (protected, available to derived classes):
void modifyPortsRemapping(const PortsRemapping& new_remapping);
This allows a node to change its port bindings at runtime, e.g. to point to different blackboard keys based on dynamic conditions.
5. Control Nodes
5.1 Sequence
Executes children in order. All must succeed for the sequence to succeed.
<Sequence>
<Action1 /> <!-- SUCCESS → continue -->
<Action2 /> <!-- SUCCESS → continue -->
<Action3 /> <!-- SUCCESS → overall SUCCESS -->
</Sequence>
FAILURE→ reset, return FAILURERUNNING→ return RUNNING, re-tick same child next timeSKIPPED→ skip to next child
Common BT design pattern — "Find the BUG!": A naive Sequence can leave the system in a bad state when a middle child fails:
<!-- BUG: If GrabBeer fails, the fridge stays open! -->
<Sequence>
<OpenFridge /> <!-- SUCCESS → fridge is now open -->
<GrabBeer /> <!-- FAILURE → sequence resets, but fridge stays open -->
<CloseFridge /> <!-- never executed -->
</Sequence>
Fix: Use a Fallback to ensure cleanup, or restructure with guarded actions:
<!-- Fix: use Inverter + Fallback to guard open/close -->
<Fallback>
<Sequence>
<OpenFridge />
<GrabBeer />
<CloseFridge />
</Sequence>
<CloseFridge /> <!-- cleanup: always close -->
</Fallback>
5.2 AsyncSequence
Same logic as Sequence, but yields once after each async child succeeds (IDLE→SUCCESS) to allow interruption by a reactive parent. Sync children that complete in one tick pass through without yielding. Implemented by registering SequenceNode with make_async=true.
<AsyncSequence>
<SyncActionA /> <!-- SUCCESS → yield, return RUNNING -->
<SyncActionB /> <!-- next tick -->
</AsyncSequence>
// Declaration (include/behaviortree_cpp/controls/sequence_node.h):
SequenceNode(const std::string& name, bool make_async = false,
const NodeConfiguration& conf = NodeConfiguration());
5.3 ReactiveSequence
Re-evaluates all children every tick. When any child returns RUNNING, halts all other children (both earlier and later). Only one child may be RUNNING at a time; throws if a second child also returns RUNNING (can be disabled via ReactiveSequence::EnableException(false)).
<ReactiveSequence>
<IsEnemyVisible /> <!-- re-checked every tick -->
<ApproachEnemy /> <!-- halted if condition becomes false -->
</ReactiveSequence>
5.4 SequenceWithMemory
Skips children that already succeeded. If a middle child fails, the index is NOT reset (unlike Sequence), so earlier succeeded children are NOT retried. On halt, the index is also NOT reset.
<SequenceWithMemory>
<GoTo location="A" />
<GoTo location="B" />
<GoTo location="C" />
</SequenceWithMemory>
5.5 Comparison Table
| Node | FAILURE | RUNNING | Yields Between Children | Re-evaluates Completed |
|---|---|---|---|---|
Sequence |
Reset, FAILURE | Re-tick same | No | No |
AsyncSequence |
Reset, FAILURE | Re-tick same | Yes | No |
ReactiveSequence |
Reset, FAILURE | Restart sequence | No | Yes (every tick) |
SequenceWithMemory |
Re-tick same | Re-tick same | No | No (skips succeeded) |
5.6 Fallback
Tries children in order. Returns SUCCESS as soon as one child succeeds. Also known as "Selector" or "Priority" in other frameworks.
<Fallback>
<PlanA /> <!-- FAILURE → try next -->
<PlanB /> <!-- SUCCESS → overall SUCCESS -->
</Fallback>
Common BT pattern — guarded action with Inverter: Use Inverter + condition to skip an action when the condition is already met:
<!-- Tick: if door is NOT closed, go open it -->
<Fallback>
<Inverter>
<IsDoorClosed /> <!-- if door IS closed → SUCCESS → Inverter → FAILURE -->
</Inverter>
<OpenDoor /> <!-- executed only if IsDoorClosed returned FAILURE (door open) -->
</Fallback>
This pattern prevents calling OpenDoor when the door is already closed. Same logic as an if guard in code.
5.7 AsyncFallback
Same as Fallback, but yields once after each async child fails (IDLE→FAILURE) to allow interruption. Sync children pass through without yielding. Implemented by registering FallbackNode with make_asynch=true.
5.8 ReactiveFallback
Re-evaluates all children every tick. If a condition in an earlier child changes from FAILURE to SUCCESS, halts the running child.
<ReactiveFallback>
<AreYouRested /> <!-- re-checked every tick -->
<Sleep /> <!-- halted if condition becomes true -->
</ReactiveFallback>
5.9 Fallback Comparison
| Node | RUNNING | All Children SKIPPED | Yields Between Children |
|---|---|---|---|
Fallback |
Re-tick same | SKIPPED | No |
AsyncFallback |
Re-tick same | SKIPPED | Yes |
ReactiveFallback |
Restart sequence | SKIPPED | No |
If all children return SKIPPED, the Fallback returns SKIPPED. The SKIPPED status propagates to the parent control node.
5.10 Parallel
Ticks all children each time. Children may be in RUNNING simultaneously. Thresholds are checked after each child (early termination; does NOT wait for all children). Configurable thresholds:
<!-- Default: all must succeed, any failure fails -->
<Parallel>
<ActionA />
<ActionB />
</Parallel>
<!-- Custom: 2 succeed → SUCCESS, 2 fail → FAILURE -->
<Parallel success_count="2" failure_count="2">
<ActionA />
<ActionB />
<ActionC />
</Parallel>
| Port | Type | Default | Description |
|---|---|---|---|
success_count |
InputPort<int> | -1 (all) | Required successes |
failure_count |
InputPort<int> | 1 | Failure threshold |
Negative values mean "all children" (same as num_children).
Programmatic API (defined in include/behaviortree_cpp/controls/parallel_node.h):
size_t successThreshold() const; // Current success threshold
size_t failureThreshold() const; // Current failure threshold
void setSuccessThreshold(int threshold);
void setFailureThreshold(int threshold);
virtual void halt() override;
These allow reading/writing thresholds at runtime without XML ports.
5.11 ParallelAll
Executes ALL children regardless of results. Never halts early (waits for every child to complete before evaluating thresholds).
<ParallelAll max_failures="2">
<ActionA />
<ActionB />
<ActionC />
</ParallelAll>
| Port | Type | Default | Description |
|---|---|---|---|
max_failures |
InputPort<int> | 1 | Max allowed failures |
5.12 IfThenElse
Condition evaluated once. Two or three children: condition, then-branch, optional else-branch.
<IfThenElse>
<IsDoorOpen /> <!-- condition (once) -->
<WalkThrough /> <!-- then (if condition succeeded) -->
<TryAnotherPath /> <!-- else (optional, if condition failed) -->
</IfThenElse>
5.13 WhileDoElse
Condition evaluated every tick. Reactive: if condition changes while child is RUNNING, halts and switches branches.
<WhileDoElse>
<IsBatteryOK /> <!-- re-checked every tick -->
<ContinueMission /> <!-- if condition is true -->
<GoToCharge /> <!-- else (optional) -->
</WhileDoElse>
5.14 Switch (Switch2–Switch6)
Selects child based on blackboard variable value:
<Switch3 variable="{robot_state}"
case_1="IDLE"
case_2="WORKING"
case_3="ERROR">
<HandleIdle />
<HandleWorking />
<HandleError />
<HandleUnknown /> <!-- default (last child) -->
</Switch3>
- Supported: string, int, float, registered enums
- If variable changes mid-execution, running child is halted
- If the matching child returns SKIPPED, Switch also returns SKIPPED
5.15 TryCatch
Executes children 1..N-1 as a "try" block. If any try-child fails, executes child N as "catch".
<TryCatch>
<Sequence>
<Step1 />
<Step2 />
</Sequence>
<Cleanup /> <!-- executed only if try-block fails -->
</TryCatch>
| Port | Type | Default | Description |
|---|---|---|---|
catch_on_halt |
InputPort<bool> | false | Execute catch if halted during try |
Requires at least 2 children.
5.16 ManualSelectorNode
Uses ncurses TUI to let the user manually select which child to execute.
Note: Not a built-in. Must be registered explicitly:
factory.registerNodeType<ManualSelectorNode>("ManualSelector");
Port:
| Port | Type | Default | Description |
|---|---|---|---|
repeat_last_selection |
InputPort<bool> | false | Re-use last selection on next tick |
5.17 ControlNode Programmatic API
The ControlNode base class (include/behaviortree_cpp/control_node.h) provides methods for managing children programmatically:
#include <behaviortree_cpp/control_node.h>
// Child management
void addChild(TreeNode* child); // Append child
size_t childrenCount() const; // Number of children
const std::vector<TreeNode*>& children() const; // All children
const TreeNode* child(size_t index) const; // Child at index (const only)
// Halting
virtual void halt() override; // Halt this node (and reset children)
void haltChildren(); // Same as resetChildren()
void haltChild(size_t i); // Halt single child at index
void haltChildren(size_t first); // Halt children from index first onward
// Reset
void resetChildren(); // Reset all children to IDLE + halt RUNNING ones
Example — iterating over children:
for (size_t i = 0; i < childrenCount(); i++)
{
auto status = child(i)->executeTick();
// ...
}
6. Decorator Nodes
6.1 Inverter
Inverts child result: SUCCESS ↔ FAILURE, RUNNING → RUNNING, SKIPPED → SKIPPED (passthrough).
6.2 ForceSuccess / ForceFailure
Forces the child's result to SUCCESS or FAILURE (passes RUNNING and SKIPPED through).
6.3 Repeat
Ticks child N times. Child must return SUCCESS each time.
<Repeat num_cycles="3">
<ClapYourHandsOnce />
</Repeat>
| Port | Type | Description |
|---|---|---|
num_cycles |
InputPort<int> | Repeat count, -1 = infinite |
- Child FAILURE → immediate FAILURE (counter reset to 0)
- Child RUNNING → counter NOT incremented
- Child SKIPPED → reset child but counter NOT incremented
- Counter resets to 0 on SUCCESS and on halt
- Async optimization: yields RUNNING between cycles when child is async
6.4 RetryUntilSuccessful
Ticks child up to N times until it succeeds.
<RetryUntilSuccessful num_attempts="5">
<OpenDoor />
</RetryUntilSuccessful>
| Port | Type | Description |
|---|---|---|
num_attempts |
InputPort<int> | Max attempts, -1 = infinite |
- Child SUCCESS → immediate SUCCESS (counter reset to 0)
- Child RUNNING → counter NOT incremented
- Child SKIPPED → child is reset, return SKIPPED
num_attemptsis re-read from the port on each failure, allowing dynamic in-flight modification- Counter resets to 0 on SUCCESS and on halt
- Async optimization: yields RUNNING between attempts when child is async
6.5 Timeout
Halts child if it runs longer than specified milliseconds.
<Timeout msec="5000">
<LongRunningAction />
</Timeout>
| Port | Type | Description |
|---|---|---|
msec |
InputPort<unsigned> | Timeout in milliseconds (0 = no timeout) |
msec=0disables the timer entirely (child runs with no time limit)
6.6 Delay
Waits specified milliseconds before ticking child.
<Delay delay_msec="5000">
<Action />
</Delay>
| Port | Type | Description |
|---|---|---|
delay_msec |
InputPort<unsigned> | Delay in milliseconds |
6.7 RunOnce
Executes child only once. After completion, behavior depends on then_skip:
then_skip |
Behavior |
|---|---|
| TRUE (default) | Return SKIPPED on subsequent ticks |
| FALSE | Return same status forever |
<RunOnce then_skip="false">
<Initialize />
</RunOnce>
Note: If the child is async (returns RUNNING), the node continues ticking it until it completes (SUCCESS/FAILURE) before the "once" behavior takes effect. A perpetually RUNNING child will be ticked indefinitely.
6.8 KeepRunningUntilFailure
Returns RUNNING as long as child returns SUCCESS. Stops when child fails.
<KeepRunningUntilFailure>
<MonitorSensor />
</KeepRunningUntilFailure>
Warning: If child returns SKIPPED, behavior is undefined (no explicit handler). Avoid using SKIPPED children under this decorator.
6.9 Precondition
Evaluates a script condition before ticking child:
<Precondition if="battery_level > 20" else="FAILURE">
<MoveToGoal />
</Precondition>
<!-- With else="RUNNING": condition re-evaluated while child is running -->
<Precondition if="sensor_ok" else="RUNNING">
<LongRunningAction />
</Precondition>
| Port | Type | Default | Description |
|---|---|---|---|
if |
InputPort<string> | (required) | Script condition |
else |
InputPort<NodeStatus> | FAILURE | Status if condition false |
- Once child starts RUNNING, condition is NOT re-evaluated until child completes
- Use
else="RUNNING"for per-tick re-evaluation pattern - Note: Precondition does NOT explicitly set its own status to RUNNING (unlike other decorators)
6.10 SubTree
Wraps a sub-tree with a separate blackboard (see §13 Subtrees).
6.11 LoopNode
Replacement for deprecated ConsumeQueue. Pops elements from a std::deque and ticks child for each element.
<LoopInt queue="{waypoints}" value="{wp}" if_empty="SUCCESS">
<UseWaypoint waypoint="{wp}" />
</LoopInt>
| Port | Type | Default | Description |
|---|---|---|---|
queue |
BidirectionalPort | (required) | Queue (SharedQueue<T> or std::vector<T>) |
value |
OutputPort<T> | — | Popped element |
if_empty |
InputPort<NodeStatus> | SUCCESS | Status when queue is empty |
Pre-registered types:
LoopInt(int)LoopBool(bool)LoopDouble(double)LoopString(std::string)
If the child returns FAILURE, the loop aborts immediately and returns FAILURE (does not continue to next element).
For custom types, register manually:
factory.registerNodeType<LoopNode<MyType>>("LoopMyType");
6.12 EntryUpdatedDecorator
Checks whether a blackboard entry was updated since last checked.
| Port | Type | Default | Description |
|---|---|---|---|
entry |
InputPort<BT::Any> | (required) | Entry to check |
<!-- Returns SKIPPED if not updated -->
<SkipUnlessUpdated entry="{sensor_data}">
<ProcessData />
</SkipUnlessUpdated>
<!-- Returns RUNNING if not updated (keeps tree active) -->
<WaitValueUpdate entry="{command}">
<ExecuteCommand />
</WaitValueUpdate>
Two built-in variants:
SkipUnlessUpdated— returns SKIPPED when not updatedWaitValueUpdate— returns RUNNING when not updated
6.13 ConsumeQueue (Deprecated)
Deprecated in v4.9. Use
LoopNodeinstead.
The ConsumeQueue template class (in decorators/consume_queue.h) is marked [[deprecated]] and is not a built-in. The header still exists and you may register it manually:
factory.registerNodeType<ConsumeQueue<Pose2D>>("ConsumeQueue");
but LoopNode<T> is the recommended replacement. See §6.11.
6.14 DecoratorNode Programmatic API
The DecoratorNode base class (include/behaviortree_cpp/decorator_node.h) provides methods for managing the single child:
#include <behaviortree_cpp/decorator_node.h>
// Child management
void setChild(TreeNode* child); // Set the single child
const TreeNode* child() const; // Read-only child access
TreeNode* child(); // Mutable child access
// Halting
virtual void halt() override; // Halt this decorator
void haltChild(); // Same as resetChild()
// Reset
void resetChild(); // Reset child to IDLE + halt if RUNNING
// Execution
NodeStatus executeTick() override; // Override of TreeNode::executeTick()
SimpleDecoratorNode (used by registerSimpleDecorator) wraps a TickFunctor:
using TickFunctor = std::function<NodeStatus(NodeStatus, TreeNode&)>;
SimpleDecoratorNode(const std::string& name, TickFunctor tick_functor,
const NodeConfig& config);
7. Built-in Action Nodes
These are registered automatically by the factory. No manual registration needed.
| Node Name | Description | Ports |
|---|---|---|
AlwaysSuccess |
Always returns SUCCESS | (none) |
AlwaysFailure |
Always returns FAILURE | (none) |
Script |
Execute script code | InputPort<string> "code" |
ScriptCondition |
Evaluate script, SUCCESS if true | InputPort<string> "code" |
SetBlackboard |
Write value to blackboard | InputPort "value", BidirectionalPort "output_key" |
UnsetBlackboard |
Remove blackboard entry | InputPort<string> "key" |
Sleep |
Sleep for N milliseconds (async) | InputPort<unsigned> "msec" |
WasEntryUpdated |
Check if entry was updated (class: EntryUpdatedAction) |
InputPort<BT::Any> "entry" |
Source: all registered in src/bt_factory.cpp:114-173. AlwaysSuccess, AlwaysFailure, Sleep, etc. are auto-registered. PopFromQueue<T> and QueueSize<T> are template classes that require manual registration (see §7.6).
7.1 Script
<Script code="answer := 42; msg := 'hello'" />
Always returns SUCCESS. See §10 for script syntax.
7.2 ScriptCondition
<ScriptCondition code="battery_level > 20" />
Returns SUCCESS if script evaluates to true, FAILURE otherwise.
7.3 SetBlackboard
<!-- Static value: stores string "42" -->
<SetBlackboard value="42" output_key="the_answer" />
<!-- Copy from another port -->
<SetBlackboard value="{src_port}" output_key="dst_port" />
7.4 Sleep
class SleepNode : public BT::StatefulActionNode { ... };
Asynchronous sleep. Can be interrupted via halt(). Use <Timeout> decorator for bounded waits.
<Sleep msec="5000" />
7.5 WasEntryUpdated (class: EntryUpdatedAction)
Checks if a blackboard entry has been updated (via sequence_id comparison) since the last check.
<WasEntryUpdated entry="{sensor_value}" />
Returns SUCCESS if the entry was written since last check, FAILURE if not updated or missing.
The class hierarchy for this node is EntryUpdatedAction (actions/updated_action.h), registered with XML name WasEntryUpdated in src/bt_factory.cpp:171. Companion decorators (§6.12) provide SkipUnlessUpdated and WaitValueUpdate for blocking/conditional variants.
7.6 Template Actions (not built-in, need manual registration)
These are defined as templates and must be registered explicitly:
| Node | Description | Header |
|---|---|---|
PopFromQueue<T> |
Pop front from ProtectedQueue | actions/pop_from_queue.hpp |
QueueSize<T> |
Get queue size | actions/pop_from_queue.hpp |
8. Blackboard and Port System
8.1 Blackboard
A key-value store shared by all nodes in a tree (and its subtrees).
// Direct access (not recommended)
config().blackboard->get("key", value);
config().blackboard->set("key", value);
8.2 Ports
Abstracted, type-safe layer over the blackboard. Ports are the recommended way to exchange data.
// Through ports (recommended)
auto val = getInput<int>("input_key"); // Returns Expected<T>
setOutput("output_key", result); // Returns Result
8.3 Port Types
| Port Type | Direction | Access |
|---|---|---|
InputPort<T> |
Read | getInput<T>(key) |
OutputPort<T> |
Write | setOutput(key, value) |
BidirectionalPort<T> |
Read/Write | getInput<T>(key) + setOutput(key, value) |
8.4 Port with Default Values
// Default value as string (parsed via convertFromString)
BT::InputPort<Point2D>("pointB", "3,4");
// Default value as same type
BT::InputPort<Point2D>("pointB", Point2D{3, 4});
// Default blackboard pointer (port name = blackboard key)
BT::InputPort<Point2D>("pointD", "{=}"); // equivalent to "{pointD}"
// Output port default pointing to blackboard entry
BT::OutputPort<Point2D>("result", "{target}");
8.5 Declaring Ports in a Node
class MyNode : public BT::SyncActionNode
{
public:
MyNode(const std::string& name, const BT::NodeConfig& config)
: BT::SyncActionNode(name, config) {}
static BT::PortsList providedPorts()
{
return {
BT::InputPort<std::string>("message"),
BT::OutputPort<int>("result"),
BT::BidirectionalPort<std::vector<int>>("data")
};
}
BT::NodeStatus tick() override
{
auto msg = getInput<std::string>("message");
setOutput("result", 42);
return BT::NodeStatus::SUCCESS;
}
};
8.6 Custom Types and convertFromString
struct Position2D { double x, y; };
// Template specialization (must be in namespace BT)
namespace BT {
template <> inline Position2D convertFromString(StringView str)
{
auto parts = splitString(str, ';');
if (parts.size() != 2)
throw RuntimeError("invalid input");
return { convertFromString<double>(parts[0]),
convertFromString<double>(parts[1]) };
}
}
The "json:" prefix triggers JSON parsing:
template <typename T>
inline T convertFromString(StringView str)
{
if (StartWith(str, "json:")) {
str.remove_prefix(5);
return convertFromJSON<T>(str);
}
// ... normal string conversion
}
8.7 Zero-Copy Access (getLockedPortContent)
Avoid copying large data with getLockedPortContent() (v4.5.1+, thread-safe):
if (auto any_locked = getLockedPortContent("cloud"))
{
if (any_locked->empty())
{
any_locked.assign(my_initial_pointcloud);
}
else if (Pointcloud* cloud_ptr = any_locked->castPtr<Pointcloud>())
{
// Modify in-place (mutex locked until any_locked goes out of scope)
}
}
8.8 Stamped Access (getInputStamped)
Access blackboard entry with timestamp/sequence info (v4+):
// Returns Expected<Timestamp> with sequence_id and stamp
auto result = getInputStamped<int>("sensor_value", destination);
// or
auto stamped = getInputStamped<int>("sensor_value");
// stamped->value, stamped->stamp.seq, stamped->stamp.time
8.9 Why Ports Instead of Direct Blackboard Access?
- Self-documenting:
providedPorts()is the node's data contract - Remappable: XML bindings can change without recompilation
- Type-safe: Runtime type checking on port connections
- Factory-aware: Factory can introspect port connections for static analysis
- Subtree-safe: Port remapping prevents naming collisions
8.10 Blackboard Public API Reference
#include <behaviortree_cpp/blackboard.h>
using Ptr = std::shared_ptr<Blackboard>;
Creation:
static Blackboard::Ptr create(Blackboard::Ptr parent = {});
Type-safe read/write:
template <typename T> bool get(const std::string& key, T& value) const; // Returns false if missing
template <typename T> T get(const std::string& key) const; // Throws if missing
template <typename T> void set(const std::string& key, const T& value);
void unset(const std::string& key); // Remove entry
[[deprecated]] void clear(); // Remove ALL entries (unsafe)
Stamped access (with timestamp/sequence_id):
template <typename T> Expected<Timestamp> getStamped(const std::string& key, T& value) const;
template <typename T> Expected<StampedValue<T>> getStamped(const std::string& key) const;
Low-level access:
std::shared_ptr<Entry> getEntry(const std::string& key); // Direct entry (has .value, .stamp, .sequence_id)
AnyPtrLocked getAnyLocked(const std::string& key); // Thread-safe raw Any pointer
[[deprecated]] Any* getAny(const std::string& key); // Use getAnyLocked instead
const TypeInfo* entryInfo(const std::string& key); // Type introspection
void createEntry(const std::string& key, const TypeInfo& info); // Pre-create entry without value
Introspection:
std::vector<StringView> getKeys() const; // List all keys in this blackboard
void debugMessage() const; // Print all entries to stdout
Subtree remapping (internal ↔ external port mapping):
void addSubtreeRemapping(StringView internal, StringView external);
Parent/root traversal:
Blackboard::Ptr parent(); // Parent blackboard (may be null)
Blackboard* rootBlackboard(); // Root of the hierarchy (for @ prefix)
const Blackboard* rootBlackboard() const;
Polymorphic cast support:
void setPolymorphicCastRegistry(std::shared_ptr<PolymorphicCastRegistry> registry);
const PolymorphicCastRegistry* polymorphicCastRegistry() const;
template <typename T> Expected<T> tryCastWithPolymorphicFallback(const Any* any) const;
Auto-remapping:
void enableAutoRemapping(bool remapping);
Entry struct fields:
struct Entry {
Any value; // Stored value
TypeInfo info; // Type information
StringConverter string_converter; // String parsing function
mutable std::mutex entry_mutex; // Thread-safety lock
uint64_t sequence_id; // Incremented on each write
std::chrono::nanoseconds stamp; // Timestamp of last write
};
8.11 Port Type System Details
Type Compatibility Rules (from gtest_port_type_rules.cpp):
- Same type always compatible (e.g.,
int↔int) AnyTypeAllowed/BT::Anyports accept any type (generic)std::stringoutput acts as "universal donor" — converts to target type viaconvertFromString<T>()- Writing a string to a non-string entry creates an
AnyTypeAllowedentry (not strongly typed) - Type locks after the first strongly-typed write: subsequent writes must match or be safely castable
- Safe numeric casting:
int64_t↔uint8_tetc. with bounds checking - A
BT::Anyentry bypasses type checking (accepts any subsequent type) - String-to-number auto-conversion: writing
"42"to anintentry works if aStringConverteris registered
Reserved Port Names:
| Name | Reserved As | Purpose |
|---|---|---|
name |
.xml attribute |
Node instance name |
ID |
.xml attribute |
Node registration type |
_autoremap |
SubTree attribute | Enables auto-remapping |
_failureIf, _successIf, _skipIf, _while |
Pre-condition | Script conditions |
_onHalted, _onFailure, _onSuccess, _post |
Post-condition | Script callbacks |
Additionally, "Root" and "root" are reserved and cannot be used as BehaviorTree ID.
Port names must start with an alphabetic character and must not contain: < > & " ' / \ : * ? | . or whitespace. Utility functions:
bool IsAllowedPortName(StringView str); // Full validation
char findForbiddenChar(StringView name); // Returns first forbidden char (or '\0')
bool IsReservedAttribute(StringView str); // Checks against reserved names
String conversion utilities (basic_types.h):
std::string toStr(NodeStatus s, bool colored = false); // Colored ANSI output available
std::string toStr(PortDirection d);
std::string toStr(NodeType t);
std::string toStr(bool b);
9. XML Format
9.1 Basic Structure
<root BTCPP_format="4" main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence>
<SaySomething message="Hello" />
</Sequence>
</BehaviorTree>
</root>
<root>should haveBTCPP_formatattribute (missing emits a warning; XSD requires it)<BehaviorTree>must haveIDattribute- Multiple
<BehaviorTree>definitions in one file - Maximum XML nesting depth: 256 levels
9.2 Compact vs Explicit Syntax
<!-- Compact (recommended) -->
<SaySomething name="hello" message="Hello World"/>
<!-- Explicit (required by Groot for full model) -->
<Action ID="SaySomething" name="hello" message="Hello"/>
9.3 Port Bindings
<!-- Static string value -->
<SaySomething message="Hello"/>
<!-- Blackboard binding -->
<SaySomething message="{my_message}"/>
<!-- Self-binding ({=} → {attr_name}, i.e., message → {message}) -->
<SaySomething message="{=}"/>
Port default values are specified in providedPorts() and used when the XML attribute is omitted. An empty attribute (message="") overrides the default and may cause a parse error if the type rejects empty strings.
Non-port underscore-prefixed attributes (e.g., _my_custom="value") are saved in TreeNode::config().other_attributes and ignored by port resolution. Attributes without _ prefix that don't match a registered port trigger a WrongPort warning.
9.4 Including External Files
<root BTCPP_format="4">
<include path="subtree_a.xml" />
<include path="subtree_b.xml" />
<BehaviorTree ID="MainTree">
<Sequence>
<SubTree ID="SubTreeA" />
<SubTree ID="SubTreeB" />
</Sequence>
</BehaviorTree>
</root>
ROS2 package paths (requires ament_index_cpp):
<include ros_pkg="my_package" path="trees/grasp.xml"/>
9.5 TreeNodeModel (for Groot)
<TreeNodesModel>
<Action ID="SaySomething">
<input_port name="message" type="std::string" />
</Action>
<Action ID="OpenGripper"/>
</TreeNodesModel>
Or generate automatically:
std::string xml_models = BT::writeTreeNodesModelXML(factory);
std::string xsd = BT::writeTreeXSD(factory); // XSD schema for validation
Serializing a tree back to XML (used internally by Groot2Publisher):
#include <behaviortree_cpp/xml_parsing.h>
std::string xml = BT::WriteTreeToXML(tree, true, true); // add metadata + builtin models
10. Scripting Language
BT.CPP 4.x introduces an XML-embedded scripting language for blackboard operations.
10.1 Assignment
<Script code="param_A := 42" /> <!-- Create if not exists -->
<Script code="param_B = 3.14" /> <!-- Update (error if missing) -->
<Script code="msg = 'hello world'" /> <!-- Strings -->
Multi-statement: A:=42; B:=24; C:=A+B. Semicolons separate statements; trailing and consecutive semicolons are allowed.
10.2 Arithmetic
+, -, *, / and assignment operators +=, -=, *=, /=
String concat: .. (any type: string+string, number+string, string+number). + also concatenates strings.
Note: += works for strings too (msg += 'world').
Number formats: integers (42), hex (0xFF), reals (3.14, 1e5).
10.3 Comparison and Logic
==, !=, <, <=, >, >=, &&, ||, !
Chained comparisons supported: 1 < x < 10 (equivalent to 1 < x && x < 10).
String-to-number comparison works: strings are converted to double for comparison (e.g., '3' == 3 is true).
Ternary: val := (A > 1) ? 42 : 24 (right-associative).
10.4 Bitwise (integers only)
&, |, ^, ~
Throws if either operand is not an integer.
10.5 Booleans
<Script code="val_A = true; val_B := !false" />
Stored as double (1.0/0.0).
10.6 Identifiers
Variable names start with a letter, _, or @. The @ prefix is used for root/nested blackboard references.
10.7 Operator Precedence (highest to lowest)
| Level | Operators | Assoc |
|---|---|---|
| Prefix | unary -, ~, ! |
— |
| 18 | *, / |
Left |
| 16 | +, -, .. |
Left |
| 14 | & |
Left |
| 12 | |, ^ |
Left |
| 10 | ==, !=, <, >, <=, >= |
Chain |
| 8 | && |
Left |
| 6 | || |
Left |
| 4 | ? : |
Right |
| 2 | :=, =, +=, -=, *=, /= |
— |
10.8 Multi-statement Return Value
A multi-statement script returns the value of the last expression. This is used in condition contexts (_skipIf, _successIf, ScriptCondition).
10.9 Enums
enum Color { RED=1, BLUE=2, GREEN=3 };
factory.registerScriptingEnums<Color>();
factory.registerScriptingEnum("THE_ANSWER", 42);
<Script code="color := RED; answer := THE_ANSWER" />
Default magic_enum range: [-128, 128]. Use manual registration for values outside this range.
10.10 Scripting Utility Functions
#include <behaviortree_cpp/scripting/script_parser.hpp>
// Validate script syntax without executing:
BT::Result res = BT::ValidateScript("x := 42"); // true if valid
bool is_valid = static_cast<bool>(res);
// Parse script into a callable function (for repeated execution):
auto func = BT::ParseScript("x := 42; y := x + 1");
BT::Ast::Environment env = { blackboard, enums_table };
BT::Any result = func(env); // evaluate, returns last expression value
// Parse and execute in one step:
auto result = BT::ParseScriptAndExecute(env, "x := 42; x + 1");
11. Pre/Post Conditions
Inline attributes (no C++ changes needed) to add conditions to any node.
11.1 Pre-conditions
| Attribute | Description | Evaluated |
|---|---|---|
_skipIf |
Skip if condition is true | When IDLE or SKIPPED |
_failureIf |
Return FAILURE if condition is true | When IDLE or SKIPPED |
_successIf |
Return SUCCESS if condition is true | When IDLE or SKIPPED |
_while |
Skip/halt if condition is false | Every tick (IDLE, SKIPPED, RUNNING) |
Evaluation order: _failureIf → _successIf → _skipIf → _while
Pre-conditions fire when status() is IDLE or SKIPPED (not re-evaluated while RUNNING, except _while). The _while condition is also checked while RUNNING: if it becomes false, the node is halted and returns SKIPPED.
<OpenDoor _skipIf="!door_closed" />
<MoveToGoal _failureIf="battery_level < 20" />
11.2 Post-conditions
| Attribute | Description |
|---|---|
_onSuccess |
Execute script on SUCCESS |
_onFailure |
Execute script on FAILURE |
_post |
Execute script on SUCCESS or FAILURE |
_onHalted |
Execute script when halted |
<MoveBase goal="{target}"
_onSuccess="result:=OK"
_onFailure="result:=ERROR"/>
Execution context:
_onSuccess,_onFailure,_postexecute incheckPostConditions(), called fromexecuteTick()when the tick completes with SUCCESS or FAILURE._onHaltedexecutes inhaltNode(), called when the node is halted externally — NOT fromcheckPostConditions().- Execution order within
checkPostConditions():_onSuccess/_onFailure(mutually exclusive, depending on status) →_post(always).
11.3 Inline vs Precondition Decorator
| Feature | Inline (_skipIf, etc.) |
<Precondition> Decorator |
|---|---|---|
| Evaluated | Once (IDLE → active) | Per-tick (with else="RUNNING") |
| Can halt running child | No | Yes |
| Use case | One-time checks | Continuous monitoring |
12. Asynchronous Actions
12.1 Concurrency vs Parallelism
BT.CPP is single-threaded. All tick() methods execute sequentially. Asynchronous nodes return RUNNING and are resumed in future ticks. Do not block in tick().
12.2 StatefulActionNode (Recommended)
Lifecycle: onStart() → onRunning()* → onHalted() (if interrupted)
class SleepNode : public BT::StatefulActionNode
{
public:
SleepNode(const std::string& name, const BT::NodeConfig& config)
: BT::StatefulActionNode(name, config) {}
static BT::PortsList providedPorts()
{
return { BT::InputPort<int>("msec") };
}
NodeStatus onStart() override
{
int msec = 0;
getInput("msec", msec);
if (msec <= 0)
return NodeStatus::SUCCESS;
deadline_ = std::chrono::system_clock::now() +
std::chrono::milliseconds(msec);
return NodeStatus::RUNNING;
}
NodeStatus onRunning() override
{
return (std::chrono::system_clock::now() >= deadline_)
? NodeStatus::SUCCESS
: NodeStatus::RUNNING;
}
void onHalted() override
{
std::cout << "SleepNode interrupted" << std::endl;
}
private:
std::chrono::system_clock::time_point deadline_;
};
12.3 CoroActionNode (Coroutine-based)
Alternative async pattern using setStatusRunningAndYield():
class MyCoroAction : public BT::CoroActionNode
{
public:
MyCoroAction(const std::string& name, const BT::NodeConfig& config)
: BT::CoroActionNode(name, config) {}
BT::NodeStatus tick() override
{
// Send request
sendRequest();
setStatusRunningAndYield(); // Pause, return RUNNING
// Resume in next tick: check response
if (responseReceived())
return BT::NodeStatus::SUCCESS;
setStatusRunningAndYield(); // Continue waiting
// ...
return BT::NodeStatus::SUCCESS;
}
void halt() override
{
std::cout << "Coroutine interrupted" << std::endl;
CoroActionNode::halt();
}
};
12.4 ThreadedAction (Use with Caution)
Runs tick() in a separate thread. Must check isHaltRequested() regularly.
// CORRECT: must check isHaltRequested() periodically
class ThreadedSleepNode : public BT::ThreadedAction
{
public:
ThreadedSleepNode(const std::string& name, const BT::NodeConfig& config)
: BT::ThreadedAction(name, config) {} // NOT ActionNodeBase
static BT::PortsList providedPorts()
{
return { BT::InputPort<int>("msec") };
}
NodeStatus tick() override
{
int msec = 0;
getInput("msec", msec);
const auto deadline = std::chrono::system_clock::now() +
std::chrono::milliseconds(msec);
while (!isHaltRequested() && std::chrono::system_clock::now() < deadline)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
return NodeStatus::SUCCESS;
}
};
Warning:
ThreadedActionis-aActionNodeBase, not a wrapper. A derived class constructor must callBT::ThreadedAction(name, config), notBT::ActionNodeBase(name, config). The former initializes theThreadedActionbase class; the latter skipsThreadedAction's own initialization and is a bug found in earlier guides.
12.5 Which Async Pattern to Use
| Pattern | Complexity | Use Case |
|---|---|---|
StatefulActionNode |
Low | Most async operations (recommended) |
CoroActionNode |
Medium | Stateful request/response with yield points |
ThreadedAction |
High | Truly parallel execution (avoid if possible) |
12.6 Action Node API Reference
All action classes are in include/behaviortree_cpp/action_node.h.
ActionNodeBase — base for all actions:
class ActionNodeBase : public LeafNode {
// type() returns NodeType::ACTION (override final)
};
SyncActionNode — synchronous (no RUNNING):
class SyncActionNode : public ActionNodeBase {
virtual NodeStatus executeTick() override; // Throws if derived returns RUNNING
virtual void halt() override final; // Calls resetStatus()
};
StatefulActionNode — async lifecycle (recommended):
class StatefulActionNode : public ActionNodeBase {
virtual NodeStatus onStart() = 0; // Called once when IDLE → RUNNING
virtual NodeStatus onRunning() = 0; // Called each tick while RUNNING
virtual void onHalted() = 0; // Called when halted
bool isHaltRequested() const; // Check if halt was requested
// tick() and halt() are override final (do not override)
};
ThreadedAction — true multi-threading:
class ThreadedAction : public ActionNodeBase {
virtual NodeStatus executeTick() override final; // Spawns std::async thread
virtual void halt() override; // Stops (waits for thread)
bool isHaltRequested() const; // Check periodically in tick()
};
Emits emitWakeUpSignal() on completion so the tree's tickOnce()/tickWhileRunning() can react immediately.
CoroActionNode — coroutine-based async:
class CoroActionNode : public ActionNodeBase {
void setStatusRunningAndYield(); // Pause and return RUNNING
virtual NodeStatus executeTick() override final;
void halt() override; // Call CoroActionNode::halt() if overriding
void tickImpl(); // Internal (but public)
};
SimpleActionNode — functor wrapper (used by registerSimpleAction):
using TickFunctor = std::function<NodeStatus(TreeNode&)>;
SimpleActionNode(const std::string& name, TickFunctor tick_functor,
const NodeConfig& config);
Note: tick() sets status to RUNNING before calling the functor (only when IDLE). The functor must return SUCCESS or FAILURE; RUNNING from a simple action is not supported.
13. Subtrees
13.1 Basic SubTree
<root BTCPP_format="4">
<BehaviorTree ID="MainTree">
<SubTree ID="GraspObject" />
</BehaviorTree>
<BehaviorTree ID="GraspObject">
<Sequence>
<OpenGripper />
<ApproachObject />
<CloseGripper />
</Sequence>
</BehaviorTree>
</root>
13.2 Port Remapping
<SubTree ID="MoveRobot"
target="{move_goal}"
frame="map"
result="{error_code}" />
13.3 Subtree Models (Default Remapping)
Define default remappings in <TreeNodesModel>:
<TreeNodesModel>
<SubTree ID="MoveRobot">
<input_port name="target" default="{move_goal}" />
<input_port name="frame" default="world" />
<output_port name="result" default="{error_code}" />
</SubTree>
</TreeNodesModel>
Then override selectively:
<SubTree ID="MoveRobot" frame="map" />
13.4 Autoremap
Automatically remap all ports with matching names:
<!-- Explicit remapping -->
<SubTree ID="MoveRobot" target="{target}" frame="{frame}" result="{result}" />
<!-- Autoremap (same effect) -->
<SubTree ID="MoveRobot" _autoremap="true" />
<!-- Autoremap + override specific -->
<SubTree ID="MoveRobot" _autoremap="true" frame="world" />
Entries starting with _ are treated as private and excluded from autoremap.
13.5 Debugging Subtrees
tree.subtrees[0]->blackboard->debugMessage();
13.6 Multiple XML Files
// Register all XMLs in a directory
for (auto const& entry : std::filesystem::directory_iterator("./"))
{
if (entry.path().extension() == ".xml")
factory.registerBehaviorTreeFromFile(entry.path().string());
}
// Then create specific trees by ID
auto tree = factory.createTree("MainTree");
14. Logging and Debugging
14.1 Logger Interface
All loggers inherit from StatusChangeLogger and receive callbacks on every node status transition.
virtual void callback(
BT::Duration timestamp, // When the transition happened
const TreeNode& node, // The node that changed status
NodeStatus prev_status, // Previous status
NodeStatus status); // New status
Base class API (loggers/abstract_logger.h):
void setEnabled(bool enabled); // Enable/disable logging
bool enabled() const;
void setTimestampType(TimestampType type); // absolute (default) or relative
void enableTransitionToIdle(bool enable); // Show/hide IDLE transitions (default: true)
bool showsTransitionToIdle() const;
14.2 Built-in Loggers
| Logger | Header | Description |
|---|---|---|
StdCoutLogger |
loggers/bt_cout_logger.h |
Prints to stdout |
FileLogger2 |
loggers/bt_file_logger_v2.h |
Binary file, requires .btlog extension (Groot2) |
SqliteLogger |
loggers/bt_sqlite_logger.h |
SQLite database, requires .db3/.btdb extension; supports append mode; setAdditionalCallback() to inject extra data per transition; execSqlStatement() for raw SQL |
MinitraceLogger |
loggers/bt_minitrace_logger.h |
Chrome tracing JSON |
Groot2Publisher |
loggers/groot2_publisher.h |
TCP to Groot2 via ZeroMQ (REP on server_port, PUB on server_port+1); default port 1667 |
TreeObserver |
loggers/bt_observer.h |
In-memory statistics collection |
auto tree = factory.createTreeFromText(xml);
// Console logger
BT::StdCoutLogger cout_logger(tree);
// File logger
BT::FileLogger2 file_logger(tree, "output.btlog");
// SQLite logger
BT::SqliteLogger sqlite_logger(tree, "output.db3");
tree.tickWhileRunning();
14.3 TreeObserver
Collects execution statistics for each node:
BT::TreeObserver observer(tree);
tree.tickWhileRunning();
// Access statistics for a specific node
const auto& stats = observer.getStatistics("last_action");
// stats.transitions_count, stats.success_count,
// stats.failure_count, stats.skip_count
NodeStatistics struct (from loggers/bt_observer.h:34-51):
struct NodeStatistics {
NodeStatus last_result = NodeStatus::IDLE; // Last SUCCESS or FAILURE
NodeStatus current_status = NodeStatus::IDLE; // Current status (any)
unsigned transitions_count = 0; // Status transition count
unsigned success_count = 0; // Times returned SUCCESS
unsigned failure_count = 0; // Times returned FAILURE
unsigned skip_count = 0; // Times returned SKIPPED
Duration last_timestamp = {}; // Last transition timestamp
};
Additional API:
void resetStatistics(); // Clear all collected statistics
const std::unordered_map<std::string, uint16_t>& pathToUID() const; // Full path → UID
const std::map<uint16_t, std::string>& uidToPath() const; // UID → full path (ordered)
14.4 Node Identification
Two mechanisms:
TreeNode::UID()— unique numeric ID (depth-first traversal order)TreeNode::fullPath()— human-readable unique path:subtree_name/node_name
14.5 Status Change Subscriptions
auto sub = node->subscribeToStatusChange(
[](TimePoint timestamp, const TreeNode& node,
NodeStatus prev_status, NodeStatus new_status) {
// React to status changes
});
14.6 Pre/Post Tick Callbacks
node->setPreTickFunction([](TreeNode& self) -> NodeStatus {
// Called before tick()
return NodeStatus::SUCCESS; // Return to skip actual tick
});
node->setPostTickFunction([](TreeNode& self, NodeStatus result) -> NodeStatus {
// Called after tick(), can modify the return status
return result;
});
15. Groot2 Integration
15.1 Basic Setup
#include <behaviortree_cpp/loggers/groot2_publisher.h>
auto tree = factory.createTreeFromText(xml);
BT::Groot2Publisher publisher(tree, 1667); // Default port (REP socket)
tree.tickWhileRunning();
Run Groot2, connect to localhost:1667. A second PUB port at 1668 (port+1) is used for real-time status streaming. Requires ZeroMQ.
15.2 JSON Export for Custom Types
#include "behaviortree_cpp/json_export.h"
struct Position2D { double x, y; };
BT_JSON_CONVERTER(Position2D, pos) {
add_field("x", &pos.x);
add_field("y", &pos.y);
}
// In main():
BT::RegisterJsonDefinition<Position2D>();
The macro auto-generates to_json/from_json functions. Exported JSON includes a "__type" field (set to BT::demangle(typeid(T))). std::vector<T> conversions are auto-registered.
Or manually:
void PositionToJson(nlohmann::json& dest, const Position2D& pos) {
dest["x"] = pos.x;
dest["y"] = pos.y;
}
BT::RegisterJsonDefinition<Position2D>(PositionToJson);
15.3 File Logging for Replay
BT::FileLogger2 logger(tree, "output.btlog");
// BT::SqliteLogger logger(tree, "output.db3");
tree.tickWhileRunning();
// Open .btlog or .db3 in Groot2 for replay
15.4 Tree Export to JSON
nlohmann::json json = BT::ExportTreeToJSON(tree);
BT::ImportTreeFromJSON(json, tree);
16. Substitution Rules and Mock Testing
16.1 C++ Substitution Rules
Replace nodes at construction time for testing:
// Replace a node with another registered node (by fullPath pattern)
factory.addSubstitutionRule("talk", "TestSaySomething");
// Wildcard matching
factory.addSubstitutionRule("mysub/action_*", "TestAction");
// Use TestNode with configuration
BT::TestNodeConfig test_config;
test_config.async_delay = std::chrono::milliseconds(2000);
test_config.post_script = "msg ='message SUBSTITUTED'";
factory.addSubstitutionRule("last_action", test_config);
16.2 JSON Substitution Rules
{
"TestNodeConfigs": {
"MyTest": {
"async_delay": 2000,
"return_status": "SUCCESS",
"post_script": "msg ='message SUBSTITUED'"
}
},
"SubstitutionRules": {
"mysub/action_*": "TestAction",
"talk": "TestSaySomething",
"last_action": "MyTest"
}
}
factory.loadSubstitutionRuleFromJSON(json_text);
16.3 TestNodeConfig Fields
| Field | Type | Default | Description |
|---|---|---|---|
return_status |
NodeStatus | SUCCESS | Status to return |
async_delay |
milliseconds | 0 | If >0, makes node async |
success_script |
string | "" | Script on SUCCESS |
failure_script |
string | "" | Script on FAILURE |
post_script |
string | "" | Script on completion |
complete_func |
function | nullptr | Custom completion logic |
17. Global Blackboard Pattern
Share data between the main loop and the tree using an external blackboard.
auto global_bb = BT::Blackboard::create();
auto maintree_bb = BT::Blackboard::create(global_bb); // global_bb is parent
auto tree = factory.createTree("MainTree", maintree_bb);
for (int i = 1; i <= 3; i++)
{
global_bb->set("value", i);
tree.tickOnce();
auto result = global_bb->get<int>("value_sqr");
}
Access global entries in XML with @ prefix:
<PrintNumber val="{@value}" />
<Script code="@value_sqr := @value * @value" />
No remapping needed in subtrees — @ always accesses the root blackboard.
18. Registration Methods
18.1 registerNodeType (Inheritance, Recommended)
class MyNode : public BT::SyncActionNode
{
public:
MyNode(const std::string& name, const BT::NodeConfig& config)
: BT::SyncActionNode(name, config) {}
static BT::PortsList providedPorts() { return { ... }; }
BT::NodeStatus tick() override { ... }
};
factory.registerNodeType<MyNode>("MyNode");
18.2 registerSimpleAction
factory.registerSimpleAction("MyAction",
[](BT::TreeNode& self) {
auto val = self.getInput<int>("value");
self.setOutput("result", val.value() * 2);
return BT::NodeStatus::SUCCESS;
},
{ BT::InputPort<int>("value"), BT::OutputPort<int>("result") }
);
18.3 registerSimpleCondition
factory.registerSimpleCondition("IsReady",
[](BT::TreeNode& self) {
return BT::NodeStatus::SUCCESS;
}
);
18.4 registerSimpleDecorator
factory.registerSimpleDecorator("MyDecorator",
[](BT::NodeStatus child_status, BT::TreeNode& self) {
return child_status; // Pass through or modify
},
{ BT::InputPort<int>("param") }
);
18.5 Constructor with Additional Arguments
class Action_A : public BT::SyncActionNode
{
public:
Action_A(const std::string& name, const BT::NodeConfig& config,
int arg_int, std::string arg_str)
: BT::SyncActionNode(name, config), _arg1(arg_int), _arg2(arg_str) {}
// ...
};
factory.registerNodeType<Action_A>("Action_A", 42, "hello world");
18.6 Post-Construction Initialization (Visitor)
class Action_B : public BT::SyncActionNode
{
public:
Action_B(const std::string& name, const BT::NodeConfig& config)
: BT::SyncActionNode(name, config) {}
void initialize(int arg_int, const std::string& arg_str) { ... }
};
factory.registerNodeType<Action_B>("Action_B");
auto tree = factory.createTreeFromText(xml);
tree.applyVisitor([](TreeNode* node) {
if (auto b = dynamic_cast<Action_B*>(node))
b->initialize(69, "interesting_value");
});
18.7 Plugin Registration
BTCPP_EXPORT void BT_RegisterNodesFromPlugin(BT::BehaviorTreeFactory& factory);
// In main:
factory.registerFromPlugin("path/to/plugin.so");
18.8 Unregistering Builders
factory.unregisterBuilder("MyNode"); // Cannot unregister built-in nodes
18.9 BehaviorTreeFactory Public API Reference
#include <behaviortree_cpp/bt_factory.h>
Builder registration (low-level):
void registerBuilder(const TreeNodeManifest& manifest, const NodeBuilder& builder);
template <typename T> void registerBuilder(const std::string& ID, const NodeBuilder& builder);
Where NodeBuilder is:
using NodeBuilder = std::function<std::unique_ptr<TreeNode>(
const std::string&, const NodeConfig&)>;
And TreeNodeManifest contains:
struct TreeNodeManifest {
NodeType type;
std::string registration_ID;
PortsList ports;
KeyValueVector metadata;
};
Plugin support (deprecated):
[[deprecated]] void registerFromROSPlugins();
Tree definition registration (parsing without instantiation):
void registerBehaviorTreeFromFile(const std::filesystem::path& filename);
void registerBehaviorTreeFromText(const std::string& xml_text);
std::vector<std::string> registeredBehaviorTrees() const;
void clearRegisteredBehaviorTrees();
Low-level node instantiation:
std::unique_ptr<TreeNode> instantiateTreeNode(const std::string& name,
const std::string& ID,
const NodeConfig& config) const;
Introspection:
const std::unordered_map<std::string, NodeBuilder>& builders() const;
const std::unordered_map<std::string, TreeNodeManifest>& manifests() const;
const std::set<std::string>& builtinNodes() const;
Substitution rule management:
void clearSubstitutionRules();
const std::unordered_map<std::string, SubstitutionRule>& substitutionRules() const;
SubstitutionRule is:
using SubstitutionRule = std::variant<std::string, TestNodeConfig,
std::shared_ptr<TestNodeConfig>>;
Scripting enum registration:
void registerScriptingEnum(StringView name, int value); // Single enum
template <typename EnumType>
void registerScriptingEnums(); // All values via magic_enum
Example:
registerScriptingEnum("THE_ANSWER", 42);
registerScriptingEnums<Color>();
// Then in XML: <Script code="my_color := Red" />
Polymorphic cast registry:
PolymorphicCastRegistry& polymorphicCastRegistry();
const PolymorphicCastRegistry& polymorphicCastRegistry() const;
std::shared_ptr<PolymorphicCastRegistry> polymorphicCastRegistryPtr() const;
The registry is shared with all trees created from this factory, allowing trees to outlive the factory while maintaining access to polymorphic cast relationships.
Tree creation alternatives:
Tree createTree(const std::string& tree_name,
Blackboard::Ptr blackboard = Blackboard::create());
Tree createTreeFromFile(const std::filesystem::path& file_path,
Blackboard::Ptr blackboard = Blackboard::create());
Tree createTreeFromText(const std::string& xml_text,
Blackboard::Ptr blackboard = Blackboard::create());
The Tree copies all needed registry data and holds a shared pointer to the PolymorphicCastRegistry. The factory can be safely destroyed after createTree() — the tree continues to work independently.
Extra-args registration overload (ports list provided explicitly, bypassing providedPorts()):
template <typename T, typename... ExtraArgs>
void registerNodeType(const std::string& ID, const PortsList& ports,
ExtraArgs... args);
19. Migration from v3.x to v4.x
19.1 Class Renaming
| v3.x | v4.x |
|---|---|
NodeConfiguration |
NodeConfig |
SequenceStar |
SequenceWithMemory |
AsyncActionNode |
ThreadedAction |
Optional<T> |
Expected<T> |
Temporary backward compatibility:
namespace BT {
using NodeConfiguration = NodeConfig;
using AsyncActionNode = ThreadedAction;
using Optional = Expected;
}
19.2 Removed Features
| v3.x Feature | v4.x Replacement |
|---|---|
UseBlockingAction |
ThreadedAction (manual) or StatefulActionNode (recommended) |
BlackboardCheckInt/Double/String |
ScriptCondition or _failureIf pre-condition |
SetBlackboard (for simple values) |
<Script code="key := value" /> |
SubTreePlus |
SubTree (new default) |
19.3 XML Changes
<!-- v3.x -->
<root>
<!-- v4.x -->
<root BTCPP_format="4">
A Python conversion script is available: convert_v3_to_v4.py (thanks to user SubaruArai).
19.4 SubTree Changes
<!-- v3.x -->
<SubTreePlus ID="MySubTree"/>
<!-- v4.x (SubTreePlus is now the default, renamed to SubTree) -->
<SubTree ID="MySubTree"/>
19.5 Script Replaces SetBlackboard + BlackboardCheck
<!-- v3.x -->
<SetBlackboard output_key="port_A" value="42" />
<BlackboardCheckInt value_A="{port_A}" value_B="{port_B}"
return_on_mismatch="FAILURE">
<MyAction/>
</BlackboardCheckInt>
<!-- v4.x -->
<Script code="port_A := 42; port_B := 69" />
<MyAction _failureIf="port_A != port_B" />
19.6 Execution Loop
// v3.x
while (status != NodeStatus::SUCCESS || status == NodeStatus::FAILURE) {
status = tree.tickRoot();
std::this_thread::sleep_for(sleep_ms);
}
// v4.x (recommended)
tree.tickWhileRunning(sleep_ms);
// v4.x alternative
while (!isStatusCompleted(status)) {
status = tree.tickOnce();
tree.sleep(sleep_ms);
}
19.7 SKIPPED Status
v4 adds SKIPPED status for PreConditions. Custom leaf nodes must NOT return it; control nodes must handle it (skip to next child).
19.8 AsyncSequence / AsyncFallback
New control nodes (v4+) to address the "atomic sequence" problem — they yield after each synchronous child, allowing reactive parents to intervene.
19.9 Support for Polymorphic Pointers
v4.5+ supports polymorphic downcasting via registerPolymorphicCast<Derived, Base>():
factory.registerPolymorphicCast<DerivedType, BaseType>();
This enables setOutput<BaseType> and getInput<std::shared_ptr<BaseType>> when the blackboard entry contains a std::shared_ptr<DerivedType>.
20. Advanced Features
20.1 Blackboard::cloneInto
Copy all entries from one blackboard to another:
src_bb->cloneInto(dst_bb);
20.2 Blackboard Backup/Restore
Save and restore entire tree state:
auto backup = BT::BlackboardBackup(tree);
// ... tick tree ...
BT::BlackboardRestore(backup, tree);
20.3 Polymorphic Cast Registry
Register polymorphic downcast rules:
factory.registerPolymorphicCast<Derived, Base>();
This enables getInput<std::shared_ptr<Base>>() even when the blackboard stores std::shared_ptr<Derived>.
20.4 Metadata
Add metadata to node manifests:
factory.addMetadataToManifest("MyNode", {{"version", "2.0"}, {"author", "me"}});
20.5 Exception Types
Exception hierarchy (from include/behaviortree_cpp/exceptions.h):
BehaviorTreeException : std::exception // Base, variadic StrCat constructor
├── LogicError // Code refactoring required (compile-time issues)
└── RuntimeError // Runtime data/condition errors
└── NodeExecutionError // tick() threw (includes node path + type)
NodeExecutionError wraps a TickBacktraceEntry with failedNode() and originalMessage():
catch(const BT::NodeExecutionError& ex) {
auto& node = ex.failedNode();
std::cerr << "Failed at " << node.node_path
<< " [" << node.registration_name << "]: "
<< ex.originalMessage();
}
20.6 PreTick / PostTick Functions
Attach callbacks that run before/after a node's tick:
node->setPreTickFunction([](TreeNode& self) -> NodeStatus {
// Called before tick(). Return non-IDLE to skip actual tick.
return {};
});
node->setPostTickFunction([](TreeNode& self, NodeStatus result) -> NodeStatus {
// Called after tick(). Can modify the return value.
return result;
});
20.6 Watchdog / Tick Monitor
Monitor total tick execution time:
node->setTickMonitorCallback(
[](TreeNode& node, NodeStatus status, std::chrono::microseconds duration) {
if (duration > std::chrono::milliseconds(100))
std::cerr << "Slow tick: " << node.name() << std::endl;
});
20.7 JSON Conversion
Export/import entire trees as JSON:
nlohmann::json json = BT::ExportTreeToJSON(tree);
BT::ImportTreeFromJSON(json, tree);
Also blackboard-specific:
nlohmann::json bb_json = BT::ExportBlackboardToJSON(blackboard);
BT::ImportBlackboardFromJSON(json, blackboard);
21. Complete Tutorial Examples
21.1 First Behavior Tree — Full Example
From tutorial_01_first_tree.md. Demonstrates three registration patterns: inheritance, free function, and class method.
XML (my_tree.xml):
<root BTCPP_format="4">
<BehaviorTree ID="MainTree">
<Sequence name="root_sequence">
<CheckBattery name="check_battery"/>
<OpenGripper name="open_gripper"/>
<ApproachObject name="approach_object"/>
<CloseGripper name="close_gripper"/>
</Sequence>
</BehaviorTree>
</root>
C++ (full program):
#include "behaviortree_cpp/bt_factory.h"
#include <iostream>
using namespace BT;
// Pattern 1: Inheritance — SyncActionNode
class ApproachObject : public SyncActionNode
{
public:
ApproachObject(const std::string& name, const NodeConfig& config)
: SyncActionNode(name, config) {}
NodeStatus tick() override
{
std::cout << "ApproachObject: " << name() << std::endl;
return NodeStatus::SUCCESS;
}
};
// Pattern 2: Free function → registerSimpleCondition
NodeStatus CheckBattery()
{
std::cout << "[ Battery: OK ]" << std::endl;
return NodeStatus::SUCCESS;
}
// Pattern 3: Class method → registerSimpleAction
class GripperInterface
{
public:
GripperInterface() : _open(true) {}
NodeStatus open() {
_open = true;
std::cout << "GripperInterface::open" << std::endl;
return NodeStatus::SUCCESS;
}
NodeStatus close() {
_open = false;
std::cout << "GripperInterface::close" << std::endl;
return NodeStatus::SUCCESS;
}
private:
bool _open;
};
int main()
{
BehaviorTreeFactory factory;
// 1. Inheritance — most common for complex nodes
factory.registerNodeType<ApproachObject>("ApproachObject");
// 2. Simple condition from a free function
factory.registerSimpleCondition("CheckBattery", CheckBattery);
// 3. Simple action wrapping class methods
GripperInterface gripper;
factory.registerSimpleAction("OpenGripper",
[&](TreeNode&) { return gripper.open(); });
factory.registerSimpleAction("CloseGripper",
[&](TreeNode&) { return gripper.close(); });
auto tree = factory.createTreeFromFile("./my_tree.xml");
tree.tickWhileRunning();
return 0;
}
/* Expected output:
[ Battery: OK ]
GripperInterface::open
ApproachObject: approach_object
GripperInterface::close
*/
21.2 Asynchronous StatefulActionNode — Full Example
From tutorial_04_sequence.md. Demonstrates async pattern with onStart/onRunning/onHalted.
class MoveBaseAction : public StatefulActionNode
{
public:
MoveBaseAction(const std::string& name, const NodeConfig& config)
: StatefulActionNode(name, config) {}
static PortsList providedPorts() {
return { InputPort<Pose2D>("goal") };
}
NodeStatus onStart() override {
if (!getInput("goal", goal_)) {
throw RuntimeError("Missing goal");
}
std::cout << "Moving to: " << goal_ << std::endl;
start_time_ = std::chrono::system_clock::now();
return NodeStatus::RUNNING;
}
NodeStatus onRunning() override {
auto elapsed = std::chrono::system_clock::now() - start_time_;
if (elapsed > std::chrono::seconds(2)) {
std::cout << "Goal reached!" << std::endl;
return NodeStatus::SUCCESS;
}
return NodeStatus::RUNNING;
}
void onHalted() override {
std::cout << "MoveTo halted, cleaning up" << std::endl;
}
private:
Pose2D goal_;
std::chrono::system_clock::time_point start_time_;
};
// Register and use:
// factory.registerNodeType<MoveBaseAction>("MoveBase");
21.3 SubTrees and CrossDoor — Full Example
From tutorial_05_subtrees.md. Demonstrates tree composition with multiple <BehaviorTree> definitions.
XML (crossdoor.xml):
<root BTCPP_format="4">
<BehaviorTree ID="MainTree">
<Sequence>
<Fallback>
<Inverter>
<IsDoorClosed/>
</Inverter>
<SubTree ID="DoorClosed"/>
</Fallback>
<PassThroughDoor/>
</Sequence>
</BehaviorTree>
<BehaviorTree ID="DoorClosed">
<Fallback>
<OpenDoor/>
<RetryUntilSuccessful num_attempts="5">
<PickLock/>
</RetryUntilSuccessful>
<SmashDoor/>
</Fallback>
</BehaviorTree>
</root>
C++:
class CrossDoor
{
public:
void registerNodes(BehaviorTreeFactory& factory);
NodeStatus isDoorClosed();
NodeStatus passThroughDoor();
NodeStatus pickLock();
NodeStatus openDoor();
NodeStatus smashDoor();
private:
bool _door_open = false;
bool _door_locked = true;
int _pick_attempts = 0;
};
void CrossDoor::registerNodes(BehaviorTreeFactory& factory)
{
factory.registerSimpleCondition(
"IsDoorClosed", [this](TreeNode&) { return isDoorClosed(); });
factory.registerSimpleAction(
"PassThroughDoor", [this](TreeNode&) { return passThroughDoor(); });
factory.registerSimpleAction(
"OpenDoor", [this](TreeNode&) { return openDoor(); });
factory.registerSimpleAction(
"PickLock", [this](TreeNode&) { return pickLock(); });
factory.registerSimpleCondition(
"SmashDoor", [this](TreeNode&) { return smashDoor(); });
}
NodeStatus CrossDoor::isDoorClosed() {
return _door_open ? NodeStatus::FAILURE : NodeStatus::SUCCESS;
}
NodeStatus CrossDoor::passThroughDoor() {
return _door_open ? NodeStatus::SUCCESS : NodeStatus::FAILURE;
}
NodeStatus CrossDoor::pickLock() {
_pick_attempts++;
if (_pick_attempts >= 3) {
_door_locked = false;
_door_open = true;
return NodeStatus::SUCCESS;
}
return NodeStatus::FAILURE;
}
NodeStatus CrossDoor::openDoor() {
if (!_door_locked) { _door_open = true; return NodeStatus::SUCCESS; }
return NodeStatus::FAILURE;
}
NodeStatus CrossDoor::smashDoor() {
_door_open = true; return NodeStatus::SUCCESS;
}
int main()
{
BehaviorTreeFactory factory;
CrossDoor cross_door;
cross_door.registerNodes(factory);
factory.registerBehaviorTreeFromFile("./crossdoor.xml");
auto tree = factory.createTree("MainTree");
printTreeRecursively(tree.rootNode());
tree.tickWhileRunning();
return 0;
}
21.4 TreeObserver — Full Example
From tutorial_10_observer.md. Demonstrates logging, statistics collection, and UID/fullPath mapping.
XML (demo_observer.xml):
<root BTCPP_format="4">
<BehaviorTree ID="MainTree">
<Sequence>
<Fallback>
<AlwaysFailure name="failing_action"/>
<SubTree ID="SubTreeA" name="mysub"/>
</Fallback>
<AlwaysSuccess name="last_action"/>
</Sequence>
</BehaviorTree>
<BehaviorTree ID="SubTreeA">
<Sequence>
<AlwaysSuccess name="action_subA"/>
</Sequence>
</BehaviorTree>
</root>
C++:
int main()
{
BehaviorTreeFactory factory;
factory.registerBehaviorTreeFromFile("./demo_observer.xml");
auto tree = factory.createTree("MainTree");
BT::printTreeRecursively(tree.rootNode());
// Observer collects per-node statistics
BT::TreeObserver observer(tree);
// Print UID ↔ fullPath mapping
std::map<uint16_t, std::string> ordered;
for (const auto& [name, uid] : observer.pathToUID()) {
ordered[uid] = name;
}
for (const auto& [uid, name] : ordered) {
std::cout << uid << " -> " << name << std::endl;
}
tree.tickWhileRunning();
// Query statistics by full path or UID
const auto& stats = observer.getStatistics("last_action");
std::cout << "last_action transitions: " << stats.transitions_count
<< " success: " << stats.success_count << std::endl;
return 0;
}
22. Best Practices
- Use ports instead of direct blackboard access — self-documenting, remappable, type-safe
- Prefer
StatefulActionNodeoverThreadedAction— simpler, safer - Never block in
tick()— useStatefulActionNodewithonStart/onRunning - Check
isHaltRequested()inThreadedAction— required for clean halting - Use
Scriptinstead ofSetBlackboard— cleaner syntax - Use
_failureIf/_skipIffor simple pre-conditions — no C++ changes needed - Use
<Precondition>withelse="RUNNING"for per-tick re-evaluation - Use
LoopNodeinstead of deprecatedConsumeQueue - Use
_autoremap="true"to reduce boilerplate in subtrees - Use
AsyncSequence/AsyncFallbackinsideReactiveSequence/ReactiveFallbackfor interruptible sequences - Use
convertFromStringtemplate specialization for custom types - Use
getLockedPortContent()for zero-copy access to large data - Use
TreeObserverfor unit testing — verify expected transitions - Use substitution rules for mock testing — replace nodes without recompilation
- Use
@prefix for global blackboard — simple cross-tree data sharing
Reference
- Official Documentation: https://www.behaviortree.dev/
- GitHub: https://github.com/BehaviorTree/BehaviorTree.CPP
- API Reference: https://behaviortree.github.io/BehaviorTree.CPP/
- Groot2: https://www.behaviortree.dev/groot/
- Version: 4.9.0 (CMake:
project(behaviortree_cpp VERSION 4.9.0)) - License: MIT
本文来自博客园,作者:Theseus‘Ship,转载请注明原文链接:https://www.cnblogs.com/yongchao/p/21515661

浙公网安备 33010602011771号