(翻译)BehaviorTree.CPP教程二:基本端口:https://www.behaviortree.dev/tutorial_02_basic_ports/
输入和输出端口
正如我们前面所解释的,自定义 TreeNodes 可用于执行任意简单或复杂的软件。他们的目标是提供一个具有更高抽象层次的接口。
因此,它们在概念上与函数没有不同。
与函数类似,我们通常希望:
- 将参数/参数传递给节点(输入)
- 从节点(输出)获取某种信息。
- 一个节点的输出可以是另一个节点的输入。
BehaviorTree.CPP提供了一种通过端口的数据流的基本机制,它简单易用,而且类型又灵活又安全。
输入端口
一个有效的输入可以是:
- 可以由节点解析的静态字符串,或
- 指向 Blackboard 入口的“指针”,由一个 key 所标示。
一个 blackboard 是由树的所有节点共享的一个简单 key / value 存储。
Blackboard 的一个 entry 是一个 key / value 对。
输入端口可以读取 Blackboard 中的 entry ,而输出端口可以写入 entry 。
假设我们要创建一个名为 SaySomething 的 ActionNode ,它应该在 std::cout 上打印给定的字符串。
此类字符串将使用名为 message 的输入端口传递。
请考虑以下替代 XML 语法:
<SaySomething name="first" message="hello world" />
<SaySomething name="second" message="{greetings}" />
第一个节点中的属性消息表示:
"静态字符串'hello world'被传递到'SaySomething'的端口'message' "
消息是从XML文件中读取的,因此不能在运行时更改。
第二个节点的语法表示:
“读取 blackboard 上名为‘greetings’的 entry 的当前值”。
此值可以在运行时更改(而且可能会更改)。
ActionNode SaySomething 可以实现如下:
// SyncActionNode(同步动作)带有输入端口。
class SaySomething : public SyncActionNode
{
public:
// 如果您的节点有端口,则必须使用此构造函数署名
SaySomething(const std::string& name, const NodeConfiguration& config)
: SyncActionNode(name, config)
{ }
// 定义这个静态方法是强制性的。
static PortsList providedPorts()
{
// 此操作有一个称为“message”的输入端口。
// 任何端口必须有一个名称。类型可选。
return { InputPort<std::string>("message") };
}
// 像往常一样,您必须重写虚函数tick()
NodeStatus tick() override
{
Optional<std::string> msg = getInput<std::string>("message");
// 检查optional是否有效。如果没有,抛出它的错误
if (!msg)
{
throw BT::RuntimeError("missing required input [message]: ",
msg.error() );
}
// 使用方法value()提取有效消息。
std::cout << "Robot says: " << msg.value() << std::endl;
return NodeStatus::SUCCESS;
}
};
或者,可以在一个简单的函数中实现相同的功能。这个函数以BT:TreeNode的实例作为输入,以便访问“message”输入端口:
// 返回节点状态的简单函数
BT::NodeStatus SaySomethingSimple(BT::TreeNode& self)
{
Optional<std::string> msg = self.getInput<std::string>("message");
// 检查optional是否有效。如果没有,抛出它的错误
if (!msg)
{
throw BT::RuntimeError("missing required input [message]: ", msg.error());
}
// 使用方法value()提取有效消息。
std::cout << "Robot says: " << msg.value() << std::endl;
return NodeStatus::SUCCESS;
}
当一个自定义TreeNode有输入和/或输出端口时,这些端口必须在静态方法中声明:
static MyCustomNode::PortsList providedPorts();
可以使用模板方法 message
TreeNode::getInput<T>(key)
读取来自端口的输入。
此方法可能由于多种原因而失败。它由用户来检查返回值的有效性并决定做什么:
- 返回?
NodeStatus::FAILURE
- 抛出异常?
- 使用其他默认值?
重点注意:
始终建议在tick()中调用getInput()方法,而不是在类的构造函数中。
c++代码不能对输入的性质做出任何假设,输入可以是静态的,也可以是动态的。动态输入可以在运行时更改,因此应该定期读取它。
输出端口
指向 blackboard 入口的输入端口只有在另一个节点已经在同一 entry 中写入了“something”时才有效。
ThinkWhatToSay 是使用输出端口将字符串写入 entry 的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") };
}
// 该操作将一个值写入端口“text”中。
NodeStatus tick() override
{
// 每次tick()的输出都可能发生变化。这里我们保持简洁。
setOutput("text", "The answer is 42" );
return NodeStatus::SUCCESS;
}
};
或者,出于调试目的,大多数时间,可以使用称为 SetBlackboard 的内置操作将静态值写入条目。
<SetBlackboard output_key="the_answer" value="The answer is 42" />
一个完整的示例
在此示例中,将执行 5 个操作的序列:
-
操作 1 和 4 读取来自静态字符串的输入
消息。
-
操作 3 和 5 读取黑板中名为 the_answer 的条目的输入
消息。
-
动作2在黑板的条目中写上一些叫做
the_answer
的东西。
saything2是一个SimpleActionNode(简单行为节点)。
<root main_tree_to_execute = "MainTree" >
<BehaviorTree ID="MainTree">
<Sequence name="root_sequence">
<SaySomething message="start thinking..." />
<ThinkWhatToSay text="{the_answer}"/>
<SaySomething message="{the_answer}" />
<SaySomething2 message="SaySomething2 works too..." />
<SaySomething2 message="{the_answer}" />
</Sequence>
</BehaviorTree>
</root>
C++代码:
#include "behaviortree_cpp_v3/bt_factory.h"
// 包含自定义节点定义的文件
#include "dummy_nodes.h"
int main()
{
using namespace DummyNodes;
BehaviorTreeFactory factory;
factory.registerNodeType<SaySomething>("SaySomething");
factory.registerNodeType<ThinkWhatToSay>("ThinkWhatToSay");
// SimpleActionNodes不能定义自己的方法providedPorts()。
// 如果我们想让动作能够使用getInput()或setOutput(),我们应该显式传递一个PortsList;
PortsList say_something_ports = { InputPort<std::string>("message") };
factory.registerSimpleAction("SaySomething2", SaySomethingSimple,
say_something_ports );
auto tree = factory.createTreeFromFile("./my_tree.xml");
tree.tickRoot();
/* Expected output:
Robot says: start thinking...
Robot says: The answer is 42
Robot says: SaySomething2 works too...
Robot says: The answer is 42
*/
return 0;
}
我们使用相同的键(the_aswer)将输出端口连接到输入端口;这意味着它们“指向”黑板上的同一个条目。
这些端口可以相互连接,因为它们的类型是相同的,即std::string。