Behavior tree -- [4] simple tree

Keywords: C++

This article describes how to build a simple tree to experience the use of the following running script tree.

An understanding of tick

Suppose you want to do two things in order, find the goal and grasp the goal (you can't grasp it until you find it first). The plan is as follows,

PS: the top two layers can be regarded as strategy (general planning), and the bottom layer can be regarded as tactics (specific how to do it)

A tick can be regarded as a trigger signal. When a tick signal is sent, go down from Sequence, first to detectobject, then to ObjectRecognition Component, and return to Sequence after execution. The Sequence will continue to execute according to the execution results of detectobject.

If the detectobject is executed successfully, it will continue to GraspObject, and finally to the control component. After execution, it will return to Sequence. If the detectobject execution fails, the Sequence execution is considered to be over.

It should be noted that only one tick signal is required for the above operations, and the node s will automatically tick according to the situation.

A more vivid example of a tick should be a Newton pendulum,

Two tree planning

The tree uses xml format for strategic planning, opening the door - > entering the house - > closing the door,

<root main_tree_to_execute = "MainTree" >

     <BehaviorTree ID="MainTree">
        <Sequence name="root_sequence">
            <OpenDoor   name="open_door_of_house"/>
            <EnterHouse name="enter_house"/>
            <CloseDoor  name="close_door_of_house"/>
        </Sequence>
     </BehaviorTree>

 </root>

The xml format has the following points,

  • The root tag is required, and it needs to have main_ tree_ to_ The attribute of execute indicates which tree to execute
  • The child element of root must have BehaviorTree, and BehaviorTree must have ID attribute
  • If root has more than one BehaviorTree, the attribute value of the ID of the BehaviorTree must be different
  • If the root has only one BehaviorTree, then main_ tree_ to_ The execute attribute is optional
  • The child element of BehaviorTree is tree nodes

The tree node type here is Sequence, which contains three children.

The characteristic of the Sequence tree node is that its children must all return SUCCESS before the execution is considered successful. If a child returns FAILURE, the execution of the tree node will fail.

  • Before ticking the first child, the node status becomes RUNNING.
  • If a child returns SUCCESS, it ticks the next child.
  • If the last child returns SUCCESS too, all the children are halted and the sequence returns SUCCESS.

There are three types of Sequence nodes,

  • Sequence (same name)
  • SequenceStar
  • ReactiveSequence

Their differences are as follows,

  • Restart means to restart the entire tree from the first child
  • Tick again means that the next time a tick signal is received, only the children that have failed in the current run will be executed, and the children that have successfully run before will not run again

Three codes

It is emphasized that xml is responsible for planning the execution logic (strategy), and the specific execution process is provided by the user (tactics).

The first way

First, you need to create an object of BehaviorTreeFactory to register node s,

    // We use the BehaviorTreeFactory to register our custom nodes
    BT::BehaviorTreeFactory factory;

Then register user-defined tactics, that is, provide user-defined operations for opening, entering and closing the door,

    factory.registerSimpleAction("OpenDoor", std::bind(OpenDoorFunc));
    factory.registerSimpleAction("EnterHouse", std::bind(EnterHouseFunc));
    factory.registerSimpleAction("CloseDoor", std::bind(CloseDoorFunc));

These tactics will be found according to the name (the first parameter) during execution, so the name must be consistent with the tag name of child in xml.

Then there is the loading strategy,

auto tree = factory.createTreeFromText(xml_text);

Here, xml is written in the code as a string. If there is an xml file, another interface is used,

auto tree = factory.createTreeFromFile(xml_file);

Finally, root will tick,

tree.tickRoot();

The overall code is as follows,

#include "behaviortree_cpp_v3/bt_factory.h"


static const char* xml_text = R"(

<root main_tree_to_execute = "MainTree" >

     <BehaviorTree ID="MainTree">
        <Sequence name="root_sequence">
            <OpenDoor   name="open_door_of_house"/>
            <EnterHouse name="enter_house"/>
            <CloseDoor  name="close_door_of_house"/>
        </Sequence>
     </BehaviorTree>

 </root>
 )";


BT::NodeStatus OpenDoorFunc()
{
    std::cout << "Door is opened" << std::endl;
    return BT::NodeStatus::SUCCESS;
}

BT::NodeStatus EnterHouseFunc()
{
    std::cout << "Enter house" << std::endl;
    return BT::NodeStatus::SUCCESS;
}

BT::NodeStatus CloseDoorFunc()
{
    std::cout << "Close door" << std::endl;
    return BT::NodeStatus::SUCCESS;
}


int main()
{
    // We use the BehaviorTreeFactory to register our custom nodes
    BT::BehaviorTreeFactory factory;


    // Registering a SimpleActionNode using a function pointer.
    // you may also use C++11 lambdas instead of std::bind
    factory.registerSimpleAction("OpenDoor", std::bind(OpenDoorFunc));
    factory.registerSimpleAction("EnterHouse", std::bind(EnterHouseFunc));
    factory.registerSimpleAction("CloseDoor", std::bind(CloseDoorFunc));


    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.tickRoot();

    return 0;
}

