行为树 — [4] 简单树

本文讲述如何构建一个简单树来体验以下行书树的使用。


一 tick的理解

假设要按顺序做2件事,发现目标和抓住目标(先发现了才能抓住),其规划如下,
在这里插入图片描述
PS:上面2层可以看作是战略 (大体的规划),最底层则可以看作是战术(具体如何做)

tick可以看做是一个触发信号,当一个tick信号发出,从Sequence开始往下走,先走到DeteckObject,然后走到ObjectRecognition Component,执行完毕后回到Sequence,Sequence会根据DeteckObject的执行结果再继续执行。

如果DeteckObject执行成功,就会继续走到GraspObject,最后走到Manipulation Component,执行完毕后回到Sequence。如果DeteckObject执行失败,那么就认为这个Sequence执行结束。

需要注意的是以上操作只需要一个tick信号,node之间会根据情况自动tick。

tick更形象的例子应该是牛顿摆,
在这里插入图片描述


二 树的规划

树使用xml格式进行战略规划,开门->进屋->关门,

<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>

xml格式有以下几个要点,

  • root这个tag是必须的,而且它需要有main_tree_to_execute的属性,表示执行哪颗树
  • root的子元素必须有BehaviorTree,BehaviorTree必须有ID属性
  • 如果root拥有多个BehaviorTree,那么BehaviorTree的ID的属性值必须是不同的
  • 如果root只有一个BehaviorTree,那么main_tree_to_execute属性可有可无
  • BehaviorTree的子元素就是树节点(tree nodes)

这里的树节点类型是Sequence,其包含3个children。

Sequence树节点的特点是它的children必须全部返回SUCCESS才认为执行成功,有一个child返回FAILURE就导致这个树节点执行失败。

  • 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.

Sequence节点又分为三种,

  • Sequence (同名)
  • SequenceStar
  • ReactiveSequence

它们的区别如下,
在这里插入图片描述

  • Restart的意思是从第一个child开始重新运行整个树
  • Tick again的意思是下一次收到tick信号,只执行当前运行失败的child,之前运行成功的child不会再运行

三 代码

强调一点,xml负责规划执行逻辑(战略),具体的执行过程则由用户提供(战术)。

第一种方式

首先需要创建BehaviorTreeFactory的对象,用于注册node,

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

然后注册用户自定义的战术,即对开门,进屋和关门提供自定义的操作,

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

执行时会根据名称(第一个参数)来找到这些战术,所以名称必须和xml里child的tag名字一致。

接着是加载战略,

auto tree = factory.createTreeFromText(xml_text);

这里是把xml以字符串的形式写在代码里,如果有xml文件,则使用另外一个接口,

auto tree = factory.createTreeFromFile(xml_file);

最后是由root来tick一下,

tree.tickRoot();

整体代码如下,

#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;
}

最后执行结果如下,
在这里插入图片描述

第二种方式(推荐)

首先需要创建BehaviorTreeFactory的对象,用于注册node,

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

对于Sequenc下的三个child (即开门,进屋和关门),我们使用类的方式来实现其操作,先是开门,

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作为基类,基类里提供了tick()函数,这个必须由用户自己去实现(战术),override关键字表示这个函数是来自于基类的虚函数,必须要实现。

同理,实现进屋和关门的操作,

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;
    }
};

有了节点后,在代码里需要注册一下,注意参数要和xml里的tag名称一样,否则找不到,

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

接着是加载战略,

auto tree = factory.createTreeFromText(xml_text);

这里是把xml以字符串的形式写在代码里,如果有xml文件,则使用另外一个接口,

auto tree = factory.createTreeFromFile(xml_file);

最后是由root来tick一下,

tree.tickRoot();

整体代码如下,

#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;
}

运行结果,
在这里插入图片描述

小结

为什么推荐第二种,当树变的复杂时,就会体现第二种的优势了。


四 总结

本文通过构建一个简单的行为树来体验下如何使用BehaviorTree.CPP,为后续的学习打好基础。

© 版权声明
THE END
喜欢就支持一下吧
点赞79 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容