1 概述 安装.
1 2 sudo apt updatesudo apt install ros-$ROS_DISTRO -behaviortree-cpp -y
行为树类似于有限状态机, 都是用来控制任务流程的. 不同的是, 行为树(BT)是由一串分层节点组成. 每次执行循环, 会向根节点发一个 tick, tick 会一路传到叶子节点.
任何被 tick 的节点都会运行自己的回调, 并返回三个状态之一, SUCCESS/FAILURE/RUNNING. 叶子节点是真正执行动作或做判断的地方, 最常见的叶子是 Action 节点.
1.1 节点的类型
ControlNode(控制节点): 有 1..N 个孩子, 按自己的规则把 tick 分发给孩子.
ecoratorNode(装饰器节点):只能有 1 个孩子, 可以改变孩子的结果、重复执行孩子、或在条件不满足时阻止孩子被 tick.
ConditionNode(条件)节点:不能返回 RUNNING, 只做判断,不改变系统状态.
ActionNode(动作节点):真正“干活”, 可以是同步(一次 tick 就返回 S/F)或异步(可能返回 RUNNING,需要后续再 tick).
1.2 执行策略
Sequence: 像逻辑 AND. 从左到右依次执行子节点, 遇到第一个不是 SUCCESS 的结果就立刻停.
Decorator: 只包一名子节点, 在其外层加一层”策略”, 可改变子节点的执行时机或返回值, 如反转、重试、限制频率、超时等.
Fallback: 像逻辑 OR. 从左到右尝试策略, 一旦某个子节点不是 FAILURE 就停.
1.3 Blackboard 整棵树/子树共享的键值存储(作用域可分层), 用来在节点之间传数据.
1.4 Ports 每个节点在类型层面声明的输入/输出”接口”(如 InputPort<int>(“speed”)), 运行时通过 XML 里的”花括号”变量把端口和黑板里的键绑定/映射起来.
2 XML 结构 最外层是一个 root 标签, BTCPP_format 声明版本信息.
内层嵌套 BehaviorTree 标签, 必须有 ID 属性(这是”这棵树的名字”).
节点用单个 XML 标签表示, 子节点根据 1.1 进行约束.
下面给出示例, 同一棵行为树的不同写法.
1 2 3 4 5 6 7 8 9 10 <root BTCPP_format ="4" > <BehaviorTree ID ="MainTree" > <Sequence name ="root_sequence" > <SaySomething name ="action_hello" message ="Hello" /> <OpenGripper name ="open_gripper" /> <ApproachObject name ="approach_object" /> <CloseGripper name ="close_gripper" /> </Sequence > </BehaviorTree > </root >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <root BTCPP_format ="4" > <TreeNodeModel > <Action ID ="SaySomething" > <input_port name ="message" type ="std::string" /> </Action > <Action ID ="OpenGripper" /> <Action ID ="ApproachObject" /> <Action ID ="CloseGripper" /> </TreeNodeModel > <BehaviorTree ID ="MainTree" > <Sequence name ="root_sequence" > <SaySomething message ="Hello" /> <OpenGripper /> <ApproachObject /> <CloseGripper /> </Sequence > </BehaviorTree > </root >
2.1 子树 我们可以把一段流程单独描述成一棵树(例如 GraspObject).
在主树里用 <SubTree ID=”GraspObject”/> 直接调用.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <root BTCPP_format ="4" > <BehaviorTree ID ="MainTree" > <Sequence > <Action ID ="SaySomething" message ="Hello World" /> <SubTree ID ="GraspObject" /> </Sequence > </BehaviorTree > <BehaviorTree ID ="GraspObject" > <Sequence > <Action ID ="OpenGripper" /> <Action ID ="ApproachObject" /> <Action ID ="CloseGripper" /> </Sequence > </BehaviorTree > </root >
2.2 拆分文件 我们可以把多棵树拆到不同文件,用 <include path=”…”/> 引入.
ROS 环境下也可用 ros_pkg=”…” path=”相对路径” 来从包里找文件.
1 2 3 4 5 6 7 8 9 10 11 <root BTCPP_format ="4" > <include path ="grasp.xml" /> <BehaviorTree ID ="MainTree" > <Sequence > <Action ID ="SaySomething" message ="Hello World" /> <SubTree ID ="GraspObject" /> </Sequence > </BehaviorTree > </root >
1 2 3 4 5 6 7 8 9 10 <root BTCPP_format ="4" > <BehaviorTree ID ="GraspObject" > <Sequence > <Action ID ="OpenGripper" /> <Action ID ="ApproachObject" /> <Action ID ="CloseGripper" /> </Sequence > </BehaviorTree > </root >
3 代码示例 3.1 运行最小 DEMO 创建 ros2 功能包.
1 ros2 pkg create --build-type ament_cmake bt_first_tree_demo
1 2 3 4 5 6 7 8 9 10 . └── src └── bt_first_tree_demo ├── CMakeLists.txt ├── include │ └── bt_first_tree_demo ├── my_tree.xml ├── package.xml └── src └── main.cpp
编写 CMakeLists.txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 find_package (ament_cmake REQUIRED)find_package (behaviortree_cpp REQUIRED)find_package (ament_index_cpp REQUIRED)add_executable (bt_demo src/main.cpp)ament_target_dependencies(bt_demo behaviortree_cpp ament_index_cpp) install (TARGETS bt_demo DESTINATION lib/${PROJECT_NAME} ) install (FILES my_tree.xml DESTINATION share/${PROJECT_NAME} )
编写 my_tree.xml
1 2 3 4 5 6 7 8 9 10 <root BTCPP_format ="4" > <BehaviorTree ID ="MainTree" > <Sequence name ="root_sequence" > <CheckBattery /> <OpenGripper /> <ApproachObject /> <CloseGripper /> </Sequence > </BehaviorTree > </root >
加入依赖.
1 2 <depend > behaviortree_cpp</depend > <depend > ament_index_cpp</depend >
编写 main.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <behaviortree_cpp/bt_factory.h> #include <behaviortree_cpp/action_node.h> #include <ament_index_cpp/get_package_share_directory.hpp> #include <filesystem> #include <iostream> using namespace BT;class ApproachObject : public SyncActionNode {public : ApproachObject (const std::string& name, const NodeConfig& config) : SyncActionNode (name, config) {} static PortsList providedPorts () { return {}; } NodeStatus tick () override { std::cout << "[ApproachObject] approaching target...\n" ; return NodeStatus::SUCCESS; } }; class GripperInterface {public : NodeStatus open () { std::cout << "[Gripper] open\n" ; return NodeStatus::SUCCESS; } NodeStatus close () { std::cout << "[Gripper] close\n" ; return NodeStatus::SUCCESS; } }; NodeStatus CheckBattery (TreeNode&) { std::cout << "[Battery] OK\n" ; return NodeStatus::SUCCESS; } int main (int , char **) { BehaviorTreeFactory factory; factory.registerNodeType <ApproachObject>("ApproachObject" ); GripperInterface gripper; factory.registerSimpleAction ("OpenGripper" , [&gripper](TreeNode&) { return gripper.open (); }); factory.registerSimpleAction ("CloseGripper" , [&gripper](TreeNode&) { return gripper.close (); }); factory.registerSimpleCondition ("CheckBattery" , CheckBattery); const std::string share_dir = ament_index_cpp::get_package_share_directory ("bt_first_tree_demo" ); const std::filesystem::path xml_path = std::filesystem::path (share_dir) / "my_tree.xml" ; std::cout << "[BT] loading tree: " << xml_path << std::endl; try { auto tree = factory.createTreeFromFile (xml_path.string ()); NodeStatus status = NodeStatus::RUNNING; while (status == NodeStatus::RUNNING) { status = tree.tickOnce (); } std::cout << "[Tree] finished with status: " << BT::toStr (status) << std::endl; return status == NodeStatus::SUCCESS ? 0 : 1 ; } catch (const std::exception& e) { std::cerr << "[BT] failed: " << e.what () << std::endl; return 2 ; } }
编译运行.
1 2 3 colcon build source install/setup.bashros2 run bt_first_tree_demo bt_demo
4 blackboard 和 ports 4.1 基础概念 总体而言, blackboard 用来存储数据, 而 ports 分为 Inputport 和 Outport 负责数据流的传递.
两种写法.
1 2 <SaySomething name ="first" message ="hello world" /> <SaySomething name ="second" message ="{greetings}" />
第一行把字面量 “hello world” 传给端口;第二行让节点到黑板的 greetings 条目里取值.
4.1.2 Outputports 我们可以通过 Script 来将常量写进黑板.
1 <Script code =" the_answer:='The answer is 42' " />
当然, 我们也可以声明一个输出端口 text,在 tick() 中用 setOutput(“text”, “The answer is 42”) 写入黑板。这样,别的节点就能通过输入端口读取到这个值.
例如这里, 我们可以在 cpp 代码中调用上面的 setOutput(), 这里 text 是输出端口, 之后我们就给 ans 赋值了.
4.2 demo 按照上面的结构, 修改 xml 和 main.cpp.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <root BTCPP_format ="4" > <BehaviorTree ID ="Main" > <Sequence > <Writer text ="{ans}" /> <Speaker message ="{ans}" /> <Speaker message ="I am a constant." /> </Sequence > </BehaviorTree > </root >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <behaviortree_cpp/bt_factory.h> #include <behaviortree_cpp/action_node.h> #include <ament_index_cpp/get_package_share_directory.hpp> #include <filesystem> #include <iostream> int main () { BT::BehaviorTreeFactory factory; factory.registerSimpleAction ( "Writer" , [](BT::TreeNode& self){ std::string value = "The answer is 42" ; self.setOutput ("text" , value); return BT::NodeStatus::SUCCESS; }, BT::PortsList{ BT::OutputPort <std::string>("text" ) } ); factory.registerSimpleAction ( "Speaker" , [](BT::TreeNode& self){ auto msg = self.getInput <std::string>("message" ); if (!msg) throw BT::RuntimeError (msg.error ()); std::cout << "[Speaker] " << msg.value () << "\n" ; return BT::NodeStatus::SUCCESS; }, BT::PortsList{ BT::InputPort <std::string>("message" ) } ); const std::string share = ament_index_cpp::get_package_share_directory ("bt_first_tree_demo" ); const std::filesystem::path xml = std::filesystem::path (share) / "my_tree.xml" ; std::cout << "[BT] loading: " << xml << std::endl; auto tree = factory.createTreeFromFile (xml.string ()); auto status = BT::NodeStatus::RUNNING; while (status == BT::NodeStatus::RUNNING) { status = tree.tickOnce (); } std::cout << "[BT] finished: " << BT::toStr (status) << std::endl; return status == BT::NodeStatus::SUCCESS ? 0 : 1 ; }
5 泛型端口 端口不仅能用字符串, 还能用任意 C++ 类型(包含你自定义的结构体), 并且能把 XML 里的字符串自动转换成这些类型.
5.1 强类型的写法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 namespace BT {template <> inline MyType1 convertFromString (BT::StringView str) { MyType1 out; return out; } template <> inline MyType2 convertFromString (BT::StringView str) { MyType2 out; return out; } }
5.2 demo main.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <behaviortree_cpp/bt_factory.h> #include <behaviortree_cpp/action_node.h> #include <ament_index_cpp/get_package_share_directory.hpp> #include <iostream> #include <string> #include <chrono> struct VelocityCmd { double v; double w; }; namespace BT {template <> inline VelocityCmd convertFromString (BT::StringView str) { auto parts = BT::splitString (str, ';' ); if (parts.size () != 2 ) throw BT::RuntimeError ("Invalid VelocityCmd: " , str); VelocityCmd out; out.v = BT::convertFromString <double >(parts[0 ]); out.w = BT::convertFromString <double >(parts[1 ]); return out; } } int main () { BT::BehaviorTreeFactory factory; factory.registerSimpleAction ( "PlanVelocity" , [](BT::TreeNode& self){ VelocityCmd c{0.20 , 0.60 }; self.setOutput ("cmd" , c); std::cout << "[PlanVelocity] v=" << c.v << " w=" << c.w << "\n" ; return BT::NodeStatus::SUCCESS; }, BT::PortsList{ BT::OutputPort <VelocityCmd>("cmd" ) } ); factory.registerSimpleAction ( "PrintVelocity" , [](BT::TreeNode& self){ auto in = self.getInput <VelocityCmd>("cmd" ); if (!in) throw BT::RuntimeError (in.error ()); const auto c = in.value (); std::cout << "[PrintVelocity] v=" << c.v << " w=" << c.w << "\n" ; return BT::NodeStatus::SUCCESS; }, BT::PortsList{ BT::InputPort <VelocityCmd>("cmd" ) } ); const std::string share = ament_index_cpp::get_package_share_directory ("bt_first_tree_demo" ); const std::string xml = share + "/my_tree.xml" ; std::cout << "[BT] loading: " << xml << std::endl; auto tree = factory.createTreeFromFile (xml); using namespace std::chrono_literals; auto status = BT::NodeStatus::RUNNING; while (status == BT::NodeStatus::RUNNING) { status = tree.tickOnce (); } std::cout << "[BT] finished: " << BT::toStr (status) << std::endl; return status == BT::NodeStatus::SUCCESS ? 0 : 1 ; }
xml.
1 2 3 4 5 6 7 8 9 10 11 12 <root BTCPP_format ="4" > <BehaviorTree ID ="MainTree" > <Sequence > <PlanVelocity cmd ="{PlanCmd}" /> <PrintVelocity cmd ="{PlanCmd}" /> <Script code =" OtherCmd:='0.50;0.00' " /> <PrintVelocity cmd ="{OtherCmd}" /> </Sequence > </BehaviorTree > </root >
6 反应式行为 反应式行为指的是在行为树里, 一边执行动作, 一边持续复查前置条件. 一旦前置条件变了, 就立刻打断正在进行的动作并改走别的分支.
1 2 3 4 5 6 7 8 9 <root BTCPP_format ="4" > <BehaviorTree ID ="Main" > <ReactiveSequence > <A /> <B /> <C /> </ReactiveSequence > </BehaviorTree > </root >
比如以上, 党执行完 A 并执行到 B 的时候, 会从头开始检查 A 的条件是否改变.
当执行到 C 的时候, 会检查 A, B 的条件是否改变.
6.2 demo main.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <behaviortree_cpp/bt_factory.h> #include <behaviortree_cpp/action_node.h> #include <ament_index_cpp/get_package_share_directory.hpp> #include <filesystem> #include <iostream> #include <chrono> #include <thread> using namespace std::chrono;class SlowWork : public BT::StatefulActionNode {public : SlowWork (const std::string& name, const BT::NodeConfig& cfg) : BT::StatefulActionNode (name, cfg) {} static BT::PortsList providedPorts () { return {}; } BT::NodeStatus onStart () override { std::cout << "[SlowWork] start\n" ; done_at_ = steady_clock::now () + 1500 ms; return BT::NodeStatus::RUNNING; } BT::NodeStatus onRunning () override { std::this_thread::sleep_for (100 ms); if (steady_clock::now () >= done_at_) { std::cout << "[SlowWork] finished\n" ; return BT::NodeStatus::SUCCESS; } return BT::NodeStatus::RUNNING; } void onHalted () override { std::cout << "[SlowWork] ABORTED\n" ; } private : steady_clock::time_point done_at_{}; }; int main () { BT::BehaviorTreeFactory factory; factory.registerSimpleCondition ("SafetyOK" , [](BT::TreeNode&){ static const auto t0 = std::chrono::steady_clock::now (); auto ms = duration_cast <milliseconds>(steady_clock::now () - t0).count (); bool ok = ms < 600 ; std::cout << "[SafetyOK] " << (ok ? "OK" : "FAIL" ) << " at " << ms << "ms\n" ; return ok ? BT::NodeStatus::SUCCESS : BT::NodeStatus::FAILURE; }); factory.registerNodeType <SlowWork>("SlowWork" ); const std::string share = ament_index_cpp::get_package_share_directory ("bt_first_tree_demo" ); const std::filesystem::path xml = std::filesystem::path (share) / "my_tree.xml" ; std::cout << "[BT] loading: " << xml << std::endl; auto tree = factory.createTreeFromFile (xml.string ()); auto status = BT::NodeStatus::RUNNING; while (status == BT::NodeStatus::RUNNING) { status = tree.tickOnce (); if (status == BT::NodeStatus::RUNNING) { tree.sleep (std::chrono::milliseconds (100 )); } } std::cout << "[BT] finished: " << BT::toStr (status) << std::endl; return status == BT::NodeStatus::SUCCESS ? 0 : 1 ; }
普通版本.
1 2 3 4 5 6 7 8 <root BTCPP_format ="4" > <BehaviorTree ID ="Main" > <Sequence > <SafetyOK /> <SlowWork /> </Sequence > </BehaviorTree > </root >
运行结果为.
1 2 3 4 5 ... [SafetyOK] OK at 0ms [SlowWork] start [SlowWork] finished [BT] finished: SUCCESS
反应式版本.
1 2 3 4 5 6 7 8 <root BTCPP_format ="4" > <BehaviorTree ID ="Main" > <ReactiveSequence > <SafetyOK /> <SlowWork /> </ReactiveSequence > </BehaviorTree > </root >
运行结果为.
1 2 3 4 5 6 7 8 9 ... [SafetyOK] OK at 0ms [SlowWork] start [SafetyOK] OK at 100ms [SafetyOK] OK at 300ms [SafetyOK] OK at 501ms [SafetyOK] FAIL at 702ms [SlowWork] ABORTED [BT] finished: FAILURE
7 子树的使用 子树在 C++ 代码中我们不需要做什么修改, xml 文件会直接解析. 直接看例子.
xml.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <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 >
main.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 #include <behaviortree_cpp/bt_factory.h> #include <ament_index_cpp/get_package_share_directory.hpp> #include <filesystem> #include <iostream> #include <string> class CrossDoor { public : void registerNodes (BT::BehaviorTreeFactory& factory) ; BT::NodeStatus isDoorClosed () ; BT::NodeStatus passThroughDoor () ; BT::NodeStatus pickLock () ; BT::NodeStatus openDoor () ; BT::NodeStatus smashDoor () ; private : bool _door_open = false ; bool _door_locked = true ; int _pick_attempts = 0 ; }; void CrossDoor::registerNodes (BT::BehaviorTreeFactory &factory) { factory.registerSimpleCondition ("IsDoorClosed" , std::bind (&CrossDoor::isDoorClosed, this )); factory.registerSimpleAction ("PassThroughDoor" , std::bind (&CrossDoor::passThroughDoor, this )); factory.registerSimpleAction ("OpenDoor" , std::bind (&CrossDoor::openDoor, this )); factory.registerSimpleAction ("PickLock" , std::bind (&CrossDoor::pickLock, this )); factory.registerSimpleCondition ("SmashDoor" , std::bind (&CrossDoor::smashDoor, this )); } BT::NodeStatus CrossDoor::isDoorClosed () { std::cout << "[IsDoorClosed] door_open=" << std::boolalpha << _door_open << "\n" ; return (_door_open == false ) ? BT::NodeStatus::SUCCESS : BT::NodeStatus::FAILURE; } BT::NodeStatus CrossDoor::passThroughDoor () { std::cout << "[PassThroughDoor] trying to pass... " ; if (_door_open) { std::cout << "success.\n" ; return BT::NodeStatus::SUCCESS; } std::cout << "failed (door closed).\n" ; return BT::NodeStatus::FAILURE; } BT::NodeStatus CrossDoor::pickLock () { _pick_attempts++; std::cout << "[PickLock] attempt " << _pick_attempts << " (locked=" << std::boolalpha << _door_locked << ")\n" ; if (_door_locked && _pick_attempts < 3 ) { return BT::NodeStatus::FAILURE; } _door_locked = false ; _door_open = true ; std::cout << "[PickLock] lock picked. Door is now OPEN.\n" ; return BT::NodeStatus::SUCCESS; } BT::NodeStatus CrossDoor::openDoor () { std::cout << "[OpenDoor] trying to open... (locked=" << std::boolalpha << _door_locked << ")\n" ; if (_door_locked) { std::cout << "[OpenDoor] failed: door is locked.\n" ; return BT::NodeStatus::FAILURE; } _door_open = true ; std::cout << "[OpenDoor] success. Door is OPEN.\n" ; return BT::NodeStatus::SUCCESS; } BT::NodeStatus CrossDoor::smashDoor () { std::cout << "[SmashDoor] smashing the door! (this always opens it)\n" ; _door_locked = false ; _door_open = true ; return BT::NodeStatus::SUCCESS; } int main (int argc, char ** argv) { (void )argc; (void )argv; BT::BehaviorTreeFactory factory; CrossDoor cross_door; cross_door.registerNodes (factory); std::string pkg_share_dir = ament_index_cpp::get_package_share_directory ("bt_first_tree_demo" ); std::filesystem::path xml_file = std::filesystem::path (pkg_share_dir) / "my_tree.xml" ; factory.registerBehaviorTreeFromFile (xml_file.string ()); auto tree = factory.createTree ("MainTree" ); BT::printTreeRecursively (tree.rootNode ()); BT::NodeStatus status = BT::NodeStatus::IDLE; do { status = tree.tickOnce (); } while (status == BT::NodeStatus::RUNNING); std::cout << "Tree finished with status: " << (status == BT::NodeStatus::SUCCESS ? "SUCCESS" : "FAILURE" ) << "\n" ; return 0 ; }
8 端口重映射 在大型树里避免命名冲突: 每个(子)树都会有自己的 Blackboard 实例, 互不相同. 为了让父树里的数据(端口/Blackboard键)传进子树, 或把子树的结果带回父树, 就需要显式做端口重映射. 这件事完全在 XML 里完成.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <root BTCPP_format ="4" > <BehaviorTree ID ="MainTree" > <Sequence > <Script code =" move_goal='1;2;3' " /> <SubTree ID ="MoveRobot" target ="{move_goal}" result ="{move_result}" /> <SaySomething message ="{move_result}" /> </Sequence > </BehaviorTree > <BehaviorTree ID ="MoveRobot" > <Fallback > <Sequence > <MoveBase goal ="{target}" /> <Script code =" result:='goal reached' " /> </Sequence > <ForceFailure > <Script code =" result:='error' " /> </ForceFailure > </Fallback > </BehaviorTree > </root >
父树 MainTree 里, 先用 Script 把 Blackboard 键 move_goal 设为 ‘1;2;3’.
调用子树 MoveRobot 时, 通过 <SubTree … target=”{move_goal}” result=”{move_result}”/> 做端口重映射:
把子树内部使用的 {target} 映射到父树的 {move_goal}. 这样子树里 <MoveBase goal=”{target}”/> 实际就拿到了父树的目标.
把子树内部使用的 {result} 映射到父树的 {move_result}. 这样子树里设置 result:=’goal reached’ 或 ‘error’ 后, 父树就能用 <SaySomething message=”{move_result}”/> 说出来.
main.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include <behaviortree_cpp/bt_factory.h> #include <behaviortree_cpp/action_node.h> #include <ament_index_cpp/get_package_share_directory.hpp> #include <filesystem> #include <iostream> #include <string> struct Pose2D { double x = 0 , y = 0 , theta = 0 ; }; namespace BT{ template <> inline Pose2D convertFromString (StringView str) { auto parts = splitString (str, ';' ); if (parts.size () != 3 ) { throw RuntimeError ("invalid Pose2D format. Expected: x;y;theta Got: " , str); } Pose2D out; out.x = convertFromString <double >(parts[0 ]); out.y = convertFromString <double >(parts[1 ]); out.theta = convertFromString <double >(parts[2 ]); return out; } } using namespace BT;class MoveBase : public SyncActionNode{ public : MoveBase (const std::string& name, const NodeConfig& cfg) : SyncActionNode (name, cfg) {} static PortsList providedPorts () { return { InputPort <Pose2D>("goal" ) }; } NodeStatus tick () override { auto goal = getInput <Pose2D>("goal" ); if (!goal) { throw RuntimeError ("MoveBase: missing required input [goal]: " , goal.error ()); } std::cout << "[MoveBase] Go to (" << goal->x << ", " << goal->y << ", " << goal->theta << ")\n" ; return NodeStatus::SUCCESS; } }; class SaySomething : public SyncActionNode{ public : SaySomething (const std::string& name, const NodeConfig& cfg) : SyncActionNode (name, cfg) {} static PortsList providedPorts () { return { InputPort <std::string>("message" ) }; } NodeStatus tick () override { auto msg = getInput <std::string>("message" ); if (!msg) { throw RuntimeError ("SaySomething: missing required input [message]: " , msg.error ()); } std::cout << "[SaySomething] " << msg.value () << "\n" ; return NodeStatus::SUCCESS; } }; int main (int argc, char ** argv) { std::string package_name = "bt_first_tree_demo" ; std::string pkg_share_dir = ament_index_cpp::get_package_share_directory (package_name); std::filesystem::path xml_path = std::filesystem::path (pkg_share_dir) / "my_tree.xml" ; BehaviorTreeFactory factory; factory.registerNodeType <MoveBase>("MoveBase" ); factory.registerNodeType <SaySomething>("SaySomething" ); factory.registerBehaviorTreeFromFile (xml_path.string ()); auto tree = factory.createTree ("MainTree" ); BT::printTreeRecursively (tree.rootNode ()); BT::NodeStatus status = BT::NodeStatus::IDLE; do { status = tree.tickOnce (); } while (status == BT::NodeStatus::RUNNING); try { auto res = tree.rootBlackboard ()->get <std::string>("move_result" ); std::cout << "[Main] move_result = " << res << "\n" ; } catch (const std::exception& e) { std::cout << "[Main] no move_result: " << e.what () << "\n" ; } return 0 ; }
9 多个 XML 文件的使用 这里有两种方式.
在 C++ 里手动把多个 XML 注册进 factory, 然后按 ID 创建想要的树. behaviortree.dev
XML.
1 2 3 4 5 6 7 8 9 <root > <BehaviorTree ID ="MainTree" > <Sequence > <SaySomething message ="starting MainTree" /> <SubTree ID ="SubTreeA" /> <SubTree ID ="SubTreeB" /> </Sequence > </BehaviorTree > </root >
1 2 3 4 5 <root > <BehaviorTree ID ="SubTreeA" > <SaySomething message ="Executing Sub_A" /> </BehaviorTree > </root >
1 2 3 4 5 <root > <BehaviorTree ID ="SubTreeB" > <SaySomething message ="Executing Sub_B" /> </BehaviorTree > </root >
main.cpp.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <behaviortree_cpp/bt_factory.h> #include <behaviortree_cpp/action_node.h> #include <ament_index_cpp/get_package_share_directory.hpp> #include <filesystem> #include <iostream> using namespace BT;class SaySomething : public SyncActionNode {public : SaySomething (const std::string& name, const NodeConfig& config) : SyncActionNode (name, config) {} static PortsList providedPorts () { return { InputPort <std::string>("message" ) }; } NodeStatus tick () override { const auto msg = getInput <std::string>("message" ).value_or ("" ); std::cout << "Robot says: " << msg << std::endl; return NodeStatus::SUCCESS; } }; int main (int argc, char ** argv) { BT::BehaviorTreeFactory factory; factory.registerNodeType <SaySomething>("SaySomething" ); const std::string share = ament_index_cpp::get_package_share_directory ("bt_first_xml_demo" ); const std::string trees_dir = share; using std::filesystem::directory_iterator; for (const auto & entry : directory_iterator (trees_dir)) { if (entry.path ().extension () == ".xml" ) { factory.registerBehaviorTreeFromFile (entry.path ().string ()); } } std::cout << "----- MainTree tick ----" << std::endl; auto main_tree = factory.createTree ("MainTree" ); main_tree.tickWhileRunning (); std::cout << "----- SubTreeA tick ----" << std::endl; auto subA = factory.createTree ("SubTreeA" ); subA.tickWhileRunning (); std::cout << "----- SubTreeB tick ----" << std::endl; auto subB = factory.createTree ("SubTreeB" ); subB.tickWhileRunning (); return 0 ; }
在主 XML 里用 <include path=”…”/> 把其它 XML 包进来, 路径相对主 XML 文件,然后直接从这个主 XML 创建树.
XML.
1 2 3 4 5 6 7 8 9 10 11 12 <root BTCPP_format ="4" > <include path ="./subtree_A.xml" /> <include path ="./subtree_B.xml" /> <BehaviorTree ID ="MainTree" > <Sequence > <SaySomething message ="starting MainTree" /> <SubTree ID ="SubTreeA" /> <SubTree ID ="SubTreeB" /> </Sequence > </BehaviorTree > </root >
main.cpp.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <behaviortree_cpp/bt_factory.h> #include <behaviortree_cpp/action_node.h> #include <ament_index_cpp/get_package_share_directory.hpp> #include <filesystem> #include <iostream> using namespace BT;class SaySomething : public SyncActionNode {public : SaySomething (const std::string& name, const NodeConfig& config) : SyncActionNode (name, config) {} static PortsList providedPorts () { return { InputPort <std::string>("message" ) }; } NodeStatus tick () override { const auto msg = getInput <std::string>("message" ).value_or ("" ); std::cout << "Robot says: " << msg << std::endl; return NodeStatus::SUCCESS; } }; int main (int argc, char ** argv) { BT::BehaviorTreeFactory factory; factory.registerNodeType <SaySomething>("SaySomething" ); const std::string share = ament_index_cpp::get_package_share_directory ("bt_first_xml_demo" ); const std::string trees_dir = share; using std::filesystem::directory_iterator; for (const auto & entry : directory_iterator (trees_dir)) { if (entry.path ().extension () == ".xml" ) { factory.registerBehaviorTreeFromFile (entry.path ().string ()); } } auto tree = factory.createTreeFromFile (trees_dir + "/my_tree.xml" ); tree.tickWhileRunning (); return 0 ; }
9 额外传参 当一些参数在部署期就确定、且运行时不会频繁变(比如句柄、阈值、指针/引用、服务客户端、日志器、设备句柄等), 这时用端口反而累赘.
9.1 构造函数时传参 自定义节点把额外参数写进构造函数签名(在 name, config 之后).
注册时用 factory.registerNodeType<T>(“ID”, extra_args…) 把这些参数传进去。
XML 里照常 <T/>,XML 不负责这些额外参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 using namespace BT;class NoCopyObj {public : explicit NoCopyObj (int v) : _v(v) { } NoCopyObj (const NoCopyObj&) = delete ; int value () const { return _v; } private : int _v{0 }; }; class Action_A : public SyncActionNode {public : Action_A (const std::string& name, const NodeConfig& cfg, int arg_int, std::string arg_str, NoCopyObj& ref_obj) : SyncActionNode (name, cfg), a_ (arg_int), s_ (std::move (arg_str)), ref_ (ref_obj) {} static PortsList providedPorts () { return {}; } NodeStatus tick () override { std::cout << name () << ": " << a_ << " / " << s_ << " / " << ref_.value () << "\n" ; return NodeStatus::SUCCESS; } private : int a_; std::string s_; NoCopyObj& ref_; }; int main () { BehaviorTreeFactory factory; NoCopyObj nc (88 ) ; factory.registerNodeType <Action_A>("Action_A" , 42 , std::string ("hello" ), std::ref (nc)); auto tree = factory.createTreeFromText (R"( <root BTCPP_format="4"><BehaviorTree><Action_A/></BehaviorTree></root> )" ); tree.tickWhileRunning (); }
9.2 用 initialize 初始化 在节点类里提供 initialize(args…).
树创建后(第一次 tick 前),对树做一次 applyVisitor,找到该类型节点并调用 initialize(…).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Action_B : public SyncActionNode {public : Action_B (const std::string& name, const NodeConfig& cfg) : SyncActionNode (name, cfg) {} void initialize (int arg_int, std::string arg_str) { a_=arg_int; s_=std::move (arg_str); } static PortsList providedPorts () { return {}; } NodeStatus tick () override { std::cout << a_ << " / " << s_ << "\n" ; return NodeStatus::SUCCESS; } private : int a_{}; std::string s_;}; BehaviorTreeFactory factory; factory.registerNodeType <Action_B>("Action_B" ); auto tree = factory.createTreeFromText (R"( <root BTCPP_format="4"><BehaviorTree><Action_B/></BehaviorTree></root> )" );tree.applyVisitor ([](TreeNode* node){ if (auto b = dynamic_cast <Action_B*>(node)){ b->initialize (69 , "init_val" ); } }); tree.tickWhileRunning ();
10 连接 groot2 groot2 是一个行为树的可视化观察工具.
第一步创建 xml 文件.
1 2 3 4 5 const std::string models_xml = BT::writeTreeNodesModelXML (factory);const std::string models_dir = share_dir + "/models" ;fs::create_directories (models_dir); const std::string models_file = models_dir + "/models.xml" ;std::ofstream (models_file) << models_xml;
第二步, 发布节点.
1 BT::Groot2Publisher publisher (tree) ;
之后接入 groot2 就可以运行了.
以下为示例.
XML.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <root BTCPP_format ="4" > <BehaviorTree ID ="Main" > <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 >
main.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include <behaviortree_cpp/bt_factory.h> #include <behaviortree_cpp/loggers/groot2_publisher.h> #include <behaviortree_cpp/xml_parsing.h> #include <ament_index_cpp/get_package_share_directory.hpp> #include <filesystem> #include <fstream> #include <iostream> #include <thread> #include <chrono> namespace fs = std::filesystem;using namespace std::chrono_literals;class CrossDoor {public : void registerNodes (BT::BehaviorTreeFactory& factory) { using BT::NodeStatus; factory.registerSimpleCondition ("IsDoorClosed" , [this ](BT::TreeNode&) -> NodeStatus { std::cout << "[IsDoorClosed] door_open=" << std::boolalpha << door_open_ << std::endl; return door_open_ ? NodeStatus::FAILURE : NodeStatus::SUCCESS; }); factory.registerSimpleAction ("OpenDoor" , [this ](BT::TreeNode&) -> NodeStatus { std::cout << "[OpenDoor] opening door\n" ; door_locked_ = false ; door_open_ = true ; return NodeStatus::SUCCESS; }); factory.registerSimpleAction ("PickLock" , [this ](BT::TreeNode&) -> NodeStatus { std::cout << "[PickLock] attempt " << (pick_attempts_+1 ) << "\n" ; if (pick_attempts_++ >= 2 ) { door_locked_ = false ; door_open_ = true ; std::cout << "[PickLock] success\n" ; return NodeStatus::SUCCESS; } std::this_thread::sleep_for (std::chrono::milliseconds (300 )); return NodeStatus::FAILURE; }); factory.registerSimpleAction ("SmashDoor" , [this ](BT::TreeNode&) -> NodeStatus { std::cout << "[SmashDoor] brute force!\n" ; door_locked_ = false ; door_open_ = true ; return NodeStatus::SUCCESS; }); factory.registerSimpleAction ("PassThroughDoor" , [this ](BT::TreeNode&) -> NodeStatus { std::cout << "[PassThroughDoor] " ; if (door_open_) { std::cout << "passed.\n" ; return NodeStatus::SUCCESS; } std::cout << "blocked.\n" ; return NodeStatus::FAILURE; }); } void reset () { door_open_ = false ; door_locked_ = true ; pick_attempts_ = 0 ; } private : bool door_open_ = false ; bool door_locked_ = true ; int pick_attempts_ = 0 ; }; int main (int argc, char ** argv) { const std::string share_dir = ament_index_cpp::get_package_share_directory ("bt_first_tree_demo" ); std::filesystem::path xml_path = std::filesystem::path (share_dir) / "my_tree.xml" ; BT::BehaviorTreeFactory factory; CrossDoor cross_door; cross_door.registerNodes (factory); const std::string models_xml = BT::writeTreeNodesModelXML (factory); const std::string models_dir = share_dir + "/models" ; fs::create_directories (models_dir); const std::string models_file = models_dir + "/models.xml" ; std::ofstream (models_file) << models_xml; std::cout << "[Info] Wrote models to: " << models_file << std::endl; factory.registerBehaviorTreeFromFile (xml_path); auto tree = factory.createTree ("Main" ); BT::Groot2Publisher publisher (tree) ; while (true ) { std::cout << "=== New run ===" << std::endl; cross_door.reset (); tree.tickWhileRunning (); std::this_thread::sleep_for (2 s); } return 0 ; }
11 References