Behavior Trees for Robotic Applications (ROS2) in C++

Markus Buchholz
8 min readJan 20, 2023

--

The following article provides you with general intuition over the Behaviour Trees which are often used in robotic applications or frameworks: ROS, Moveit, and NAV2. Understanding the principles of the Behavior Tress (BT) framework gives you an excellent opportunity to apply knowledge in the gaming domain. The BT can be integrated with Unity or Unreal.

Since BT is the C++ library that provides a framework to create BT (in C++), the article focuses on an overview of BT. Your interest in this area can be expanded by the online course, and hands-on course, which I highly recommend. If you are interested more about learning ROS, robotics, AI, ML, software, and many more please take a look at The ConstructSim website and be inspired by what you can learn. They have also a perfectly done Robotics Developer Master Class which approaches you to land in the dream company.

In addition, to the online course please download the free book and be inspired by this YouTube channel.

Here you will find also the brilliant introduction to BT link to the online environment where you can run your mobile robot.

Behavior Trees. Software Architecture

The BT framework is the C++ library that enables you to architect the BT in your robotics or gaming application. BT provides abstract methods (built in C++) to secure logical relationships within your application. For instance, if you develop a game, the behavior tree can be related to the performance of the game or define how the game characters behave in certain game situations, etc. In this regard, we state that the BT framework offers the tools to enable logical/abstract relationships between C++ classes, whose methods are called in accordance with the logical tree (BT) structure of your program.

The BT can be associated with State Machine (SM) however there are certain differences in which BT outperforms the SM.
The main problem with SM is transitions between states which increase while the number of states grows. States in SM are tightly connected and there is often a problem when there is a necessity for reuse. It is true that SM can be handled with ease by a designer (SW engineer), but a person will undoubtedly find it difficult to comprehend and forecast the system’s overall behavior. Most of these issues are resolved by Behavior Trees, which enhance the modular design and can be reused. Behavior Trees are by nature hierarchical.

The following figure provides you with a general overview of BT. As you can see the BT framework can be run independently, without ROS (as I mention while you design a game). Later I will give you an example of how to build a simple BT and run your first program. Please note the tutorials on the official site are outstanding and should be studied accordingly to exhaust all the remaining issues.

General overview of SW architecture (by author)

We can imagine your robot performing the task. In order to guarantee the task performance by a robot (for example robot cleaner) the “main” task is divided into several subtasks, like path planning, perception, orientation in the space, and power management, etc (in the ROS all these subtasks can be distributed across the nodes). All these subtasks can be considered low-level tasks, which are not architected by BT.
BT can be considered as the logical flow of your application. One task or action can depend on one or several previous actions, and at the same time, different conditions have to be true/false in order to affect the robot/application action.

With regard to robotics specifical, the BT is the sort of abstraction that shifts from low-level task control (path planner, power management, etc) to higher—level behaviors.

When we design a robot system, we still have to consider how the fundamental components (low-level actions/tasks) work and look for optimization (eg. boost the performance of the path algorithm by adding heuristics).

However, in order to build the architecture of the whole robot application, we need to move to a higher level of programming abstraction, meaning evaluating the task like path planner or power management as a simple component.

We use the idea of a high level of abstraction therefore it seems to be reasonable to elaborate on the location of this abstraction. The following figure gives you an overview of a stack of abstractions.

Abstraction stack. (by author)

The depicted stack can be considered as follows. The engineer/designer takes the requirements for the robot application and architects the logical flow of the application (create the behavior trees).
The structure of BT (logical flow) mimics the behavior of the robot (figure in the game). We can say, the designer logically connects the robot primitives to reflect the specification for the robot application.
The BT is modeled as an XML format file. The C++ framework (BehaviourTree.CPP) allows the construction of BT in your application. The definition of the robot tasks (program function in C++) completes the following stack.

While discussing the concept of BT, I used the logical flow formulation. In the context of software design, we can evaluate the discussed concept as a logical connection between certain software modules (nodes). The logical connection can be described as the usage of a certain logic operator like AND, OR, or NOT wrapped into the BT framework.

The beauty of the BT framework is the fact that designers receive a full implementation of all the mechanisms and other components to architect complex logical structures. Moreover, the logical flow can be designed as a multithreading application without unnecessary effort. All C++ mechanisms to support threading are incorporated into the framework.

Logical Primitives

Since the idea of the article is the only introduction to the BT I will focus on the basic logical components, which I will use later in the practical C++ example. The ROS2 (Galactic) example you will find here or in the course.

In simple principle (only for the purpose of this article), we can distinguish two logical nodes (BT mechanisms). The fallback realizes the logical OR and sequence node, which realizes the AND logic.

Both can be depicted as follows,

