原文地址:https://www.behaviortree.dev/docs/intro
BehaviorTree.CPP行为树库地址:GitHub - BehaviorTree/BehaviorTree.CPP: Behavior Trees Library in C++. Batteries included.
一.关于
1.关于这个库
这个 C++库提供了一个创建行为树的框架。它设计得灵活、易用且快速。
即使我们的主要用例是机器人技术,您也可以使用此库来 为游戏构建人工智能,或者在您的应用程序中替换有限状态机。
与其他实现相比,BehaviorTree.CPP具有许多有趣的特性:
- 它使异步操作(即非阻塞例程)成为头等公民。
- 树是在运行时使用解释语言(基于 XML)创建的。
- 它包括一个日志/分析基础设施,允许用户可视化、记录、重放和分析状态转换。
- 您可以静态链接自定义的 TreeNode 或将它们转换为在运行时加载的插件。
2.什么是行为树?
行为树 ( BT ) 是一种构建自主智能体(例如机器人或计算机游戏中的虚拟实体)中不同任务之间切换的方式。
BT 是一种非常有效的创建复杂系统的方法,这些系统既模块化又具有响应性。这些特性在许多应用中都至关重要,这使得 BT 从计算机游戏编程扩展到人工智能和机器人技术的许多分支。
如果您已经熟悉有限状态机(FSM),那么您将很容易掌握大部分概念,但希望您会发现 BT 更具表现力且更容易推理。
把树的节点看作一组构建块。这些块用 C++ 实现,并且是“可组合的”:换句话说,它们可以“组装”以构建行为。
3.行为树的主要优点
-
它们本质上是分层的:我们可以组合 复杂的行为,包括将整个树作为更大树的子分支。例如,“取啤酒”行为可以重用“抓取对象”树。
-
它们的图形表示具有语义含义:更容易“阅读” BT 并理解相应的工作流程。相比之下,FSM 中的状态转换无论是在文本还是图形表示中都更难理解。
-
它们更具表现力:现成的 ControlNodes(控制节点) 和 DecoratorNodes(装饰节点) 可以表达更复杂的控制流。用户可以使用自己的自定义节点扩展“词汇表”。
4.为什么我们需要行为树(或 FSM)?
许多软件系统(机器人就是一个显著的例子)本质上都是复杂的。
管理复杂性、异构性和可扩展性的通常方法是使用基于组件的软件工程的概念 。
任何现有的机器人中间件都以非正式或正式的方式采用了这种方法,其中ROS、YARP和 SmartSoft就是一些著名的例子。
一个“好的”软件架构应该具备以下特点:
- 模块化。
- 组件的可重用性。
- 可组合性。
- 很好地分离关注点。
如果我们从一开始就不牢记这些概念,我们创建的软件就会紧密耦合、可重用性较差。
通常,软件系统的业务逻辑会“分散”到许多组件中,开发人员很难对其进行推理和调试错误。
为了实现强有力的关注点分离,最好将业务逻辑集中在一个位置。
有限状态机就是为了实现这个目标而创建的,但近年来, 行为树变得越来越流行,尤其是在游戏行业。
二.基本概念
1.简介
与有限状态机不同,行为树是 控制“任务”执行流程的分层节点树。
基本概念
-
一个称为“滴答”的信号被发送到树的根部,并在树中传播,直到到达叶节点。
-
任何收到tick信号的 TreeNode 都会执行其回调。此回调必须返回
- SUCCESS
- FAILURE
- RUNNING
-
RUNNING 表示该操作需要更多时间才能返回有效结果。
-
如果 TreeNode 具有一个或多个子节点,则它有责任传播tick;每种节点类型可能对于是否、何时以及tick子节点的次数有不同的规则。
-
叶节点(LeafNodes)是那些没有任何子节点的树节点,它们是实际的命令,即行为树与系统其余部分交互的节点。 动作节点(Actionnodes)是最常见的叶节点类型。
tick这个词经常用作动词(滴答/被滴答),其意思是:
"调用' TreeNode '的' tick() '回调"。
在面向服务的架构中,叶子节点包含与执行实际操作的“服务器”进行通信的“客户端”代码。
tick的工作原理
为了直观地了解勾选树的工作原理,请看下面的图片。

