Behavior Tree 与有限状态机是两种常用于游戏以及机器人复杂任务决策的框架,而行为树有着有限状态机所不具备的扩展性,ROS2的
Navigation2
中也引入了行为树来组织机器人的工作流程和动作执行。本文主要介绍行为树的基本概念与抽象模型,并记录BehaviorTree.CPP API的学习与笔记。
- Behavior Tree
Created 2021.01.26 by William Yu; Last modified: 2022.07.12-V1.2.4
Contact: windmillyucong@163.com
Copyleft! 2022 William Yu. Some rights reserved.
Behavior Tree
References
- API BehaviorTree.CPP https://www.behaviortree.dev
- github https://github.com/BehaviorTree/BehaviorTree.CPP/
- post: ROS2中的行为树 https://www.guyuehome.com/38442
- paper: Behavior Trees in Robotics and AI
- 该文详细介绍了行为树,并且对比了行为树和状态机之间的优劣。
Ch1. 基本概念
Fig1. Btree
0. Basic Concepts
keywords
所有节点汇总
- ControlNode 控制节点
- SequenceNode 序列节点
- SequenceNode 普通顺序节点
- SequenceStarNode
- ReactiveSequence
- Selector 选择节点
- FallbackNode
- ReactiveFallback
- IfThenElseNode
- ManualSelectorNode
- SwitchNode
- WhileDoElseNode
- ParallelNode 并行节点
- SequenceNode 序列节点
-
ConditionNode 条件节点
- ActionNode 行为节点
- 同步节点 SyncActionNode
- AlwaysFailureNode
- AlwaysSuccessNode
- PopFromQueue
- SetBlackboard
- 异步节点 AsyncActionNode
- 协程节点 CoroActionNode
- 同步节点 SyncActionNode
- 装饰节点
- …
sentence
-
行为树有三类主要节点:
-
根节点
- 逻辑控制类节点(Control Node)
- 选择节点 Priority / Selector / Fallback_node / Fallback_star_node
- 规则:从begin到end迭代执行自己的Child Node:如遇到一个Child Node执行后返回True,那停止迭代,本Node向自己的Parent Node也返回True;否则所有Child Node都返回False,那本Node向自己的Parent Node返回False。
-
并行节点 ParallelNode
- 规则:从头到尾,平行执行所有的节点。
- Parallel Selector Node: 有一个子节点True返回True,否则返回False。 Parallel Sequence Node: 有一个子节点False返回False,否则返回True。 Parallel Fall On All Node: 所有子节点False才返回False,否则返回True。 Parallel Succeed On All Node: 所有子节点True才返回True,否则返回False。 Parallel Hybrid Node: 指定数量的子节点返回True或False后,才决定结果。
- 规则:从头到尾,平行执行所有的节点。
- 序列节点 Sequence Node / Sequence Star Node
- 规则:从头到尾,按顺序执行每一个子节点,遇到False停止。
- 选择节点 Priority / Selector / Fallback_node / Fallback_star_node
-
行为节点
-
条件节点
- 条件装饰节点 一般用来作为额外的附加条件,包括间隔控制,次数控制,频率控制等
- 反转
- Retry
-
-
对于有限状态机FSM,每个节点表示一个状态,而对于BT,每个节点表示一个行为。
-
每个行为节点有4种运行状态:
- 空闲 Idle
- 运行中 Running
- 完成 Success
- 失败 Failure
Example
1. 根节点 root
根节点是行为树的入口点,具有以下特点:
- 作用
- 作为行为树的起始点
- 负责初始化和管理行为树的执行
- 控制整个行为树的生命周期
- 特性
- 每个行为树有且仅有一个根节点
- 根节点通常是一个控制节点(如 Sequence 或 Fallback)
- 根节点的状态决定整个行为树的执行状态
- 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
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
4. 执行方式
- 通过调用 `tickRoot()` 方法触发行为树的执行
- 根节点会依次调用其子节点的 tick() 方法
- 子节点的执行结果会向上传递给根节点
### 2. 控制类节点 Control Node (重点)
控制类节点划分为 序列节点,并行节点,选择节点。
##### 2.1 序列节点
- [https://www.behaviortree.dev/sequencenode/](https://www.behaviortree.dev/sequencenode/)
e.g.

- 三种序列节点
- Sequence
- SequenceStar
- ReactiveSequence
- 三种序列节点共有的规则
- 执行有顺序,从左到右依次执行
- 如果子节点返回success,执行下一个子节点
- 如果中间有任何一个子节点返回failure,不再执行后面的子节点,Sequence直接退出,并向自己的父节点返回failure
- 所有子节点都返回success时,Sequence向父节点返回success
- 在第一个子节点执行前,序列节点的状态变为 **RUNNING**
- 三种序列节点的区别
| Type of ControlNode | Child returns FAILURE | Child returns RUNNING |
| ------------------- | --------------------- | --------------------- |
| Sequence | Restart | Tick again |
| SequenceStar | Tick again | Tick again |
| ReactiveSequence | Restart | Restart |
- "**Restart**" means that the entire sequence is restarted from the first child of the list. (个人理解为:比如对于SequenceNode,当一个子节点返回失败的时候,会触发Restart,这个restart的意思是,下一次调用这个SequenceNode的时候会从头开始执行每一个子节点,所以个人认为,上述原文描述再加几句:"**Restart**" means that the entire sequence <u>will be</u> restarted from the first child of the list, <u>when the next time this sequence is ticked</u>.)
- "**Tick again**" means that the next time the sequence is ticked, the same child is ticked again. Previous sibling, which returned SUCCESS already, are not ticked again.
###### 2.1.1 Sequence Node

相当于如下 pseudocode
```c++
BT::NodeStatus Sequence() {
status = RUNNING;
// _index is a private member
while(_index < number_of_children) {
child_status = child[_index]->tick();
if( child_status == SUCCESS ) {
_index++;
}
else if( child_status == RUNNING ) {
// keep same index
return RUNNING;
}
else if( child_status == FAILURE ) {
HaltAllChildren();
_index = 0;
return FAILURE;
}
}
// all the children returned success. Return SUCCESS too.
HaltAllChildren();
_index = 0;
return SUCCESS;
}
2.1.2 ReactiveSequence
相当于如下伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BT::NodeStatus ReactiveSequence() {
status = RUNNING;
for (int index=0; index < number_of_children; index++) {
child_status = child[index]->tick();
if( child_status == RUNNING ) {
return RUNNING;
}
else if( child_status == FAILURE ) {
HaltAllChildren();
return FAILURE;
}
}
// all the children returned success. Return SUCCESS too.
HaltAllChildren();
return SUCCESS;
}
2.1.3 SequenceStar
相当于如下伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BT::NodeStatus SequenceStar() {
status = RUNNING;
// _index is a private member
while( index < number_of_children) {
child_status = child[index]->tick();
if( child_status == SUCCESS ) {
_index++;
}
else if( child_status == RUNNING ||
child_status == FAILURE ) {
// keep same index
return child_status;
}
}
// all the children returned success. Return SUCCESS too.
HaltAllChildren();
_index = 0;
return SUCCESS;
}
2.1.4 总结
如上三种Node之间的区别就已经比较明确了:
- Reactive Sequence 每一次被调用都会从头开始执行
- Sequence 有一个全局静态标记
- Sequence 每一次被调用,都是去执行标记的节点
- 这个标记 指向 RUNNING状态的节点
- 如果RUNNING子节点返回FAILURE,标记清空,下一次被调用时从头再来
- 如果RUNNING子节点返回SUCCESS,标记后移,指向下一个子节点
- SequenceStar 有一个全局静态标记
- SequenceStar 每一次被调用,都是去执行标记的节点
- 这个标记 指向 RUNNING 状态的节点
- 如果RUNNING子节点返回FAILURE,标记并不会清空,还指向当前子节点,下一次被调用时从失败的地方继续
- 如果RUNNING子节点返回SUCCESS,标记后移,指向下一个子节点
从游戏的角度举例理解,非常有趣(再一次感受到,行为树所尝试的事情,是抽象现实世界的行为)
- Reactive Sequence 是不支持存档的游戏,第二次打开要从头开始过关。
- 如果第一次打开后已经过了某一关,则第二次打开时候需再过一次
- 如果第一次打开后某一关是异步的且正在进行,则第二次打开之后到这一关的时候接回之前的进度条
- Sequence 支持存档,过一关存一个档,下次打开还可以从第一次打开的进度的地方继续进行,但是一旦死亡,存档也会丢失,只有一条命
- Sequence Star 支持存档,过一关存一个档,下次打开还可以从存档的地方继续进行,即便死亡,也不会丢档,有无数条命,原地复活
补充说明:
- 当然你需要明确的一点是:序列节点的效果,不仅由序列节点的不同而异,还要考虑子执行节点是同步还是异步,如果你的子执行节点是同步的,它会阻塞父节点,此时SequenceNode任期内不可能发生第二次tick,几种顺序节点就没区别了
2.2 并行节点 ParallelNode
- 并行节点同时执行所有的子节点
- 但是并不是在不同的线程里执行
- 意味着如果所有的子节点都是同步节点,那还是相当于在一个一个按照顺序执行
- 如果所有的子节点都是异步节点,则这些节点同时进行
- 如果子节点是一个异步节点,后面跟一个同步节点,则这两个节点也可以同时进行
- 如果子节点是一个同步节点,后面跟一个异步节点,则会被这个同步节点阻塞
-
并行节点是唯一一个可以存在多个RUNNING子节点的节点
- 有成功阈值和失败阈值
- 当成功的子节点数量或者失败的子节点数量达到阈值,则halt其余还在running的子节点,并且向父节点返回结果
- 阈值为-1表示等于子节点的数量
- 默认失败阈值为-1
2.3 选择节点 Fallback / ReactiveFallback Node
- https://www.behaviortree.dev/fallbacknode/
- 又称为 “Selector” or “Priority”
e.g.
- 两种选择节点
- Fallback
- ReactiveFallback
- 共同特点
- 在执行子节点之前,将当前节点的状态设置为 RUNNING
- 按照顺序执行子节点
- 如果子节点返回FAILURE,执行下一个子节点
- 如果最后一个子节点也返回失败,则向当前节点的父节点返回失败
- 一旦某一个子节点返回成功,Fallback直接向父节点返回success,不再执行后面的节点
- 区别
- Fallback 有一个全局静态变量 标志,指向当前在执行的子节点
- Fallback 被调用的时候会执行 标志指向的子节点
- 如果子节点返回失败,执行下一个
- 如果子节点返回成功,标志清空,不再执行后面的子节点,当前节点退出并向父节点返回成功
- ReactiveFallback 每次被调用的时候都会从头开始执行
3. 装饰器节点 Decorator Node
3.1 用处
- 转接子节点接收的结果,可以取反
- 终止子节点
- 重复执行子节点
e.g.
4. 行为节点 Action Node
行为节点又细分为 异步节点,同步节点,和协程节点。
4.1 四种状态
- 空闲 Idle
- 运行中 Running
- 完成 Success
- 失败 Failure
4.2 三种行为节点 (重点)
4.1.1 AsyncActionNode 异步节点
- 被触发时, 返回running
- 会在另外一个线程中执行,不阻塞父节点所在的线程
- 用于处理具有以下特性的任务:
- 花很长时间才能得出结论的任务
- 可以停止
- 可以返回”running”
4.1.2 SyncActionNode 同步节点
- 不可能返回running
- 在父节点所在的线程内执行,会阻塞父节点所在的线程
4.1.3 CoroActionNode 协程节点
- coroutine node
- 可能返回running,也可能不反悔running
- 在父节点所在的线程内另开一个协程执行,不会阻塞父节点的线程
- 不会产生一个新的线程,效率更高
5. 条件节点 ConditionNode
- 非常简单
- 条件满足,返回成功
- 条件不满足,返回失败
6. Blackboard
-
https://blog.csdn.net/Travis_X/article/details/87772326?utm_medium=distribute.pc_relevant.none-task-blog-searchFromBaidu-18.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-searchFromBaidu-18.control
- 黑板:树的所有节点共享的键/值存储,本质上就是一个键值对
- A “blackboard” is a simple key/value storage shared by all the nodes of the Tree.
- An “entry” of the Blackboard is a key/value pair.
- 黑板可以分配给任何树
- 树之间也可以共享黑板
- 差不多相当于数据库
7. 子树
可以在不改变现有代码的情况下面扩展子树,非常有效地开发代码
8. XML格式
行为树使用 XML 格式来描述树的结构。XML 格式具有以下特点:
8.1 基本结构
1
2
3
4
5
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<!-- 树的内容 -->
</BehaviorTree>
</root>
<root>
是最外层标签,必须指定main_tree_to_execute
属性<BehaviorTree>
定义一个行为树,必须指定唯一的ID
属性- 每个节点都可以有
name
属性,用于调试和可视化
8.2 节点格式
节点的基本格式为:
1
2
3
<NodeType name="node_name" param1="value1" param2="value2">
<!-- 子节点 -->
</NodeType>
- 控制节点(如 Sequence、Fallback)可以包含多个子节点
- 装饰节点只能包含一个子节点
- 动作节点和条件节点是叶子节点,不能包含子节点
8.3 参数传递
XML 支持两种方式传递参数:
- 静态值:直接在属性中指定 ```xml
1
2
3
4
2. 黑板值:使用 `{key}` 语法引用黑板中的值
```xml
<SaySomething message="{blackboard_key}" />
8.4 子树
可以在一个 XML 文件中定义多个树,并通过 SubTree
节点引用:
1
2
3
4
5
6
7
8
9
10
11
12
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="SubTree">
<!-- 子树内容 -->
</BehaviorTree>
<BehaviorTree ID="MainTree">
<Sequence>
<SubTree ID="SubTree"/>
<!-- 其他节点 -->
</Sequence>
</BehaviorTree>
</root>
8.5 常见属性
name
: 节点名称,用于调试ID
: 树或节点的唯一标识符message
: 消息内容(用于 SaySomething 等节点)output_key
: 输出到黑板的键名input_key
: 从黑板读取的键名num_attempts
: 重试次数(用于 RetryUntilSuccessful 等节点)
Ch2. BehaviorTree.CPP
本章简单介绍Btree.cpp库的安装与使用,以创建一个简单的Btree结构为例。
1. Lib Install & Usage
BehaviorTree.CPP库的安装和使用
Install
1
2
3
4
5
6
7
8
9
git clone https://github.com/BehaviorTree/BehaviorTree.CPP.git
cd BehaviorTree.CPP
git checkout 3.8.6
mkdir build
cd build
cmake ..
make -j12
sudo make install
usage
1
2
3
4
5
6
7
8
9
10
set(BINARY btree-test)
set(SOURCES
first_tree.cpp
)
add_executable(${BINARY} ${SOURCES})
find_library(BT_LIB behaviortree_cpp_v3)
target_link_libraries(${BINARY} PUBLIC
${BT_LIB}
)
2. Create a tree
code example: t1.hello_btree
- https://www.behaviortree.dev/tutorial_01_first_tree/
- https://blog.csdn.net/whahu1989/article/details/112295130 编译与实操
- https://blog.csdn.net/Travis_X/article/details/87687914?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control
- https://blog.csdn.net/travis_x/article/details/87693812?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-14&spm=1001.2101.3001.4242
步骤
- 实现功能:实现函数功能,可以是简单的函数,可以是类的方法
- 在接口中,将这些功能注册为node
- 在main函数中,使用node搭建行为树
1. 创建Node
-
可以是直接写一个简单函数
-
可以是类中的方法
-
可以是直接一个函数,并且传入BT::TreeNode &self
-
可以是类继承行为节点
-
此时需要继承行为节点
-
并在类内重载tick()函数
e.g.
1 2 3 4 5 6 7 8 9 10 11 12 13
// 创建一个ApproachObject Node,它是SyncActionNode类型的同步行为节点。 class ApproachObject : public BT::SyncActionNode { public: ApproachObject(const std::string& name) : BT::SyncActionNode(name, {}) { } // You must override the virtual function tick() BT::NodeStatus tick() override { std::cout << "ApproachObject: " << this->name() << std::endl; return BT::NodeStatus::SUCCESS; } };
-
2. 注册Node
-
创建一个 BT::BehaviorTreeFactory factory;
-
调用生成工厂的方法来注册所有的Node
e.g.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// 注册所有的node inline void RegisterNodes(BT::BehaviorTreeFactory& factory) { // 传入工厂 // The recommended way to create a Node is through inheritance. factory.registerNodeType<ApproachObject>("ApproachObject"); factory.registerNodeType<SaySomething>( "SaySomething"); //直接指向类,则默认触发重载后的tick()方法 // Registering a SimpleActionNode using a function pointer. // you may also use C++11 lambdas instead of std::bind factory.registerSimpleCondition("CheckBattery", std::bind(CheckBattery)); // You can also create SimpleActionNodes using methods of a class static GripperInterface grip_singleton; factory.registerSimpleAction( "OpenGripper", std::bind(&GripperInterface::open, &grip_singleton)); // 直接指向具体的类内方法 factory.registerSimpleAction( "CloseGripper", std::bind(&GripperInterface::close, &grip_singleton)); // 直接指向具体的类内方法 // Trees are created at deployment-time (i.e. at run-time, but only // once at the beginning). }
-
对于简单函数,如下方式注册成node
1
factory.registerSimpleCondition("CheckBattery", std::bind(CheckBattery));
-
对于简单类内方法,如下
1 2 3 4 5 6 7 8
// You can also create SimpleActionNodes using methods of a class static GripperInterface grip_singleton; factory.registerSimpleAction( "OpenGripper", std::bind(&GripperInterface::open, &grip_singleton)); // 直接指向具体的类内方法 factory.registerSimpleAction( "CloseGripper", std::bind(&GripperInterface::close, &grip_singleton)); // 直接指向具体的类内方法
-
对于继承行为节点的类,如下
1 2 3
// The recommended way to create a Node is through inheritance. factory.registerNodeType<ApproachObject>("ApproachObject"); factory.registerNodeType<SaySomething>("SaySomething"); //直接指向继承了行为节点的类,默认触发重载后的tick()方法
3. 实现树
- 使用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
//--------------------------------
// 设计树的结构
// clang-format off
static const char* xml_text = R"(
<root main_tree_to_execute = "MainTree" >
<BehaviorTree ID="MainTree">
<Sequence name="root_sequence">
<CheckBattery name="battery_ok"/>
<SaySomething name="say_something" message="catch..."/>
<OpenGripper name="open_gripper"/>
<ApproachObject name="approach_object"/>
<CloseGripper name="close_gripper" />
<SaySomething name="say_something" message="get u"/>
</Sequence>
</BehaviorTree>
</root>
)";
int main() {
BT::BehaviorTreeFactory factory;
DummyNodes::RegisterNodes(factory);
auto tree = factory.createTreeFromText(xml_text);
tree.tickRoot();
return 0;
}
Ch3. Basic Ports
- https://www.behaviortree.dev/tutorial_02_basic_ports/
和functions一样,我们有时需要Node传参,包括传入参数,以及获取返回参数。
1. 传入参数 Input ports
2种方法
- static strings which can be parsed by the Node,or 使用可以被Node解析的静态字符串
- “pointers” to an entry of the Blackboard, identified by a key. 使用指针指向Blackboard,根据key检索
创建树
- 树实现时,使用 键值对 传入信息
-
可以一次性传入多个键
- 例如一个node名叫SaySomething,此时 键为message
1
2
<SaySomething name="first" message="hello world" />
<SaySomething name="second" message="{greetings}" />
- 前一种从xml里面读取string, 只能创建静态的信息
- 后一种回去blackboard里面查找键’greetings’,支持动态信息
Node创建
- Node创建时,使用getInput<std::string>(“message”) 接收这个message关键词
方式1:继承实现
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
/**---------------------------------------------------------------------------
* 创建可输入信息的Node 第一种创建方式:继承
*----------------------------------------------------------------------------*/
// SyncActionNode(synchronous action) with an input port.
// 有输入参数的同步行为节点
class SaySomething : public BT::SyncActionNode {
public:
// 强制构造函数格式
// If your Node has ports, you must use this constructor signature
SaySomething(const std::string& name, const BT::NodeConfiguration& config)
: SyncActionNode(name, config) {}
// 强制实现一个静态方法providedPorts
// It is mandatory to define this static method.
static BT::PortsList providedPorts() {
// This action has a single input port called "message"
// Any port must have a name. The type is optional.
return {BT::InputPort<std::string>("message")};
}
// As usual, you must override the virtual function tick()
BT::NodeStatus tick() override {
BT::Optional<std::string> msg = getInput<std::string>(
"message"); // 使用 TreeNode::getInput<T>(key). 方法获取参数
// Check if optional is valid. If not, throw its error
if (!msg) {
throw BT::RuntimeError("missing required input [message]: ", msg.error());
}
// use the method value() to extract the valid message.
std::cout << "Robot says: " << msg.value() << std::endl;
return BT::NodeStatus::SUCCESS;
}
};
注意:
- 最好只在tick中调用 getInput 方法
方式2:简单函数的实现
- takes an instance of
BT:TreeNode
as input - 传入Tree Node
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**---------------------------------------------------------------------------
* 创建可输入信息的Node 第二种创建方式:简单函数
*----------------------------------------------------------------------------*/
// Simple function
BT::NodeStatus SaySomethingSimple(BT::TreeNode& self) {
BT::Optional<std::string> msg = self.getInput<std::string>("message");
// Check if optional is valid. If not, throw its error
if (!msg) {
throw BT::RuntimeError("missing required input [message]: ", msg.error());
}
// use the method value() to extract the valid message.
std::cout << "Robot says: " << msg.value() << std::endl;
return BT::NodeStatus::SUCCESS;
}
注册Node
1
2
3
4
5
6
7
8
9
inline void RegisterNodes(BT::BehaviorTreeFactory& factory) {
// 对于第一种方式创建的Node, 使用如下方式注册
factory.registerNodeType<SaySomething>("SaySomething");
// 对于第二种方式创建的Node, 使用如下方式注册
BT::PortsList say_something_ports = {BT::InputPort<std::string>("message")};
factory.registerSimpleAction("SaySomethingSimple", SaySomethingSimple,
say_something_ports);
}
2. 返回参数 Output ports
创建树
1
2
3
4
5
6
7
8
9
10
11
static const char* xml_text = R"(
<root main_tree_to_execute = "MainTree" >
<BehaviorTree ID="MainTree">
<Sequence name="root_sequence">
<ThinkWhatToSay text="{the_answer}"/> //输出值到黑板中的键the_answer
<SaySomething message="get answer"/>
<SaySomething message="{the_answer}"/> // 读取黑板中的键the_answer
</Sequence>
</BehaviorTree>
</root>
)";
Node创建
使用setOutput将值写入到黑板中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**---------------------------------------------------------------------------
* 实现返回信息的Node
*----------------------------------------------------------------------------*/
class ThinkWhatToSay : public SyncActionNode {
public:
ThinkWhatToSay(const std::string& name, const NodeConfiguration& config)
: SyncActionNode(name, config) {}
static PortsList providedPorts() { return {OutputPort<std::string>("text")}; }
// This Action writes a value into the port "text"
NodeStatus tick() override {
// the output may change at each tick(). Here we keep it simple.
setOutput("text", "The answer is 42");
return NodeStatus::SUCCESS;
}
};
注册Node
1
2
3
4
inline void RegisterNodes(BT::BehaviorTreeFactory& factory) {
factory.registerNodeType<SaySomething>("SaySomething");
factory.registerNodeType<ThinkWhatToSay>("ThinkWhatToSay");
}
以上,比较简单。
Ch4. Generic Ports
Ports with generic types
- 在前面的教程中,我们介绍了输入和输出端口,端口的数据类型为 std::string
- 这种类型是最容易处理的,因为任何由xml传递的参数都自然而然是个string
- 接下来,我们介绍如何传递任意c++数据类型
- 对于通用简单数据类型,可以自动转换,因此重点研究用户自定义的数据类型
- 对于用户自定义数据类型,我们需要编写解析函数
- 该解析函数将具有一定格式的字符串转换为数据类型
数据
1
2
3
4
5
// We want to be able to use this custom type
struct Position2D {
double x;
double y;
};
为该类型创建解析方法
- 可以定义任何解析规则,只要是完备表达的即可
- 例如此处定义字符串规则为”x;y” “-1;3”, 以;作为分割
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Template specialization to converts a string to Position2D.
namespace BT
{
template <> inline Position2D convertFromString(StringView str)
{
// The next line should be removed...
printf("Converting string: "%s"\n", str.data() );
// We expect real numbers separated by semicolons
auto parts = splitString(str, ';');
if (parts.size() != 2)
{
throw RuntimeError("invalid input)");
}
else{
Position2D output;
output.x = convertFromString<double>(parts[0]);
output.y = convertFromString<double>(parts[1]);
return output;
}
}
} // end namespace BT
解析方法的调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class PrintTarget : public SyncActionNode {
public:
PrintTarget(const std::string& name, const NodeConfiguration& config)
: SyncActionNode(name, config) {}
static PortsList providedPorts() {
// Optionally, a port can have a human readable description
const char* description = "Simply print the goal on console...";
return {InputPort<Position2D>("target", description)};
}
NodeStatus tick() override {
auto res = getInput<Position2D>("target");
if (!res) {
throw RuntimeError("error reading port [target]:", res.error());
}
Position2D target = res.value();
printf("Target positions: [ %.1f, %.1f ]\n", target.x, target.y);
return NodeStatus::SUCCESS;
}
};
使用getInput方法获取输入,此时自动调用convertFromString,按照规则将字符串转换为用户自定义类型,使用.value()方法获取最终数据。
Ch5. Sequences
- https://www.behaviortree.dev/tutorial_04_sequence_star/
Sequence node 和 Reactive Sequence node 的区别与特点,前文已述
Ch6. Subtree
BehaviorTree是如何表达子树的呢?
- https://www.behaviortree.dev/tutorial_05_subtrees/
直接使用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
static const char* xml_text = R"(
<root main_tree_to_execute = "MainTree">
<!--------------------------------------->
<BehaviorTree ID="DoorClosed">
<Sequence name="door_closed_sequence">
<Inverter>
<Condition ID="IsDoorOpen"/>
</Inverter>
<RetryUntilSuccesful num_attempts="4">
<OpenDoor/>
</RetryUntilSuccesful>
<PassThroughDoor/>
</Sequence>
</BehaviorTree>
<!--------------------------------------->
<BehaviorTree ID="MainTree">
<Sequence>
<Fallback name="root_Fallback">
<Sequence name="door_open_sequence">
<IsDoorOpen/>
<PassThroughDoor/>
</Sequence>
<SubTree ID="DoorClosed"/>
<PassThroughWindow/>
</Fallback>
<CloseDoor/>
</Sequence>
</BehaviorTree>
<!--------------------------------------->
</root>
)";