游戏思考25:behaviorTree源码剖析行为树(3.8版本,2023-11-22)


链接:https://www.behaviortree.dev/docs/3.8/tutorial-advanced/asynchronous_nodes
原文链接:https://github.com/BehaviorTree/BehaviorTree.CPP/tree/3.8.0

一、前置理解

1)行为树介绍

2)主要概念介绍

3)XML主题介绍

二、基础篇教程

1)建立第一个行为树

  • 先做一个顺序节点的行为树,结构图如下
    在这里插入图片描述
(1)三种方式创建行为树节点(继承、依赖注入、定义类)
  • (1)默认常规方法:继承去做
// Example of custom SyncActionNode (synchronous action)
// without ports.(port表示的意思是节点的传入、传出参数,这个ApproachObject节点没有传入传出参数)
class ApproachObject : public BT::SyncActionNode
{
public:
	//构造函数,传入行为树节点名字
  ApproachObject(const std::string& name) :
      BT::SyncActionNode(name, {})
  {}

  // You must override the virtual function tick()
  //需要重写tick虚函数,返回对应的节点状态(必须返回RUNNING, SUCCESS or FAILURE 这几个状态)
  BT::NodeStatus tick() override
  {
    std::cout << "ApproachObject: " << this->name() << std::endl;
    return BT::NodeStatus::SUCCESS;
  }
};
  • (2)第二种方法:依赖注入
BT::NodeStatus myFunction()
BT::NodeStatus myFunction(BT::TreeNode& self) 
  • 举例
using namespace BT;

// Simple function that return a NodeStatus
BT::NodeStatus CheckBattery()
{
  std::cout << "[ Battery: OK ]" << std::endl;
  return BT::NodeStatus::SUCCESS;
}
  • (3)第三种方法:定义类方法来定义行为树节点(这里可以调用这个类的open和close)
// We want to wrap into an ActionNode the methods open() and close()
class GripperInterface
{
public:
  GripperInterface(): _open(true) {}
    
  NodeStatus open() 
  {
    _open = true;
    std::cout << "GripperInterface::open" << std::endl;
    return NodeStatus::SUCCESS;
  }

  NodeStatus close() 
  {
    std::cout << "GripperInterface::close" << std::endl;
    _open = false;
    return NodeStatus::SUCCESS;
  }

private:
  bool _open; // shared information
};
(2)主main函数调用行为树节点(链接静态库用于调试,链接动态库用用于实际使用)
  • 链接静态库方式调用节点(registering all the nodes one by one.)
int main()
{
  //1、 We use the BehaviorTreeFactory to register our custom nodes
  BehaviorTreeFactory factory;//这个是用来注册节点的

	//2、
  // Note: the name used to register should be the same used in the XML.
  // Note that the same operations could be done using DummyNodes::RegisterNodes(factory)

  using namespace DummyNodes;

  // The recommended way to create a Node is through inheritance.
  // Even if it requires more boilerplate, it allows you to use more functionalities
  // like ports (we will discuss this in future tutorials).
  factory.registerNodeType<ApproachObject>("ApproachObject");

  // Registering a SimpleActionNode using a function pointer.
  // you may also use C++11 lambdas instead of std::bind
  factory.registerSimpleCondition("CheckBattery", std::bind(CheckBattery));

  //You can also create SimpleActionNodes using methods of a class
  GripperInterface gripper;
  factory.registerSimpleAction("OpenGripper",
                               std::bind(&GripperInterface::open, &gripper));
  factory.registerSimpleAction("CloseGripper",
                               std::bind(&GripperInterface::close, &gripper));

	//3、跑行为树逻辑
  // Trees are created at deployment-time (i.e. at run-time, but only once at the beginning).
  // The currently supported format is XML.
  // IMPORTANT: when the object "tree" goes out of scope, all the TreeNodes are destroyed
  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.tickRootWhileRunning();

}

  • 链接动态库方式调用节点
