Learning BehaviourTree.CPP#
1. Install BehaviorTree.CPP#
1.1 Install from source#
1.2 Install Groot2#
NOTICE#
There are several differences between the official source and that in this repository. I modify the reactive_fallback.cpp(342ce3029e37c8836c0a53ca3e52a975de72cc79). See more details in the commit history.
This documentation is a digest of the official documentation. For more details, please refer to the official documentation.
The official documentation recommends way of integrate BehaviorTree.CPP with ROS2 is to use the server-client model. It is good, fine. However, as for the system I am working on, the only task of the decision module is to move the robot to a specific position, so I directly use seperate ros nodes to control the robot.
2. A INTRO#
Basic Idea#
Unlike a Finite State Machine, a behavior Tree is a tree of hierarchical nodes that controls the flow of execution of “tasks”.
Everytime the tree is ticked, the tree will be traversed from the root node to the leaf node, and the leaf node will return a status code to the parent node, and the parent node will return a status code to the grandparent node, and so on, until the root node is reached.
Though it is easy to understand, there are still some mistakes that are easy to make. And the only way to avoid these mistakes is to make mistakes and learn from them, at least for me.
3. Learning From a Detailed Example#
Tree Structure#
The tree structure is defined in a xml file. A recommended way to create a tree is to use the Groot2, a graphical editor for BehaviorTree.CPP.
Tree Structure in xml file
<?xml version="1.0" encoding="UTF-8"?>
<root BTCPP_format="4">
<BehaviorTree ID="RMUL">
<ReactiveSequence>
<RunOnce then_skip="true">
<Script code="spin:=false;human_command:=false"/>
</RunOnce>
<Topics2Blackboard base_front_remain="{base_front_remain}"
base_shield_enable="{base_shield_enable}"
remain_hp="{remain_hp}"
bullet_remaining_num_17mm="{bullet_remaining_num_17mm}"
game_progress="{game_progress}"
stage_remain_time="{stage_remain_time}"
tracking="{tracking}"
target_armor_id="{target_armor_id}"
target_position="{target_position}"
my_outpost_hp="{my_outpost_hp}"
my_base_hp="{my_base_hp}"
enemy_outpost_hp="{enemy_outpost_hp}"
enemy_base_hp="{enemy_base_hp}"/>
<HumanCommand human_command="{human_command}"
human_command_type="{command_type}"
twist="{twist}"
goal_point="{setted_point}"
odom="{relocalization}"/>
<Spin spin="{spin}"/>
<ReactiveFallback _while="game_progress==4">
<Nav2Pose name="nav_to_human_set_point"
goal="{setted_point}"
_skipIf="!human_command"/>
<SequenceWithMemory name="GO HOME"
_while="my_outpost_hp<200"
_post="spin:=false">
<Timeout msec="3000">
<Nav2Pose name="get_supply"
goal="1.0,0.4,0.0,0.0,0.0,0.0,0.0"/>
</Timeout>
<KeepRunningUntilFailure>
<Script code="spin:=true"/>
</KeepRunningUntilFailure>
</SequenceWithMemory>
<SequenceWithMemory name="GET SUPPLY"
_while="bullet_remaining_num_17mm<=10">
<Nav2Pose name="get_supply"
goal="0.1,0.1,0.0,0.0,0.0,0.0,0.0"/>
<KeepRunningUntilFailure>
<Script code="spin:=false"/>
</KeepRunningUntilFailure>
</SequenceWithMemory>
<ReactiveSequence name="ATTCK!"
_while="tracking==true"
_onHalted="spin:=false">
<Attack name="Calculate Attack Pose"
target_position="{target_position}"
attack_pose="{attack_pose}"
_onSuccess="spin:=true"/>
<KeepRunningUntilFailure>
<Nav2Pose goal="{attack_pose}"/>
</KeepRunningUntilFailure>
</ReactiveSequence>
<RetryUntilSuccessful num_attempts="1000">
<Sequence>
<Nav2Pose goal="1.96,-2.12,0.0,0.0,0.0,0.0,0.0"/>
<Nav2Pose goal="1.05,1.25,0.0,0.0,0.0,0.0,0.0"/>
<Nav2Pose goal="2.44,0.68,0.0,0.0,0.0,0.0,0.0"/>
<AlwaysFailure/>
</Sequence>
</RetryUntilSuccessful>
</ReactiveFallback>
</ReactiveSequence>
</BehaviorTree>
<!-- Description of Node Models (used by Groot) -->
<TreeNodesModel>
<Action ID="Attack"
editable="true">
<input_port name="target_position"/>
<output_port name="attack_pose"/>
</Action>
<Action ID="HumanCommand"
editable="true">
<output_port name="human_command"/>
<output_port name="human_command_type"/>
<output_port name="twist"/>
<output_port name="goal_point"/>
<output_port name="odom"/>
</Action>
<Action ID="Nav2Pose"
editable="true">
<input_port name="goal"/>
</Action>
<Action ID="Spin"
editable="true">
<input_port name="spin"/>
</Action>
<Action ID="Topics2Blackboard"
editable="true">
<output_port name="base_front_remain"/>
<output_port name="base_shield_enable"/>
<output_port name="remain_hp"/>
<output_port name="bullet_remaining_num_17mm"/>
<output_port name="game_progress"/>
<output_port name="stage_remain_time"/>
<output_port name="tracking"/>
<output_port name="target_armor_id"/>
<output_port name="target_position"/>
<output_port name="my_outpost_hp"/>
<output_port name="my_base_hp"/>
<output_port name="enemy_outpost_hp"/>
<output_port name="enemy_base_hp"/>
</Action>
</TreeNodesModel>
</root>
It is east to grasp the intention of the tree. But it readlly take some time to make it right.
Control Node#
Control node manage the logic of the tree. There are three basic types of control node: sequence, fallback(selector), and parallel. Derived from these basic types, there are several other types of control node, such as reactive sequence, reactive fallback, and so on.
The best way to understand them is too see what the node will do when the child node returns RUNNING or FAILURE, and also see what scenario a spcific type of control node should be used.
| Type of ControlNode | Child returns RUNNING | Child returns FAILURE |
|---|---|---|
| Fallback | Tick again | Tick next |
| ReactiveFallback | Restart | Tick next |
| Sequence | Tick again | Restart |
| ReactiveSequence | Restart | Restart |
| SequenceWithMemory | Tick again | Tick again |
Action Node#
Create custom nodes with inheritance.
Concept#
Since it is highly recommended to implement your own action node based on the SyncActionNode or StatefulActionNode class, it is necessary to understand the difference between them.
SyncActionNode#
return only
SUCCESSorFAILURE.use SyncActionNode if
override the
tick()method is required.override the
providedPorts()method if you want to use thesetInput<T>()orgetOutput<T>()method.
Attack Node Example
//File: attck.hpp
#ifndef RM_DECISION_ATTACK_HPP_
#define RM_DECISION_ATTACK_HPP_
#include "behaviortree_cpp/bt_factory.h"
#include <rclcpp/rclcpp.hpp>
#include <geometry_msgs/msg/point_stamped.hpp>
#include <sensor_msgs/msg/point_cloud2.hpp>
#include <nav_msgs/msg/odometry.hpp>
#include "auto_aim_interfaces/msg/target.hpp"
#include <visualization_msgs/msg/marker_array.hpp>
#include <rclcpp/qos.hpp>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>
#include <pcl_conversions/pcl_conversions.h>
#include "rm_decision_cpp/custume_types.hpp"
#include <memory>
#include <optional>
#include <time.h>
using namespace BT;
namespace rm_decision
{
class Attack : public SyncActionNode
{
public:
Attack(const std::string &name, const NodeConfig &config);
NodeStatus tick() override;
static PortsList providedPorts();
private:
rclcpp::Subscription<sensor_msgs::msg::PointCloud2>::SharedPtr obs_pcl_sub_;
rclcpp::Subscription<nav_msgs::msg::Odometry>::SharedPtr state_estimation_sub_;
rclcpp::Publisher<visualization_msgs::msg::MarkerArray>::SharedPtr attack_pose_vis_pub_;
rclcpp::Publisher<visualization_msgs::msg::Marker>::SharedPtr final_attack_pose_vis_pub_;
rclcpp::Publisher<sensor_msgs::msg::PointCloud2>::SharedPtr croped_pcl_pub_;
rclcpp::Node::SharedPtr node_;
bool target_tracking_,obs_pcl_received_,vehicle_pose_received_;
double distance_to_target_,vehicle_dim_;
double vehicle_X_,vehicle_Y_;
int pose_candidate_num,min_obs_num_;
double obs_intensity_threshold_;
std::string gimble_frame_,global_frame_;
std::string obs_pcl_topic_,target_topic_;
auto_aim_interfaces::msg::Target target_info_;
geometry_msgs::msg::Pose target_point_;
pcl::PointCloud<pcl::PointXYZI>::Ptr obs_pcl_;
pcl::PointCloud<pcl::PointXYZI>::Ptr surrouding_obs_;
void obs_pcl_callback_(const sensor_msgs::msg::PointCloud2::SharedPtr msg);
void state_estimation_callback_(const nav_msgs::msg::Odometry::SharedPtr msg);
};
} // end namespace rm_decision
#endif
StatefulActionNode#
return
SUCCESS,FAILURE, orRUNNING.use StatefulActionNode if
the action node needs to be interrupted by another node.
Nav2Pose Node Example
//FILE: nav2pose.hpp
#ifndef RM_DECISION_NAV2POSE_HPP_
#define RM_DECISION_NAV2POSE_HPP_
#include "behaviortree_cpp/bt_factory.h"
#include <rclcpp/rclcpp.hpp>
#include <geometry_msgs/msg/point_stamped.hpp>
#include <geometry_msgs/msg/pose_stamped.hpp>
#include <std_msgs/msg/bool.hpp>
#include <std_msgs/msg/float32.hpp>
#include <nav_msgs/msg/odometry.hpp>
#include <action_msgs/msg/goal_status_array.hpp>
#include "rm_decision_cpp/custume_types.hpp"
using namespace BT;
namespace rm_decision
{
class Nav2Pose : public StatefulActionNode
{
public:
Nav2Pose(const std::string &name, const NodeConfig &config);
// this function is invoked once at the beginning.
NodeStatus onStart() override;
// If onStart() returned RUNNING, we will keep calling
// this method until it return something different from RUNNING
NodeStatus onRunning() override;
// callback to execute if the action was aborted by another node
void onHalted() override;
static PortsList providedPorts();
private:
rclcpp::Publisher<geometry_msgs::msg::PoseStamped>::SharedPtr goal_pub_;
rclcpp::Subscription<action_msgs::msg::GoalStatusArray>::SharedPtr planner_status_sub_;
rclcpp::Subscription<action_msgs::msg::GoalStatusArray>::SharedPtr controller_status_sub_;
rclcpp::Node::SharedPtr node_;
geometry_msgs::msg::PoseStamped goal_;
std::string goal_topic_,planner_status_sub_topic_,controller_status_sub_topic_;
void plannerStatusCallback(const action_msgs::msg::GoalStatusArray::SharedPtr msg);
void controllerStatusCallback(const action_msgs::msg::GoalStatusArray::SharedPtr msg);
int8_t planner_status_,controller_status_;
};
} // end namespace rm_decision
#endif
Steps of Creating a Custom Node#
Create a new class that inherits from
SyncActionNodeorStatefulActionNode.Override the required methods.
Define the ports that the node will use.
Scripting#
Behavior Tree 4.X introduces a simple but powerful new concept: a scripting language within XML.
simplify the tree structure
make the tree more readable
largely replace the use of the
DecoratororConditionnode.
See official tutorial for more details.
Usual Mistakes#
Fetch Beer#
the tree on the left side will always return SUCCESS, no matter if we have actually grabbed the beer.
the tree on the right side would return SUCCESS if the beer was there, FAILURE otherwise.
CUSTOM TYPE#
BehaviorTree.CPP supports the automatic conversion of strings into common types, such as int, long, double, bool, NodeStatus, etc. User-defined types can be supported easily as well.
How to Define a Custom Type#
and by invoke res=getInput
while and skipif
on halt and on post
different sequence