行为树基本概念与BehaviorTree.CPP库在C++的代码实现
我从事一些机器人开发学习工作,深感使用状态机在复杂的状态逻辑工程中往往会导致最后的逻辑链条十分混乱,同时可拓展性也很差,往往会导致“牵一发而动全身”的繁杂过程,此外频繁的使用各种标志位也导致最后的代码可读性很差。为此我开展学习了拓展性更强的行为树来取代状态机,并记录一些基础知识和库的教程。 此外,为了加深对代码实践过程中的理解,我自己设计了一个简单的Demo来进一步加深对行为树的理解,具体可以看:基于BehaviorTree.CPP库的简单Demo实现
基本概念
行为树的节点按照大类可分为:根节点(Root)、行为节点(Action)、条件节点(Conditional)、组合节点(Composite)、装饰节点(Decorator)。其中,组合节点包括了选择节点与序列节点。常用的非叶节点有选择节点和序列节点。常用的叶节点有条件节点和动作节点 根节点Root:行为树的最顶部节点,也是入口节点 行为(动作)节点ActionNode:没有子节点,用以执行具体行为的节点,当动作完成后会返回“成功”;当动作无法完成会返回“失败”;如果动作正在进行中,会返回“运行中” 条件节点ConditionNode:条件节点代表了一个判断,没有子节点,条件满足会返回“成功”状态;否则返回“失败”状态。 组合节点用于连接多个子节点,比较常见的组合节点有 选择节点与 序列节点 选择节点Fallback 选择节点按照自左向右的顺序计算每个子节点,一旦某个子节点返回了“成功”或“运行中”的状态,那么选择节点就会立刻将自身的状态相应地更改为“成功”或“运行中”, 并不再执行后面的节点。选择节点类似于一种按照一定顺序判断的“或”逻辑,一旦存在一个成功执行的子节点,后续的子节点将不再执行。 序列节点Sequence 序列节点按照自左向右的顺序计算每个子结点,一旦某个子结点返回了“失败”或“运行中”的状态,那么序列节点就会立刻将自身的状态相应地更改为“失败”或“运行中”,并不再执行后面的节点。序列节点类似于“且”的逻辑,每个子节点按照顺序依次执行,一旦有一个子节点未能成功执行,后续的子节点将不会被调用。如果按照顺序所有的节点都返回了成功的状态,那么序列节点的状态对应被更新为“成功”。 装饰节点DecoratorNode:只有一个子节点,用以执行特定逻辑。 BehaviorTree.cpp在编译后生成的是一个静态库,可以将静态库链接到应用程序中。下面是库相关的主要概念。
MIT-BehaviorTree Lib官方教学(BehaviorTree.CPP)
官方文档
BehaviorTree.CPP行为树基本组成
节点与树 用户需要创建自己的动作节点与条件节点(叶节点);BTC会帮助构建最后的行为树。自定义节点具有(或应该具有)高度的可重用性。 tick()回调函数 任何 TreeNode 都可以看作是调用回调(即运行一段代码)的机制。至于这个回调会做什么,就由你自己决定了。在下面的大多数教程中,我们的 Actions 会简单地在控制台打印信息或休眠一定时间,以模拟长时间的计算。在实际代码中,尤其是在模型驱动开发和基于组件的软件工程中,动作/条件很可能会与系统的其他组件或服务通信。 - // The simplest callback you can wrap into a BT Action
- NodeStatus HelloTick()
- {
-
-
- std::cout << "Hello World\n";
- return NodeStatus::SUCCESS;
- }
- // Allow the library to create Actions that invoke HelloTick()
- // (explained in the tutorials)
- factory.registerSimpleAction("Hello", std::bind(HelloTick));
复制代码
通过继承创建自定义节点 在上述的例子中,一个调用HelloTick()函数的特定树节点通过函数指针的方式完成了创建。通常情况下,为了定义一个自定义的树节点,需要从TreeNode继承,或者从它的派生节点:
- ActionNodeBsae
- ConditionNode
- DecoratorNode
数据流,接口以及黑板(Dataflow,Ports and Blackboard)
- 黑板是一个被所有树节点共享的键值存储空间
- Ports是一种节点之间交换信息的机制
- 接口的数量、名字以及类型在C++编译时必须是已知的。
Tutorials 01 第一棵行为树
行为树与状态机类似,只不过是一种在适当的条件下适时调用回调的机制。在这些回调中会发生什么取决于你。将交替使用 "调用回调 "和 "to tick "这两个表述。在本系列教程中,大多数情况下我们的虚拟 Actions 只是在控制台上打印一些信息,但请记住,真正的 "生产 "代码可能会做一些更复杂的事情。接下来,我们将创建这棵简单的树: 推荐的创建行为树的方式是通过继承: - // Example of custom SyncActionNode (synchronous action)
- // without ports.
- 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;
- }
- };
复制代码
可以看到:
- 任意一个TreeNode的实例都有一个名称name,这个标签应当是具有可读性的,且不需要独一无二。
- tick()方法是放置真正需要执行的动作的位置,必须有一个NodeStatus的返回量,也即RUNNING,SUCCESS或者FAILURE
另外,我们也可以使用输入参数来创建一个给定函数指针(即 “functor”)的 TreeNode。其中的回调函数必须有这样的特征: - BT::NodeStatus myFunction(BT::TreeNode& self)
复制代码
例如: - using namespace BT;
- // Simple function that return a NodeStatus
- BT::NodeStatus CheckBattery()
- {
-
-
- std::cout << "[ Battery: OK ]" << std::endl;
- return BT::NodeStatus::SUCCESS;
- }
- // We want to wrap into an ActionNode the methods open() and close()
- class GripperInterface
- {
-
-
- public:
- GripperInterface(): _open(true) {
-
- }
-
- NodeStatus open()
- {
-
-
- _open = true;
- std::cout << "GripperInterface::open" << std::endl;
- return NodeStatus::SUCCESS;
- }
- NodeStatus close()
- {
-
-
- std::cout << "GripperInterface::close" << std::endl;
- _open = false;
- return NodeStatus::SUCCESS
复制代码 |