Interop with BehaviorTree.CPP

Why integrate with BT.CPP?

Architecture overview

The integration follows a three-stage workflow:

  1. Design: Build trees visually in the Behavior Tree Editor
  2. Export: Generate BT.CPP-compatible XML
  3. 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:

BT.CPP port system

BT.CPP distinguishes between input and output ports explicitly:

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

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:

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

  1. Design tree in the Behavior Tree Editor
  2. Add custom nodes and set port values
  3. Export as "BehaviorTree.CPP XML"
  4. Copy XML file to your C++ project
  5. Load with factory.createTreeFromFile()
  6. Compile and run

Workflow 2: BT.CPP → Editor (for debugging)

  1. Instrument your BT.CPP code to emit NDJSON events
  2. Run your application
  3. Connect the editor via WebSocket (see WebSocket monitor)
  4. Watch real-time execution in the browser
  5. Identify issues and iterate on tree design

Workflow 3: Hybrid development

  1. Start with existing BT.CPP XML files
  2. Import into the editor for visualization
  3. Make visual edits and rearrange nodes
  4. Export back to BT.CPP XML
  5. Test in C++ runtime
  6. 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:

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:

5. Use the WebSocket monitor for debugging

Instrument your BT.CPP runtime to emit WebSocket events. This lets you:

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

← Reactivity patterns Robotics patterns →