文章目录
链接:https://www.behaviortree.dev/docs/3.8/tutorial-advanced/asynchronous_nodes
原文链接:https://github.com/BehaviorTree/BehaviorTree.CPP/tree/3.8.0
一、前置理解
1)行为树介绍
2)主要概念介绍
3)XML主题介绍
二、基础篇教程
1)建立第一个行为树
- 先做一个顺序节点的行为树,结构图如下
(1)三种方式创建行为树节点(继承、依赖注入、定义类)
- (1)默认常规方法:
继承
去做
// Example of custom SyncActionNode (synchronous action)
// without ports.(port表示的意思是节点的传入、传出参数,这个ApproachObject节点没有传入传出参数)
class ApproachObject : public BT::SyncActionNode
{
public:
//构造函数,传入行为树节点名字
ApproachObject(const std::string& name) :
BT::SyncActionNode(name, {})
{}
// You must override the virtual function tick()
//需要重写tick虚函数,返回对应的节点状态(必须返回RUNNING, SUCCESS or FAILURE 这几个状态)
BT::NodeStatus tick() override
{
std::cout << "ApproachObject: " << this->name() << std::endl;
return BT::NodeStatus::SUCCESS;
}
};
- (2)第二种方法:
依赖注入
BT::NodeStatus myFunction()
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;
}
- (3)第三种方法:定义类方法来定义行为树节点(这里可以调用这个类的open和close)
// 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;
}
private:
bool _open; // shared information
};
(2)主main函数调用行为树节点(链接静态库用于调试,链接动态库用用于实际使用)
- 链接静态库方式调用节点(registering all the nodes one by one.)
int main()
{
//1、 We use the BehaviorTreeFactory to register our custom nodes
BehaviorTreeFactory factory;//这个是用来注册节点的
//2、
// Note: the name used to register should be the same used in the XML.
// Note that the same operations could be done using DummyNodes::RegisterNodes(factory)
using namespace DummyNodes;
// The recommended way to create a Node is through inheritance.
// Even if it requires more boilerplate, it allows you to use more functionalities
// like ports (we will discuss this in future tutorials).
factory.registerNodeType<ApproachObject>("ApproachObject");
// 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
GripperInterface gripper;
factory.registerSimpleAction("OpenGripper",
std::bind(&GripperInterface::open, &gripper));
factory.registerSimpleAction("CloseGripper",
std::bind(&GripperInterface::close, &gripper));
//3、跑行为树逻辑
// Trees are created at deployment-time (i.e. at run-time, but only once at the beginning).
// The currently supported format is XML.
// IMPORTANT: when the object "tree" goes out of scope, all the TreeNodes are destroyed
auto tree = factory.createTreeFromText(xml_text);
// To "execute" a Tree you need to "tick" it.
// The tick is propagated to the children based on the logic of the tree.
// In this case, the entire sequence is executed, because all the children
// of the Sequence return SUCCESS.
tree.tickRootWhileRunning();
}
- 链接动态库方式调用节点
int main()
{
//1、 We use the BehaviorTreeFactory to register our custom nodes
BehaviorTreeFactory factory;//这个是用来注册节点的
//2、
// Load dynamically a plugin and register the TreeNodes it contains
// it automated the registering step.
factory.registerFromPlugin("./libdummy_nodes_dyn.so");//(源码宏定义已经注册)
//3、跑行为树逻辑
// Trees are created at deployment-time (i.e. at run-time, but only once at the beginning).
// The currently supported format is XML.
// IMPORTANT: when the object "tree" goes out of scope, all the TreeNodes are destroyed
auto tree = factory.createTreeFromText(xml_text);
// To "execute" a Tree you need to "tick" it.
// The tick is propagated to the children based on the logic of the tree.
// In this case, the entire sequence is executed, because all the children
// of the Sequence return SUCCESS.
tree.tickRootWhileRunning();
}
(3)xml解析
<root main_tree_to_execute = "MainTree" >
<BehaviorTree ID="MainTree">
<Sequence name="root_sequence">
<CheckBattery name="battery_ok"/>
<OpenGripper name="open_gripper"/>
<ApproachObject name="approach_object"/>
<CloseGripper name="close_gripper"/>
</Sequence>
</BehaviorTree>
</root>
- 注释
1)
BehaviorTree ID
的作用是标识行为树的根节点,以便在执行行为树时从正确的节点开始执行。
2)Sequence
表示顺序节点
3)root main_tree_to_execute
表示tickRootWhileRunning()被调用的时候,执行BehaviorTree ID为MainTree的树逻辑,MainTree为根节点
- 原来的字符串
static const char* xml_text = R"(
<root main_tree_to_execute = "MainTree" >
<BehaviorTree ID="MainTree">
<Sequence name="root_sequence">
<CheckBattery name="battery_ok"/>
<OpenGripper name="open_gripper"/>
<ApproachObject name="approach_object"/>
<CloseGripper name="close_gripper"/>
</Sequence>
</BehaviorTree>
</root>
)";
这里的R表示:
xml_text 变量中括号前的 R 是原始字符串字面量的前缀,它表示字符串中的任何转义序列(例如 \n 表示换行符)都被视为字面字符而不是它们的特殊含义。在这种情况下,它允许将 XML 文本写成单个多行字符串,而无需转义任何字符。
(4)输出结果
- 注意
原来原来是链接linux动态库,这里需要改成链接win动态库,win的动态库需要和执行文件放一起
2)黑板数据和port
(1)黑板存在的作用(通过port来实现)
①给节点传递参数
②获得节点的输出信息
③把某个节点的输出信息作为输入信息传递给另外一个节点
(2)黑板的测试案例思维导图(这个黑板只存储了简单键值对)
(3)Input输入参数的ports举例使用
- 举例
1)main函数注册SaySomething函数
factory.registerNodeType<SaySomething>("SaySomething");
2)SaySomething这个类
class SaySomething : public BT::SyncActionNode
{
public:
SaySomething(const std::string& name, const BT::NodeConfiguration& config)
: BT::SyncActionNode(name, config)
{
}
// You must override the virtual function tick()
NodeStatus tick() override;
// It is mandatory to define this static method.
static BT::PortsList providedPorts()
{
return{ BT::InputPort<std::string>("message") };
}
};
这里的static BT::PortsList providedPorts()是强制性要求写成静态方式的;而且必须重写虚函数NodeStatus tick() override;
(4)输出参数的ports举例使用
- 原版xml
<root main_tree_to_execute = "MainTree" >
<BehaviorTree ID="MainTree">
<Sequence name="root">
<SaySomething message="hello" />
<SaySomething2 message="this works too" />
<ThinkWhatToSay text="{the_answer}"/>
<SaySomething2 message="{the_answer}" />
</Sequence>
</BehaviorTree>
</root>
- 这里的ThinkWhatToSay修改了text对应的the_answer的值,导致下面的the_answer也跟着变化
class ThinkWhatToSay : public BT::SyncActionNode
{
public:
ThinkWhatToSay(const std::string& name, const BT::NodeConfiguration& config) :
BT::SyncActionNode(name, config)
{}
// This Action simply write a value in the port "text"
BT::NodeStatus tick() override
{
setOutput("text", "The answer is 42");
return BT::NodeStatus::SUCCESS;
}
// A node having ports MUST implement this STATIC method
static BT::PortsList providedPorts()
{
return {BT::OutputPort<std::string>("text")};
}
};
(5)t02_basic_ports项目讲解
1)SaySomething在tick里面打印了hello
2)ThinkWhatToSay输出参数port到the_answer
3)这里由于SaySomething2之前对应了this works too,但是又因为ThinkWhatToSay修改了the_answer值,所以,SaySomething2的message会对应两个,打印(注意这里两个ThinkWhatToSay节点都是唯一
的,所以都会被打印两次
)
Robot says: this works too
Robot says: The answer is 42
3)黑板传递通用参数-port with generic types
(1)黑板能传递通用数据(能把字符串转成其他整数、bool、double或结构体
形式)
// We want to use this custom type
struct Position2D
{
double x;
double y;
};
- 为了从字符串实例化成
Position2D
结构体
需要提供模板实例化函数BT::convertFromString<Position2D>(StringView)
namespace BT
{
template <>
inline Position2D convertFromString(StringView str)
{
printf("Converting string: \"%s\"\n", str.data());
// 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
①注意这里的参数StringView
:既可以传递C++的string
也可以传递C的const char*
②库提供了简单的字符串分离函数splitString
,也可以使用boost::algorithm::split代替
③字符串转化成其他形式的参数,可以用convertFromString<double>()
(2)创建两个行为树节点,一个写一个读
CalculateGoal节点
:写port到黑板里面
class CalculateGoal: public SyncActionNode
{
public:
CalculateGoal(const std::string& name, const NodeConfiguration& config):
SyncActionNode(name,config)
{}
static PortsList providedPorts()
{
return { OutputPort<Position2D>("goal") };
}
NodeStatus tick() override
{
Position2D mygoal = {1.1, 2.3};
setOutput<Position2D>("goal", mygoal);
return NodeStatus::SUCCESS;
}
};
PrintTarget节点
:从黑板读port数据出来(打印这个)
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;
}
};
(3)t03_generic_ports项目讲解(整体顺序节点)
①通过类CalculateGoal传递GoalPosition这个Postion2D结构体到黑板里面
②调用节点PrintTarget,从黑板port读取GoalPosition这个Postion2D结构体并打印
Target positions: [ 1.1, 2.3 ]
③利用内部脚本,设置黑板参数把字符串转成OtherGoal,也就是调用函数
template <>
inline Position2D convertFromString(StringView str)
④读取OtherGola并转成Position2D打印出来
Target positions: [ -1.0, 3.0 ]
4)反射性行为和异步行为
(1)异步节点的标准要求:
①一个需要很长时间才能完成的行动,在没有达到完成标准时将返回RUNNING。
②不应该在tick()函数中阻塞太多时间。执行的流程应该尽可能快地返回。
③如果调用了halt()函数,应该尽可能快
地中止
它。
(2)状态同步节点StatefulActionNode
- 备注
①StatefulAsyncNode是实现异步动作节点的首选方式。
②当你的代码包含请求-回复模式时,即当动作向另一个进程发送异步请求,并定期检查是否收到回复时,它特别有用。
③根据该答复,它可能返回SUCCESS或FAILURE。
④如果你不与外部进程通信,而是进行一些需要很长时间的计算,你可能想把它分成小的 “块”,或者你可能想把该计算转移到另一个线程(see AsyncThreadedAction 教程) 必须重写以下方法:
①NodeStatus onStart()
:当节点处于IDLE状态时调用。它可能立即成功或失败,或者返回RUNNING。在后一种情况下,下次收到tick时,将执行onRunning方法。
②NodeStatus onRunning()
: 当节点处于RUNNING状态时调用。返回新的状态。
③void onHalted()
:当这个节点被树上的另一个节点中止时被调用。- 示例:创建一个名为MoveBaseAction的虚拟节点。
// Custom type
struct Pose2D
{
double x, y, theta;
};
namespace chr = std::chrono;
class MoveBaseAction : public BT::StatefulAsyncAction
{
public:
// Any TreeNode with ports must have a constructor with this signature
MoveBaseAction(const std::string& name, const BT::NodeConfig& config)
: StatefulAsyncAction(name, config)
{}
// It is mandatory to define this static method.
static BT::PortsList providedPorts()
{
return{ BT::InputPort<Pose2D>("goal") };
}
// this function is invoked once at the beginning.
BT::NodeStatus onStart() override;
// If onStart() returned RUNNING, we will keep calling
// this method until it return something different from RUNNING
BT::NodeStatus onRunning() override;
// callback to execute if the action was aborted by another node
void onHalted() override;
private:
Pose2D _goal;
chr::system_clock::time_point _completion_time;
};
//-------------------------
BT::NodeStatus MoveBaseAction::onStart()
{
if ( !getInput<Pose2D>("goal", _goal))
{
throw BT::RuntimeError("missing required input [goal]");
}
printf("[ MoveBase: SEND REQUEST ]. goal: x=%f y=%f theta=%f\n",
_goal.x, _goal.y, _goal.theta);
// We use this counter to simulate an action that takes a certain
// amount of time to be completed (200 ms)
_completion_time = chr::system_clock::now() + chr::milliseconds(220);
return BT::NodeStatus::RUNNING;
}
BT::NodeStatus MoveBaseAction::onRunning()
{
// Pretend that we are checking if the reply has been received
// you don't want to block inside this function too much time.
std::this_thread::sleep_for(chr::milliseconds(10));
// Pretend that, after a certain amount of time,
// we have completed the operation
if(chr::system_clock::now() >= _completion_time)
{
std::cout << "[ MoveBase: FINISHED ]" << std::endl;
return BT::NodeStatus::SUCCESS;
}
return BT::NodeStatus::RUNNING;
}
void MoveBaseAction::onHalted()
{
printf("[ MoveBase: ABORTED ]");
}
(3)顺序序列树和反射性顺序序列树
5)利用子树
6)port remapping
7)use multiple xml files
8)传递额外参数
三、进阶篇教程(异步流程)
四、Nodes Library
1)装饰器
①InverterNode
调用子节点一次,如果子节点失败,返回SUCCESS,如果子节点成功,返回FAILURE。如果子节点返回RUNNING,这个节点也返回RUNNING。
②ForceSuccessNode
调用子节点一次,如果子节点返回RUNNING,这个节点也返回RUNNING。否则,它总是返回SUCCESS。
③ForceFailureNode
如果子节点返回RUNNING,这个节点也返回RUNNING。否则,它总是返回FAILURE
④RepeatNode
只要子节点返回SUCCESS,就一直调用子节点,最多N次,其中N是作为输入端口传递的。如果子节点返回FAILURE,则中断循环,在这种情况下,也返回FAILURE。如果子节点返回RUNNING,这个节点也返回RUNNING
⑤RetryNode
只要子节点返回失败,就调用子节点,最多N次,其中N是作为输入端口传递的。如果子节点返回SUCCESS,则中断该循环,在这种情况下,也返回SUCCESS。如果子节点返回RUNNING,这个节点也返回RUNNING
2)回调函数(Fallbacks,有suc则为suc)
-
介绍
这个系列的节点在框架中被称为 "选择器 "或 “优先权”。它们的目的是尝试不同的策略,直到我们找到一个 "有效 "的策略。 -
目前,该框架提供了两种节点:
1)Fallback
2)ReactiveFallback -
规则:
1)在调用第一个子节点之前,节点状态变成RUNNING
2)如果一个子节点返回失败,回退时将会选择下一个子节点
3)如果最后一个子节点也返回FAILURE,那么所有的子节点都会被停止,回退也会返回FAILURE。
4)如果一个子节点返回SUCCESS,它就停止并返回SUCCESS。所有的子节点都将停止 -
例子
在这个例子中,我们尝试不同的策略来打开门。只会在最开始检查一次门是否打开
-
ReactiveFallback
这个控制节点用于当你想中断一个异步子节点,如果之前的一个条件将其状态从失败变为成功时。 -
ReactiveFallback例子
在下面的例子中,角色将睡眠长达8小时。如果他/她已经完全休息了,那么节点areYouRested? 将返回SUCCESS,异步节点Timeout(8小时)和Sleep将被中断。
3)顺序节点
-
规则
1)在执行第一个子节点之前,节点状态变成RUNNING。
2)如果一个子节点返回SUCCESS,它将执行下一个子节点。
3)如果最后一个子节点也返回SUCCESS,所有的子节点都被停止,序列返回SUCCESS -
该节点分类
1)Sequence
2)SequenceWithMemory
3)ReactiveSequence -
要了解这三个控制节点的区别,请参考下表:
1)Restart
意味着整个序列从列表的第一个子节点开始重新启动。
2)Tick again
意味着下一次序列被执行时,同一个孩子会被再次执行。已经返回成功的前一个兄弟节点不会再被执行。 -
行为树节点分类
1)序列Sequence节点:
顺序执行所有子节点,若都成功则返回成功,若有一个失败则返回失败
2)ReactiveSequence节点
- 介绍
这个节点对连续检查条件特别有用;但用户在使用异步子节点时也应该小心,以确保它们不会被调用的次数超过预期。 - 例子
ApproachEnemy 是一个异步动作,它返回RUNNING,直到它最终完成。条件isEnemyVisible将被多次调用,如果它变成false(即 “failure”),ApproachEnemy将被停止
3)SequenceWithMemory节点
当你不想再次调用已经返回SUCCESS的孩子时,请使用这个控制节点
这是一个巡逻机器人,必须只访问一次地点A、B和C。如果行动GoTo(B)失败,GoTo(A)将不会被再次调用