int main()
{
  //1、 We use the BehaviorTreeFactory to register our custom nodes
  BehaviorTreeFactory factory;//这个是用来注册节点的

	//2、
  // Load dynamically a plugin and register the TreeNodes it contains
  // it automated the registering step.
  factory.registerFromPlugin("./libdummy_nodes_dyn.so");//(源码宏定义已经注册)

	//3、跑行为树逻辑
  // Trees are created at deployment-time (i.e. at run-time, but only once at the beginning).
  // The currently supported format is XML.
  // IMPORTANT: when the object "tree" goes out of scope, all the TreeNodes are destroyed
  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.tickRootWhileRunning();

}
(3)xml解析
 <root main_tree_to_execute = "MainTree" >

     <BehaviorTree ID="MainTree">
        <Sequence name="root_sequence">
            <CheckBattery   name="battery_ok"/>
            <OpenGripper    name="open_gripper"/>
            <ApproachObject name="approach_object"/>
            <CloseGripper   name="close_gripper"/>
        </Sequence>
     </BehaviorTree>

 </root>
  • 注释

1)BehaviorTree ID 的作用是标识行为树的根节点,以便在执行行为树时从正确的节点开始执行。
2)Sequence表示顺序节点
3)root main_tree_to_execute表示tickRootWhileRunning()被调用的时候,执行BehaviorTree ID为MainTree的树逻辑,MainTree为根节点

  • 原来的字符串
static const char* xml_text = R"(

 <root main_tree_to_execute = "MainTree" >

     <BehaviorTree ID="MainTree">
        <Sequence name="root_sequence">
            <CheckBattery   name="battery_ok"/>
            <OpenGripper    name="open_gripper"/>
            <ApproachObject name="approach_object"/>
            <CloseGripper   name="close_gripper"/>
        </Sequence>
     </BehaviorTree>

 </root>
 )";

这里的R表示:
xml_text 变量中括号前的 R 是原始字符串字面量的前缀,它表示字符串中的任何转义序列(例如 \n 表示换行符)都被视为字面字符而不是它们的特殊含义。在这种情况下,它允许将 XML 文本写成单个多行字符串,而无需转义任何字符。

(4)输出结果
  • 注意

原来原来是链接linux动态库,这里需要改成链接win动态库,win的动态库需要和执行文件放一起
在这里插入图片描述

2)黑板数据和port

(1)黑板存在的作用(通过port来实现)

①给节点传递参数
②获得节点的输出信息
③把某个节点的输出信息作为输入信息传递给另外一个节点

(2)黑板的测试案例思维导图(这个黑板只存储了简单键值对)

在这里插入图片描述

(3)Input输入参数的ports举例使用
  • 举例
1)main函数注册SaySomething函数
  factory.registerNodeType<SaySomething>("SaySomething");
2)SaySomething这个类
class SaySomething : public BT::SyncActionNode
{
  public:
    SaySomething(const std::string& name, const BT::NodeConfiguration& config)
      : BT::SyncActionNode(name, config)
    {
    }

    // You must override the virtual function tick()
    NodeStatus tick() override;

    // It is mandatory to define this static method.
    static BT::PortsList providedPorts()
    {
        return{ BT::InputPort<std::string>("message") };
    }
};
这里的static BT::PortsList providedPorts()是强制性要求写成静态方式的;而且必须重写虚函数NodeStatus tick() override;
(4)输出参数的ports举例使用
  • 原版xml
 <root main_tree_to_execute = "MainTree" >

     <BehaviorTree ID="MainTree">
        <Sequence name="root">
            <SaySomething     message="hello" />
            <SaySomething2    message="this works too" />
            <ThinkWhatToSay   text="{the_answer}"/>
            <SaySomething2    message="{the_answer}" />
        </Sequence>
     </BehaviorTree>

 </root>
  • 这里的ThinkWhatToSay修改了text对应的the_answer的值,导致下面的the_answer也跟着变化
