Interop with BehaviorTree.CPP
BehaviorTree.CPP (BT.CPP) is the most widely-used C++ behavior tree library in robotics and game development. This guide shows you how to seamlessly integrate the Behavior Tree Editor with BT.CPP runtimes, enabling visual design in the browser and execution on robots, simulators, or game engines.
Why integrate with BT.CPP?
- Visual design: Drag-and-drop interface instead of hand-writing XML
- Rapid iteration: Test tree logic in the browser before compiling C++ code
- Team collaboration: Designers and AI programmers can work in parallel
- Runtime debugging: Use the editor's WebSocket monitor with BT.CPP's tick system
- Cross-platform: Design on any OS, deploy to Linux robots, Windows games, or macOS simulators
Architecture overview
The integration follows a three-stage workflow:
- Design: Build trees visually in the Behavior Tree Editor
- Export: Generate BT.CPP-compatible XML
- Execute: Load XML into BT.CPP runtime and run
┌─────────────────────┐ Export BTCPP XML ┌──────────────────┐
│ Browser Editor │ ───────────────────────► │ C++ Application │
│ (Visual Design) │ │ (BT.CPP Runtime)│
└─────────────────────┘ └──────────────────┘
▲ │
│ WebSocket / NDJSON Logs │
└────────────────────────────────────────────────┘
Port mapping between systems
The editor and BT.CPP use different terminology for node inputs and outputs. Understanding the mapping is crucial for successful integration.
Editor port system
The editor uses a unified port model where each port has:
- Name: Identifier (e.g.,
duration) - Type: Data type (string, number, boolean)
- Value: Default or current value
- Direction: Implicit (input by default)
BT.CPP port system
BT.CPP distinguishes between input and output ports explicitly:
- InputPort<T>: Read-only parameter passed to the node
- OutputPort<T>: Write-only result produced by the node
- BidirectionalPort<T>: Both read and write (rarely used)
Mapping table
| Editor concept | BT.CPP equivalent | Example |
|---|---|---|
| Port with value | InputPort | <Action duration="2.0"/> |
| Port without value | InputPort with default | <Action/> uses default |
| Custom node with outputs | OutputPort | Node writes result to blackboard |
| Blackboard key | {key} syntax | <Action target="{goal}"/> |
Registering custom node types
For BT.CPP to recognize your custom nodes, you must register them in both the editor and the C++ runtime.
Step 1: Define in the editor
Create a custom node JSON library (see Define custom node types):
{
"nodes": [
{
"name": "NavigateToGoal",
"type": "action",
"ports": [
{
"name": "goal_x",
"type": "number",
"value": 0.0
},
{
"name": "goal_y",
"type": "number",
"value": 0.0
}
]
}
]
}
Import this library via the Import Nodes button in the editor toolbar.
Step 2: Implement in C++
#include <behaviortree_cpp/bt_factory.h>
using namespace BT;
class NavigateToGoal : public SyncActionNode
{
public:
NavigateToGoal(const std::string& name,
const NodeConfiguration& config)
: SyncActionNode(name, config)
{}
static PortsList providedPorts()
{
return {
InputPort<double>("goal_x", 0.0, "Target X coordinate"),
InputPort<double>("goal_y", 0.0, "Target Y coordinate")
};
}
NodeStatus tick() override
{
double goal_x = 0.0;
double goal_y = 0.0;
if (!getInput("goal_x", goal_x)) {
throw RuntimeError("Missing required input [goal_x]");
}
if (!getInput("goal_y", goal_y)) {
throw RuntimeError("Missing required input [goal_y]");
}
// Execute navigation logic
bool success = robot_navigate(goal_x, goal_y);
return success ? NodeStatus::SUCCESS : NodeStatus::FAILURE;
}
};
Step 3: Register with BT factory
int main()
{
BehaviorTreeFactory factory;
// Register custom node
factory.registerNodeType<NavigateToGoal>("NavigateToGoal");
// Load tree from XML file exported from editor
auto tree = factory.createTreeFromFile("behavior_tree.xml");
// Execute
tree.tickWhileRunning();
return 0;
}
XML format compatibility
Editor export format
When you export as "BehaviorTree.CPP XML", the editor generates:
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="PatrolAndCharge">
<Condition ID="IsBatteryLow" threshold="20.0" />
<Action ID="NavigateToGoal" goal_x="5.0" goal_y="3.0" />
<Action ID="Wait" duration="5.0" />
</Sequence>
</BehaviorTree>
<TreeNodesModel>
<Condition ID="IsBatteryLow">
<input_port name="threshold">Battery level threshold</input_port>
</Condition>
<Action ID="NavigateToGoal">
<input_port name="goal_x">Target X coordinate</input_port>
<input_port name="goal_y">Target Y coordinate</input_port>
</Action>
<Action ID="Wait">
<input_port name="duration">Duration in seconds</input_port>
</Action>
</TreeNodesModel>
</root>
Key elements
-
<root>: Root element withmain_tree_to_executeattribute -
<BehaviorTree>: Contains the tree structure -
<TreeNodesModel>: Declares all custom node types and their ports - Port values appear as attributes on node elements
Loading in BT.CPP
// Method 1: From file
auto tree = factory.createTreeFromFile("tree.xml");
// Method 2: From string
std::string xml_text = load_file_content("tree.xml");
auto tree = factory.createTreeFromText(xml_text);
// Method 3: With custom blackboard
auto blackboard = BT::Blackboard::create();
blackboard->set("goal_x", 10.0);
blackboard->set("goal_y", 5.0);
auto tree = factory.createTreeFromFile("tree.xml", blackboard);
Blackboard integration
BT.CPP's blackboard is a shared key-value store that nodes use to communicate. The editor supports blackboard keys via special syntax.
Using blackboard keys in the editor
In port values, wrap variable names in curly braces:
{goal_x}— reads from blackboard key "goal_x"{battery_level}— reads battery status{result}— writes output to "result" key
Generated XML
<Action ID="NavigateToGoal" goal_x="{goal_x}" goal_y="{goal_y}" />
C++ side setup
auto blackboard = BT::Blackboard::create();
blackboard->set<double>("goal_x", 10.0);
blackboard->set<double>("goal_y", 5.0);
auto tree = factory.createTreeFromFile("tree.xml", blackboard);
tree.tickWhileRunning();
Round-trip workflow
Workflow 1: Editor → BT.CPP
- Design tree in the Behavior Tree Editor
- Add custom nodes and set port values
- Export as "BehaviorTree.CPP XML"
- Copy XML file to your C++ project
- Load with
factory.createTreeFromFile() - Compile and run
Workflow 2: BT.CPP → Editor (for debugging)
- Instrument your BT.CPP code to emit NDJSON events
- Run your application
- Connect the editor via WebSocket (see WebSocket monitor)
- Watch real-time execution in the browser
- Identify issues and iterate on tree design
Workflow 3: Hybrid development
- Start with existing BT.CPP XML files
- Import into the editor for visualization
- Make visual edits and rearrange nodes
- Export back to BT.CPP XML
- Test in C++ runtime
- Repeat until satisfied
Known limitations and workarounds
Limitation 1: Subtrees
Issue: BT.CPP supports subtrees (reusable tree fragments), but the editor treats all nodes as part of a single tree.
Workaround: Design subtrees as separate trees in the
editor, export them individually, and manually compose them in your
C++ code using <SubTree> nodes.
Limitation 2: Custom C++ types
Issue: The editor only supports string, number, and boolean port types. BT.CPP supports arbitrary C++ types (vectors, matrices, custom structs).
Workaround: Use blackboard keys for complex types.
Pass simple parameters through ports, and reference complex data via
blackboard: {robot_state}, {sensor_data}.
Limitation 3: Decorator semantics
Issue: Some BT.CPP decorators have C++-specific behavior (e.g., timeout based on std::chrono) that can't be fully represented in the editor.
Workaround: Use generic decorator nodes in the editor, then replace them with specialized C++ implementations after export. Document these substitutions clearly.
Limitation 4: Parallel node synchronization
Issue: BT.CPP's Parallel node has advanced synchronization modes (sync_children, threshold) not exposed in the editor UI.
Workaround: Add comments in port values to document
intended behavior, e.g., threshold="2 (success_threshold)".
Manually adjust the XML after export if needed.
Best practices
1. Version control your trees
Store both the editor JSON (for visual editing) and BT.CPP XML (for deployment) in Git. This gives you:
- Visual diffs when reviewing changes
- Easy rollback if a tree breaks
- Branch-based experimentation
2. Use consistent naming conventions
# Good
NavigateToChargingStation
CheckBatteryLevel
EmergencyStop
# Avoid
node1
action_2
condition_final
3. Document custom nodes thoroughly
Include docstrings in both the editor JSON library and C++ implementation. This helps team members understand node purpose and port semantics.
4. Test round-trips regularly
Periodically verify that:
- Editor → XML → BT.CPP loading works without errors
- All port values are preserved correctly
- Custom nodes are registered properly
5. Use the WebSocket monitor for debugging
Instrument your BT.CPP runtime to emit WebSocket events. This lets you:
- See which nodes are executing in real-time
- Identify bottlenecks or infinite loops
- Share live demos with remote teammates
Example: Complete integration
Here's a minimal end-to-end example for a mobile robot patrol task:
Step 1: Design in editor
Create a tree with: Sequence → IsBatteryLow → NavigateToGoal → Wait
Step 2: Export as BT.CPP XML
Click Export → select "BehaviorTree.CPP XML" → save as
patrol_tree.xml
Step 3: C++ implementation
#include <behaviortree_cpp/bt_factory.h>
#include <iostream>
using namespace BT;
// Custom action: Navigate to goal coordinates
class NavigateToGoal : public SyncActionNode
{
public:
NavigateToGoal(const std::string& name,
const NodeConfiguration& config)
: SyncActionNode(name, config)
{}
static PortsList providedPorts()
{
return {
InputPort<double>("goal_x", "Target X"),
InputPort<double>("goal_y", "Target Y")
};
}
NodeStatus tick() override
{
double x = 0, y = 0;
getInput("goal_x", x);
getInput("goal_y", y);
std::cout << "Navigating to (" << x << ", " << y << ")" << std::endl;
// Simulate navigation
std::this_thread::sleep_for(std::chrono::seconds(2));
return NodeStatus::SUCCESS;
}
};
// Custom condition: Check battery level
class IsBatteryLow : public ConditionNode
{
public:
IsBatteryLow(const std::string& name,
const NodeConfiguration& config)
: ConditionNode(name, config)
{}
static PortsList providedPorts()
{
return {
InputPort<double>("threshold", "Battery threshold")
};
}
NodeStatus tick() override
{
double threshold = 20.0;
getInput("threshold", threshold);
double current_battery = get_current_battery_level();
return (current_battery < threshold) ?
NodeStatus::SUCCESS : NodeStatus::FAILURE;
}
};
int main()
{
BehaviorTreeFactory factory;
// Register custom nodes
factory.registerNodeType<NavigateToGoal>("NavigateToGoal");
factory.registerNodeType<IsBatteryLow>("IsBatteryLow");
// Load tree
auto tree = factory.createTreeFromFile("patrol_tree.xml");
// Execute
std::cout << "Starting patrol behavior..." << std::endl;
tree.tickWhileRunning();
std::cout << "Patrol complete." << std::endl;
return 0;
}
Step 4: Run and monitor
# Compile
g++ -std=c++17 main.cpp -lbehaviortree_cpp -o patrol_bot
# Run
./patrol_bot
# Output:
# Starting patrol behavior...
# Navigating to (5.0, 3.0)
# Patrol complete.
Related resources
- Import & export formats — detailed format specifications
- WebSocket live monitor — real-time debugging integration
- Behavior trees in robotics — domain-specific patterns
- BT.CPP official documentation