The final implementation results are as follows:,

Second method (recommended)

First, you need to create an object of BehaviorTreeFactory to register node s,

    // We use the BehaviorTreeFactory to register our custom nodes
    BT::BehaviorTreeFactory factory;

For the three child ren under Sequenc (i.e. opening the door, entering the house and closing the door), we use class to realize their operations. First, open the door,

class OpenDoorImpl : public BT::SyncActionNode
{
  public:
    OpenDoorImpl(const std::string& name) :
        BT::SyncActionNode(name, {})
    {
    }

    // You must override the virtual function tick()
    BT::NodeStatus tick() override
    {
        std::cout << "Door is opened" << std::endl;
        return BT::NodeStatus::SUCCESS;
    }
};

BT::SyncActionNode is used as the base class. The tick() function is provided in the base class. This function must be implemented by the user (tactical). The override keyword indicates that this function is a virtual function from the base class and must be implemented.

Similarly, realize the operation of entering and closing the door,

class EnterHouseImpl : public BT::SyncActionNode
{
  public:
    EnterHouseImpl(const std::string& name) :
        BT::SyncActionNode(name, {})
    {
    }

    // You must override the virtual function tick()
    BT::NodeStatus tick() override
    {
        std::cout << "Enter house" << std::endl;
        return BT::NodeStatus::SUCCESS;
    }
};

class CloseDoorImpl : public BT::SyncActionNode
{
  public:
    CloseDoorImpl(const std::string& name) :
        BT::SyncActionNode(name, {})
    {
    }

    // You must override the virtual function tick()
    BT::NodeStatus tick() override
    {
        std::cout << "Close door" << std::endl;
        return BT::NodeStatus::SUCCESS;
    }
};

After you have a node, you need to register it in the code. Note that the parameters should be the same as the tag name in xml, otherwise you can't find it,

    factory.registerNodeType<OpenDoorImpl>("OpenDoor");
    factory.registerNodeType<EnterHouseImpl>("EnterHouse");
    factory.registerNodeType<CloseDoorImpl>("CloseDoor");

Then there is the loading strategy,

auto tree = factory.createTreeFromText(xml_text);

Here, xml is written in the code as a string. If there is an xml file, another interface is used,

auto tree = factory.createTreeFromFile(xml_file);

Finally, root will tick,

tree.tickRoot();

The overall code is as follows,

#include "behaviortree_cpp_v3/bt_factory.h"


static const char* xml_text = R"(

<root main_tree_to_execute = "MainTree" >

     <BehaviorTree ID="MainTree">
        <Sequence name="root_sequence">
            <OpenDoor   name="open_door_of_house"/>
            <EnterHouse name="enter_house"/>
            <CloseDoor  name="close_door_of_house"/>
        </Sequence>
     </BehaviorTree>

 </root>
 )";


class OpenDoorImpl : public BT::SyncActionNode
{
  public:
    OpenDoorImpl(const std::string& name) :
        BT::SyncActionNode(name, {})
    {
    }

    // You must override the virtual function tick()
    BT::NodeStatus tick() override
    {
        std::cout << "Door is opened" << std::endl;
        return BT::NodeStatus::SUCCESS;
    }
};

class EnterHouseImpl : public BT::SyncActionNode
{
  public:
    EnterHouseImpl(const std::string& name) :
        BT::SyncActionNode(name, {})
    {
    }

    // You must override the virtual function tick()
    BT::NodeStatus tick() override
    {
        std::cout << "Enter house" << std::endl;
        return BT::NodeStatus::SUCCESS;
    }
};

class CloseDoorImpl : public BT::SyncActionNode
{
  public:
    CloseDoorImpl(const std::string& name) :
        BT::SyncActionNode(name, {})
    {
    }

    // You must override the virtual function tick()
    BT::NodeStatus tick() override
    {
        std::cout << "Close door" << std::endl;
        return BT::NodeStatus::SUCCESS;
    }
};


int main()
{
    // We use the BehaviorTreeFactory to register our custom nodes
    BT::BehaviorTreeFactory factory;


    factory.registerNodeType<OpenDoorImpl>("OpenDoor");
    factory.registerNodeType<EnterHouseImpl>("EnterHouse");
    factory.registerNodeType<CloseDoorImpl>("CloseDoor");


    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.tickRoot();

    return 0;
}

Operation results,

Summary

Why recommend the second? When the tree becomes complex, it will reflect the advantages of the second.

IV. summary

This paper builds a simple behavior tree to experience how to use BehaviorTree.CPP, so as to lay a good foundation for subsequent learning.

Posted by 182x on Fri, 22 Oct 2021 21:05:26 -0700