The fallback node (OR operator). (by author)
The sequence node (AND operator). (by author)

The architecture of the BT is specified in XML file which is read by the application. BehaviourTree.CPP framework manages the logical flow (tick the BT node) according to XML specification (we say invoke a callback). Nodes (here tasks) based on the status send the return signal to the “root” — the main node of the application. The signal can be SUCCESS, FAILURE, or RUNNING.

The following example combines the above mechanism and displays how the mentioned signals are distributed across the BT.

BT in action. (by author)

We can describe the above BT as follows,

Deriving this BT philosophy we can describe how following BT works by considering the following example.

1. presented BT consists of 4 tasks, falling to fallback, and sequence nodes.

2. the root that sends the tick to that fallback. We follow the left side. Fallback sends a tick to task 1 which is a failure (of course this is only the simulation ). Normally the decision SUCCESS or FAILURE is taken by the running program. In our case callback returns Failure.
Please note, now we run the Fallback node (OR) which needs at least one SUCCESS to return SUCCESS therefore all the nodes can be checked. For the sequence node (AND) the first FAILURE terminates “node investigation”.

3. Similar to task 1, the task 2 callback returns Failure. Since we still ran the fallback mechanism (OR) we continue.

4. Now the tick is sent to the Sequence node (AND). Sequence sends tick to task 3 and receives SUCCESS but the Sequence is logical AND operation so so we continue to check if all are SUCCESS.
Task 4 is SUCCESS therefore the root receives also SUCCESS.

The XML describing the logic depicted above BT can be formulated:

 <root main_tree_to_execute = "MainTree" >

<BehaviorTree ID="MainTree">
<Fallback name="root">
<Task1 name="task_1"/>
<Task2 name="task_2"/>
<Sequence>
<Task3 name="task_3"/>
<Task4 name="task_4"/>
</Sequence>
</Fallback>
</BehaviorTree>

</root>
)";

Below you will find the code which you can run in the ROSject. (ROS2 Galactic)

#include "behaviortree_cpp_v3/bt_factory.h"

using namespace BT;

class Task1 : public BT::SyncActionNode
{
public:
Task1(const std::string& name) : BT::SyncActionNode(name, {})
{
}

// You must override the virtual function tick()
NodeStatus tick() override
{
std::cout << "Task1: " << this->name() << std::endl;
return BT::NodeStatus::FAILURE;
}
};

class Task2 : public BT::SyncActionNode
{
public:
Task2(const std::string& name) : BT::SyncActionNode(name, {})
{
}

// You must override the virtual function tick()
NodeStatus tick() override
{
std::cout << "Task2: " << this->name() << std::endl;
return BT::NodeStatus::FAILURE;
}
};

class Task3 : public BT::SyncActionNode
{
public:
Task3(const std::string& name) : BT::SyncActionNode(name, {})
{
}

// You must override the virtual function tick()
NodeStatus tick() override
{
std::cout << "Task3: " << this->name() << std::endl;
return BT::NodeStatus::SUCCESS;
}
};

class Task4 : public BT::SyncActionNode
{
public:
Task4(const std::string& name) : BT::SyncActionNode(name, {})
{
}

// You must override the virtual function tick()
NodeStatus tick() override
{
std::cout << "Task4: " << this->name() << std::endl;
return BT::NodeStatus::SUCCESS;
}
};

static const char* xml_text_medium = R"(

<root main_tree_to_execute = "MainTree" >

<BehaviorTree ID="MainTree">
<Fallback name="root">
<Task1 name="task_1"/>
<Task2 name="task_2"/>
<Sequence>
<Task3 name="task_3"/>
<Task4 name="task_4"/>
</Sequence>
</Fallback>
</BehaviorTree>

</root>
)";

int main()
{
BehaviorTreeFactory factory;

factory.registerNodeType<Task1>("Task1");
factory.registerNodeType<Task2>("Task2");
factory.registerNodeType<Task3>("Task3");
factory.registerNodeType<Task4>("Task4");

std::cout << "\n------------ BUILDING A NEW TREE ------------" << std::endl;

auto tree = factory.createTreeFromText(xml_text_medium);

tree.tickRoot();

std::cout << std::endl;

// }

return 0;
}

In the ROSject (on a Linux terminal) perform the following actions,

cd /ros2_ws/src/bt_course_files/BehaviorTree.CPP/course_bt

touch bt_medium.cpp

// go to VSC to the same folder where you created bt_medium.cpp

// paste above code

// in the same folder modify the CMakeList.txt by adding:
// CompileExample("bt_medium")

cd ros2_ws/src/bt_course_files/BehaviorTree.CPP/build
cmake ..
make

// after building is finish

cd course_bt
./bt_medium

Expected output

Thank you for reading.

--

--