class ThinkWhatToSay : public BT::SyncActionNode
{
public:
  ThinkWhatToSay(const std::string& name, const BT::NodeConfiguration& config) :
    BT::SyncActionNode(name, config)
  {}

  // This Action simply write a value in the port "text"
  BT::NodeStatus tick() override
  {
    setOutput("text", "The answer is 42");
    return BT::NodeStatus::SUCCESS;
  }

  // A node having ports MUST implement this STATIC method
  static BT::PortsList providedPorts()
  {
    return {BT::OutputPort<std::string>("text")};
  }
};
(5)t02_basic_ports项目讲解

1)SaySomething在tick里面打印了hello
2)ThinkWhatToSay输出参数port到the_answer
3)这里由于SaySomething2之前对应了this works too,但是又因为ThinkWhatToSay修改了the_answer值,所以,SaySomething2的message会对应两个,打印(注意这里两个ThinkWhatToSay节点都是唯一的,所以都会被打印两次

        Robot says: this works too
        Robot says: The answer is 42

3)黑板传递通用参数-port with generic types

(1)黑板能传递通用数据(能把字符串转成其他整数、bool、double或结构体形式)
// We want to use this custom type
struct Position2D 
{ 
  double x;
  double y; 
};
  • 为了从字符串实例化成Position2D结构体
    需要提供模板实例化函数BT::convertFromString<Position2D>(StringView)
namespace BT
{
template <>
inline Position2D convertFromString(StringView str)
{
  printf("Converting string: \"%s\"\n", str.data());

  // 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++的string也可以传递C的const char*
②库提供了简单的字符串分离函数splitString,也可以使用boost::algorithm::split代替
③字符串转化成其他形式的参数,可以用convertFromString<double>()

(2)创建两个行为树节点,一个写一个读
  • CalculateGoal节点:写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;
    }
};
  • PrintTarget节点:从黑板读port数据出来(打印这个)
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;
    }
};
(3)t03_generic_ports项目讲解(整体顺序节点)

①通过类CalculateGoal传递GoalPosition这个Postion2D结构体到黑板里面
②调用节点PrintTarget,从黑板port读取GoalPosition这个Postion2D结构体并打印

Target positions: [ 1.1, 2.3 ]

③利用内部脚本,设置黑板参数把字符串转成OtherGoal,也就是调用函数

template <>
inline Position2D convertFromString(StringView str)

④读取OtherGola并转成Position2D打印出来

Target positions: [ -1.0, 3.0 ]

4)反射性行为和异步行为

(1)异步节点的标准要求:

①一个需要很长时间才能完成的行动,在没有达到完成标准时将返回RUNNING。
②不应该在tick()函数中阻塞太多时间。执行的流程应该尽可能快地返回。
③如果调用了halt()函数,应该尽可能中止它。

