Ports with generic types
In the previous tutorials we introduced input and output ports, where the
type of the port was a std::string
.
Next, we will show how to assign generic C++ types to your ports.
Parsing a string
BehaviorTree.CPP supports the automatic conversion of strings into common
types, such as int
, long
, double
, bool
, NodeStatus
, etc.
User-defined types can be supported easily as well.
For instance:
// 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<Position2D>(StringView)
.
It is up to you how Position2D
is serialized into a string; in this case,
we simply separate two numbers with a semicolon.
// 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
is a C++11 version of std::string_view. You can pass either astd::string
or aconst char*
.- The library provides a simple
splitString
function. Feel free to use another one, like boost::algorithm::split. - We can use the specialization
convertFromString<double>()
.
Example
As we did in the previous tutorial, we can create two custom Actions, one will write into a port and the other will read from a 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;
}
};
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;
}
};
We can now connect input/output ports as usual, pointing to the same entry of the Blackboard.
The tree in the next example is a Sequence of 4 actions:
Store a value of
Position2D
in the entry GoalPosition, using the actionCalculateGoal
.Call
PrintTarget
. The input "target" will be read from the Blackboard entry GoalPosition.Use the built-in action
SetBlackboard
to assign the string "-1;3" to the key OtherGoal. The conversion from string toPosition2D
will be done automatically.Call
PrintTarget
again. The input "target" will be read from the entry OtherGoal.
static const char* xml_text = R"(
<root main_tree_to_execute = "MainTree" >
<BehaviorTree ID="MainTree">
<Sequence name="root">
<CalculateGoal goal="{GoalPosition}" />
<PrintTarget target="{GoalPosition}" />
<SetBlackboard output_key="OtherGoal" value="-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.tickRootWhileRunning();
return 0;
}
/* Expected output:
Target positions: [ 1.1, 2.3 ]
Converting string: "-1;3"
Target positions: [ -1.0, 3.0 ]
*/