Sequence是最简单的ControlNode:它依次执行其子节点,如果所有子节点都成功,它也会返回 SUCCESS 。
- 第一个tick 将 Sequence 节点设置为 RUNNING(橙色)。
- Sequence tick第一个子项“OpenDoor”,最终返回 SUCCESS。
- 结果,第二个子项“Walk”和后来的“CloseDoor”都被tick了。
- 一旦最后一个子任务完成,整个序列就会从 RUNNING 切换到 SUCCESS。
节点类型

| 树节点的类型 | 子节点数量 | 注意事项 |
|---|---|---|
| ControlNode | 1...N | 通常,根据兄弟姐妹的结果或/和其自身的状态来tick一个子节点。 |
| DecoratorNode | 1 | 除其他事情外,它可能会改变其子项的结果或多次tick它。 |
| ConditionNode | 0 | 不应改变系统。不应返回 RUNNING。 |
| ActionNode | 0 | 这是“做一些事情”的节点 |
在ActionNodes上下文中,我们可以进一步区分同步节点和异步节点。
前者以原子方式执行并阻止树直到返回 SUCCESS 或 FAILURE。
相反,异步操作可能会返回 RUNNING 来表明该操作仍在执行。
我们需要再次tick它们,直到最终返回SUCCESS或FAILURE。
示例
为了更好地理解行为树的工作原理,让我们关注一些实际示例。为了简单起见,我们不会考虑当动作返回 RUNNING 时会发生什么。
我们将假设每个动作都是原子且同步执行的。
第一个控制节点:Sequence
让我们使用最基本和最常用的 ControlNode(即SequenceNode)来说明 BT 的工作原理。
ControlNode 的子节点始终是有序的;在图形表示中,执行顺序是从左到右。
简而言之:
- 如果子节点回答“SUCCESS”,则tick下一个。
- 如果一个子项返回 FAILURE,则不再tick任何子项,并且序列返回 FAILURE。
- 如果所有子项都返回 SUCCESS,则 Sequence 也返回 SUCCESS。
找到BUG:
如果GrabBeer操作失败,由于跳过了最后一个CloseFridge操作,冰箱门将保持打开状态。
装饰器
根据DecoratorNode的类型,此节点的目标可以是:
- 来转化从子节点获得的结果。
- 停止对子节点的运行。
- 根据装饰器的类型来重复tick子项。

节点Inverter是一个装饰器,它反转其子节点返回的结果;因此,后面跟着 isDoorOpen节点的 Inverter相当于
"Is the door closed?".(门是否是关闭的?)
如果子节点返回 FAILURE,则Retry节点将重复对子节点进行tick,最多可达num_attempts次(在本例中为 5 次)。
显然,左侧的分支意味着:
If the door is closed, then try to open it.(如果门是关闭的,就尝试打开它)
Try up to 5 times, otherwise give up and return FAILURE.(尝试最多5次,否则就放弃且返回FAILURE)
但...
找到BUG:
如果isDoorOpen返回 FAILURE,则我们得到了所需的行为。但如果它返回 SUCCESS,则左侧分支失败,并且整个 Sequence 被中断。
第二个控制节点:Fallback
FallbackNodes,也称为“选择器”,是可以表达后备策略的节点,顾名思义,即如果子节点返回 FAILURE ,下一步该做什么。
它按顺序tick子项,并且:
- 如果子节点回答“FAILURE ”,则tick下一个。
- 如果子进程返回 SUCCESS,则不再tick其他子节点,并且 Fallback 返回 SUCCESS。
- 如果所有子项都返回 FAILURE,则 Fallback 也会返回 FAILURE。
在下一个示例中,您可以看到如何组合Sequences和Fallbacks:

Is the door open?门开着吗?
If not, try to open the door.如果没有,请尝试打开门。
Otherwise, if you have a key, unlock and open the door.否则,如果您有钥匙,请解锁并打开门。
Otherwise, smash the door.否则就砸门。
If any of these actions succeeded, then enter the room.如果任何这些操作成功,则进入房间。
复习"Fetch me a beer"例子
我们现在可以改进“给我拿一杯啤酒”的例子,如果啤酒不在冰箱里,冰箱门就会打开。
我们使用“绿色”表示返回 SUCCESS 的节点,使用“红色”表示返回 FAILURE 的节点。黑色节点表示未执行。

让我们创建一个替代树,即使GrabBeer 返回 FAILURE,它也会关闭门。

这两棵树最终都会关上冰箱门,但是:
- 无论我们是否真的拿到了啤酒, 左侧的树总会返回 SUCCESS。
- 如果有啤酒,右侧的树将返回 SUCCESS,否则将返回 FAILURE。
如果GrabBeer返回 SUCCESS,则一切都按预期工作。

2.主要概念
BehaviorTree.CPP是一个 C++ 库,可以轻松集成到您最喜欢的分布式中间件中,例如ROS或SmartSoft。
您可以将其静态链接到您的应用程序(例如游戏)。
这些是您首先需要了解的主要概念。
节点与树
用户必须创建自己的ActionNodes和ConditionNodes(LeafNodes);这个库可以帮助您轻松地将它们组合成树。
把 LeafNodes 视为构建复杂系统所需的构建块。如果 Nodes 是乐高积木,那么你的树就是乐高积木。

在运行时使用XML格式实例化树
尽管该库是用 C++ 编写的,但树本身可以在运行时(更具体地说,在部署时)使用基于 XML 的脚本语言创建和编写。
tick() callback
任何 TreeNode 都可以看作是一种调用callback的机制,即 运行一段代码。这个callback的作用由你决定。
在大多数以下教程中,我们的操作只会在控制台上打印消息或睡眠一段时间以模拟长时间计算。
在生产代码中,尤其是在模型驱动开发和基于组件的软件工程中,(Action/Condition)动作/条件可能会与系统的其他组件或服务进行通信。
//可以封装到BT Action中的最简单的回调
NodeStatus HelloTick()
{
std::cout << "Hello World\n";
return NodeStatus::SUCCESS;
}
//允许库创建调用HelloTick()的Actions(在教程中解释)
factory.registerSimpleAction("Hello", std::bind(HelloTick));
提示:
factory可能会创建节点Hello的多个实例。
使用继承创建自定义节点
在上面的例子中, 使用函数指针(依赖注入)HelloTick创建了一种调用特定类型的 TreeNodes。
通常,要定义自定义 TreeNode,您应该从类继承TreeNode,或者更具体地说,从其派生类继承:
ActionNodeBaseConditionNodeDecoratorNode
数据流、端口和黑板(Dataflow, Ports and Blackboard)
目前,重要的是要知道:
-
黑板(Blackboard)是树的所有节点共享的键/值存储。
-
端口是节点之间可以用来交换信息的一种机制。
-
端口使用黑板上的同一个键进行“连接” 。
-
节点的端口数量、名称和类型必须在编译时(C++)知道;端口之间的连接在部署时完成(XML)。
-
您可以将任何 C++ 类型存储为值(我们使用类似于std::any 的_类型擦除技术)。
3.XML简介
下面是一颗简单的树
<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>
您可能会注意到:
-
树的第一个标签是
<root>。它应该包含1 个或多个标签<BehaviorTree>。 -
该标签
<BehaviorTree>应该具有属性[ID]。 -
标签
<root>应该包含属性[BTCPP_format]。 -
每个 TreeNode 都由一个标签表示。具体来说:
- 标签的名称是用于在factory中注册TreeNode的ID 。
- 该属性
[name]指的是实例的名称,是可选的。 - 端口使用属性进行配置。在上例中,操作
SaySomething需要输入端口message。
-
就子节点数量而言:
ControlNodes包含1 到 N 个孩子。DecoratorNodes和子树仅包含1 个子树。ActionNodes和Cond



被折叠的 条评论
为什么被折叠?