(2)状态同步节点StatefulActionNode
  • 备注
    ①StatefulAsyncNode是实现异步动作节点的首选方式。
    ②当你的代码包含请求-回复模式时,即当动作向另一个进程发送异步请求,并定期检查是否收到回复时,它特别有用。
    ③根据该答复,它可能返回SUCCESS或FAILURE。
    ④如果你不与外部进程通信,而是进行一些需要很长时间的计算,你可能想把它分成小的 “块”,或者你可能想把该计算转移到另一个线程(see AsyncThreadedAction 教程
  • 必须重写以下方法:
    NodeStatus onStart():当节点处于IDLE状态时调用。它可能立即成功或失败,或者返回RUNNING。在后一种情况下,下次收到tick时,将执行onRunning方法。
    NodeStatus onRunning(): 当节点处于RUNNING状态时调用。返回新的状态。
    void onHalted():当这个节点被树上的另一个节点中止时被调用。
  • 示例:创建一个名为MoveBaseAction的虚拟节点。
// Custom type
struct Pose2D
{
    double x, y, theta;
};

namespace chr = std::chrono;

class MoveBaseAction : public BT::StatefulAsyncAction
{
  public:
    // Any TreeNode with ports must have a constructor with this signature
    MoveBaseAction(const std::string& name, const BT::NodeConfig& config)
      : StatefulAsyncAction(name, config)
    {}

    // It is mandatory to define this static method.
    static BT::PortsList providedPorts()
    {
        return{ BT::InputPort<Pose2D>("goal") };
    }

    // this function is invoked once at the beginning.
    BT::NodeStatus onStart() override;

    // If onStart() returned RUNNING, we will keep calling
    // this method until it return something different from RUNNING
    BT::NodeStatus onRunning() override;

    // callback to execute if the action was aborted by another node
    void onHalted() override;

  private:
    Pose2D _goal;
    chr::system_clock::time_point _completion_time;
};

//-------------------------

BT::NodeStatus MoveBaseAction::onStart()
{
  if ( !getInput<Pose2D>("goal", _goal))
  {
    throw BT::RuntimeError("missing required input [goal]");
  }
  printf("[ MoveBase: SEND REQUEST ]. goal: x=%f y=%f theta=%f\n",
         _goal.x, _goal.y, _goal.theta);

  // We use this counter to simulate an action that takes a certain
  // amount of time to be completed (200 ms)
  _completion_time = chr::system_clock::now() + chr::milliseconds(220);

  return BT::NodeStatus::RUNNING;
}

BT::NodeStatus MoveBaseAction::onRunning()
{
  // Pretend that we are checking if the reply has been received
  // you don't want to block inside this function too much time.
  std::this_thread::sleep_for(chr::milliseconds(10));

  // Pretend that, after a certain amount of time,
  // we have completed the operation
  if(chr::system_clock::now() >= _completion_time)
  {
    std::cout << "[ MoveBase: FINISHED ]" << std::endl;
    return BT::NodeStatus::SUCCESS;
  }
  return BT::NodeStatus::RUNNING;
}

void MoveBaseAction::onHalted()
{
  printf("[ MoveBase: ABORTED ]");
}
(3)顺序序列树和反射性顺序序列树

5)利用子树

6)port remapping

7)use multiple xml files

8)传递额外参数

三、进阶篇教程(异步流程)

四、Nodes Library

1)装饰器

InverterNode
调用子节点一次,如果子节点失败,返回SUCCESS,如果子节点成功,返回FAILURE。如果子节点返回RUNNING,这个节点也返回RUNNING。

ForceSuccessNode
调用子节点一次,如果子节点返回RUNNING,这个节点也返回RUNNING。否则,它总是返回SUCCESS。

ForceFailureNode
如果子节点返回RUNNING,这个节点也返回RUNNING。否则,它总是返回FAILURE

RepeatNode
只要子节点返回SUCCESS,就一直调用子节点,最多N次,其中N是作为输入端口传递的。如果子节点返回FAILURE,则中断循环,在这种情况下,也返回FAILURE。如果子节点返回RUNNING,这个节点也返回RUNNING
RetryNode
只要子节点返回失败,就调用子节点,最多N次,其中N是作为输入端口传递的。如果子节点返回SUCCESS,则中断该循环,在这种情况下,也返回SUCCESS。如果子节点返回RUNNING,这个节点也返回RUNNING

2)回调函数(Fallbacks,有suc则为suc)

  • 介绍
    这个系列的节点在框架中被称为 "选择器 "或 “优先权”。它们的目的是尝试不同的策略,直到我们找到一个 "有效 "的策略。

  • 目前,该框架提供了两种节点:
    1)Fallback
    2)ReactiveFallback

  • 规则:
    1)在调用第一个子节点之前,节点状态变成RUNNING
    2)如果一个子节点返回失败,回退时将会选择下一个子节点
    3)如果最后一个子节点也返回FAILURE,那么所有的子节点都会被停止,回退也会返回FAILURE。
    4)如果一个子节点返回SUCCESS,它就停止并返回SUCCESS。所有的子节点都将停止

  • 例子
    在这个例子中,我们尝试不同的策略来打开门。只会在最开始检查一次门是否打开
    在这里插入图片描述

  • ReactiveFallback
    这个控制节点用于当你想中断一个异步子节点,如果之前的一个条件将其状态从失败变为成功时。

  • ReactiveFallback例子
    在下面的例子中,角色将睡眠长达8小时。如果他/她已经完全休息了,那么节点areYouRested? 将返回SUCCESS,异步节点Timeout(8小时)和Sleep将被中断。
    在这里插入图片描述

