(为免误导,特免责声明如下:本文所有内容,只是基于个人当前理解和实际做法,后面亦会有更正和修订,但任何版本都不免有个人能力有限、理解有误或者工作环境不同的状况,故文中内容仅供参考。任何人都可以借鉴或者直接使用代码片段,但对任何直接引用或者借鉴产生的技术问题等后果,作者不承担任何责任。)
- 简介
这部分内容是讲述:行为树节点之间如何传递参数(或者从xml传入)和交换数据的。
- 黑板
黑板是所有节点都可以访问的键/值存储的共享信息。
与黑板的接入形式就是键值对。
节点的输入端口可以从黑板读取数据,节点的输出端口可以写入黑板。
如上图中,行为节点ThinkWhatToSay有个输出端口,会写入the_answer:“xxxx”, 然后另外一个行为节点SaySomething有个输入端口,会从黑板中读取这个键值对的数据。
-
输入端口 inputs ports
输入端口可以是:- 静态的字符串,节点可以读取和解析
- 一个指向黑板的指针,被当做键值对的键(key)
比如行为节点SaySomething需要打印出一个字符串,为了传递这个字符串,我们用了一个叫做message的端口。
xml文件里可以写成下面的任何一种:
<SaySomething name="fist" message="hello world" />
<SaySomething name="second" message="{greetings}" />
第一种就是直接读取字符串,本例为“hello world”
第二种是指向了黑板的,greetings这个键,需要再运行时候读取黑板内容。
SaySomething的代码实现如下:
// SyncActionNode (synchronous action) with an input port.
class SaySomething : public SyncActionNode
{
public:
// If your Node has ports, you must use this constructor signature
SaySomething(const std::string& name, const NodeConfig& config)
: SyncActionNode(name, config)
{ }
// It is mandatory to define this STATIC method.
static PortsList providedPorts()
{
// This action has a single input port called "message"
return { InputPort<std::string>("message") };
}
// Override the virtual function tick()
NodeStatus tick() override
{
Expected<std::string> msg = getInput<std::string>("message");
// Check if expected 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 NodeStatus::SUCCESS;
}
};
When a custom TreeNode has input and/or output ports, these ports must be declared in the static method:
当自定义的树节点需要用到输入输出端口的时候,这些端口必须声明在下面这个静态函数里:
static MyCustomNode::PortsList providedPorts();
使用输入端口(这里是message)进行读取的时候,可以用模板方法:
TreeNode::getInput(key).
这个方法有可能失败,用户可以根据需要进行处理,比如:返回错误、抛出异常、或者使用缺省值等。
建议在tick里面调用getInput(),而不是在类的构造函数中,因为真实值可能在运行中被修改,所以要定期去更新。
- 输出端口Output ports
当输入端口指向黑板入口时,必须先有节点通过输出端口已经在该入口写入了一些数据,才会有效。
下例中,ThinkWhatToSay这个节点就是通过输出端口往text这个入口写字符串的。
class ThinkWhatToSay : public SyncActionNode
{
public:
ThinkWhatToSay(const std::string& name, const NodeConfig& 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;
}
};
Alternatively, most of the time for debugging purposes, it is possible to write a static value into an entry using the built-in Actions called Script.
另外,很多时候为了调试,可以在xml文件里用脚本(Script)往黑板写一个静态的数值。脚本相当于执行了一个内建的动作。
<Script code=" the_answer:='The answer is 42' " />
- 完整的例子
<root BTCPP_format="4" >
<BehaviorTree ID="MainTree">
<Sequence name="root_sequence">
<SaySomething message="hello" />
<ThinkWhatToSay text="{the_answer}"/>
<SaySomething message="{the_answer}" />
</Sequence>
</BehaviorTree>
</root>
In this example, a Sequence of 3 Actions is executed:
本例中一个顺序控制节点有3个行动子节点会被执行。
- Action 1 从静态字符串中读取message.
- Action 2 通过输出端口text,写入数据到黑板入口:the_answer.
- Action 3 从黑板入口:the_answer,读取到输入端口:message .
#include "behaviortree_cpp/bt_factory.h"
// file that contains the custom nodes definitions
#include "dummy_nodes.h"
using namespace DummyNodes;
int main()
{
BehaviorTreeFactory factory;
factory.registerNodeType<SaySomething>("SaySomething");
factory.registerNodeType<ThinkWhatToSay>("ThinkWhatToSay");
auto tree = factory.createTreeFromFile("./my_tree.xml");
tree.tickWhileRunning();
return 0;
}
/* Expected output:
Robot says: hello
Robot says: The answer is 42
*/
连接输入与输出端口的是相同的键(Key):the_answer, 就是说指向黑板的相同入口。
这些端口可以相连是因为他们的类型都是相同的,这里都是字符串,如果类型不通在factory.createTreeFromFile 的时候会抛出异常。
- 通用类型端口
解析字符串: 除了前面例子里的string类型,BehaviorTree.CPP 支持自动把字符串转换为通用的类型,比如int,long,double,bool,NodeStatus等等。
而用户自定义类型也可以简单的获得支持。
比如有个自定义的类型:
// We want to use this custom type
struct Position2D
{
double x;
double y;
};
To allow the XML loader to instantiate a Position2D from a string, we need to provide a template specialization of BT::convertFromString(StringView).
为了xml读取器能从字符串中实例化Position2D,我们需要提供一个模板特例化:
BT::convertFromString(StringView)
这个取决于你是如何把Position2D序列化(serialized )成字符串的,本例中,我们简单的把他用分号分成2个数字。
// Template specialization to converts a string to Position2D.
namespace BT
{
template <> inline Position2D convertFromString(StringView str)
{
// 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
StringView是一个 C++11版的std::string_view. 你也可以传递std::string 或者 const char*. 库中提供了简单的字符串分割函数splitString.也可以随便用其他,比如: boost::algorithm::split.
通用类型的话会直接调用特例化,比如转换成double,convertFromString().
- 实例
class CalculateGoal: public SyncActionNode
{
public:
CalculateGoal(const std::string& name, const NodeConfig& 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;
}
};
class PrintTarget: public SyncActionNode
{
public:
PrintTarget(const std::string& name, const NodeConfig& 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;
}
};
现在可以正常连接输入输出端口,都指向黑板相同的入口。
下例中树是一个顺序控制,有4个行为子节点:
- 用行为CalculateGoal存储一个Position2D 到黑板入口:GoalPosition
Call PrintTarget. The input “target” will be read from the Blackboard entry GoalPosition. - 调用PrintTarget,输入端口target会读取黑板入口:GoalPosition
- 采用内建行为脚本,指定字符串"-1;3" 到黑板的键:OtherGoal,字符串到Position2D 的转换会自动进行。
- 再次调用PrintTarget,输入端口读取的黑板入口是:OtherGoal
static const char* xml_text = R"(
<root BTCPP_format="4" >
<BehaviorTree ID="MainTree">
<Sequence name="root">
<CalculateGoal goal="{GoalPosition}" />
<PrintTarget target="{GoalPosition}" />
<Script code=" OtherGoal:='-1;3' " />
<PrintTarget target="{OtherGoal}" />
</Sequence>
</BehaviorTree>
</root>
)";
int main()
{
BT::BehaviorTreeFactory factory;
factory.registerNodeType<CalculateGoal>("CalculateGoal");
factory.registerNodeType<PrintTarget>("PrintTarget");
auto tree = factory.createTreeFromText(xml_text);
tree.tickWhileRunning();
return 0;
}
/* Expected output:
Target positions: [ 1.1, 2.3 ]
Converting string: "-1;3"
Target positions: [ -1.0, 3.0 ]
*/
这次树结构式定义在字符串(语法和xml一样)中的,用createTreeFromText读取。
★ 在运行时的共享是在内存中的,类型转换可以自动完成,需要指定特例化函数是因为需要从xml字符串中读取自定义类型的对象数据。