3)顺序节点

  • 规则
    1)在执行第一个子节点之前,节点状态变成RUNNING。
    2)如果一个子节点返回SUCCESS,它将执行下一个子节点。
    3)如果最后一个子节点也返回SUCCESS,所有的子节点都被停止,序列返回SUCCESS

  • 该节点分类
    1)Sequence
    2)SequenceWithMemory
    3)ReactiveSequence

  • 要了解这三个控制节点的区别,请参考下表:
    在这里插入图片描述
    在这里插入图片描述
    1)Restart 意味着整个序列从列表的第一个子节点开始重新启动。
    2)Tick again 意味着下一次序列被执行时,同一个孩子会被再次执行。已经返回成功的前一个兄弟节点不会再被执行。

  • 行为树节点分类
    1)序列Sequence节点:
    顺序执行所有子节点,若都成功则返回成功,若有一个失败则返回失败

在这里插入图片描述

2)ReactiveSequence节点

  • 介绍
    这个节点对连续检查条件特别有用;但用户在使用异步子节点时也应该小心,以确保它们不会被调用的次数超过预期。
  • 例子
    在这里插入图片描述
    ApproachEnemy 是一个异步动作,它返回RUNNING,直到它最终完成。条件isEnemyVisible将被多次调用,如果它变成false(即 “failure”),ApproachEnemy将被停止

3)SequenceWithMemory节点
当你不想再次调用已经返回SUCCESS的孩子时,请使用这个控制节点
在这里插入图片描述

这是一个巡逻机器人,必须只访问一次地点A、B和C。如果行动GoTo(B)失败,GoTo(A)将不会被再次调用

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Tree.js 是一个用于创建动态 3D 场景的开源 JavaScript 库。它可以让开发者在网页上直观地呈现出树状结构的图形,并且支持对这些图形进行交互和动画,拥有强大的渲染能力和用户体验。如果想要入门 Tree.js,可以参考以下步骤: 1. 准备工作:首先,在开始学习 Tree.js 之前,需要具备一定的前端开发知识,包括 HTML、CSS 和 JavaScript。另外,需要确保已经安装了最新版的浏览器,以便于在学习和开发过程中进行实时预览和调试。 2. 下载和引入:在官网 https://threejs.org/ 上可以找到最新的 Tree.js 版本,可以选择下载或者通过 CDN 引入。然后在 HTML 文件中引入 Tree.js 的 JavaScript 文件。 3. 创建场景:在 JavaScript 中创建一个基本的 3D 场景,并添加相机、光源和几何体。这可以通过 Tree.js 提供的 API 来实现,例如使用 Scene、PerspectiveCamera、AmbientLight、DirectionalLight 和 BoxGeometry。 4. 渲染场景:在 JavaScript 中设置渲染器和将场景渲染到 HTML 中。可以使用 Renderer、CSS3DRenderer 和 WebGLRenderer 来实现不同的渲染效果。 5. 交互和动画:通过 JavaScript 实现鼠标交互、键盘事件或者自动动画效果,使得用户可以与场景进行交互。 6. 学习资源:Tree.js 官网提供了丰富的文档和示例,还有一个活跃的社区,可以在社区中学习和交流。 通过以上步骤,可以初步了解如何使用 Tree.js 创建一个基本的 3D 场景,并进行交互和动画。随着深入学习和实践,会发现 Tree.js 的强大和灵活,可以实现各种炫酷的 3D 效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值