如何正确打开行为树的使用

什么是行为树

行为树(Behavior Tree)是一种在计算机科学领域用于构建和模拟实体行为的层次性图形化建模工具。其为一种行为建模范式,以层次化的节点组织为特征,用于描述和规划复杂系统中各种实体的交互和决策。行为树是一种高抽象层次的建模工具/语言,将系统设计师与具体的技术细节解耦,直接使用行为树在更高层次上设计整个系统。
行为树最初用于开发电子游戏,但现在广泛应用于机器人、计算机科学和人工智能(AI)领域,行为树看起来就像我们通常在医院大厅看到的办事流程图,它解释了特定过程的流程。
不过,行为树也有局限性,其中之一是在浏览大型行为树时需要工具。最重要的限制是,它不能教会程序做出更好的决策。由于决策过程被硬编码到树中,它不会教角色或机器人如何做出决策。

为什么要用行为树

很多场景下行为树可用来替换状态机,我们从状态机的缺点分析为什么要用行为树替代状态机,再举一个案例来对比说明。

状态机的缺点

  • 可维护性差:在添加或删除状态时,需要更改所有其他与新旧状态之间存在转换的状态的条件。较大的更改更容易出现错误,可能会被忽视。
  • 可扩展性差:具有许多状态的有限状态机丧失了图形可读性的优势,如果有限状态机(FSM)有N个状态,那么跳转关系就有NN个,如果N的量级很大,那么NN就会更大。
  • 可重用性差:由于状态跳转的判断条件和逻辑都位于状态内部,状态之间的耦合强度很大,几乎不可能在多个项目中使用相同的行为。

分层状态机(HFSM)跳转关系示例

在这里插入图片描述

行为树的优点(对比状态机)

  • 可维护性好:BT中的转换由BT的结构定义,而不是由状态内的条件定义。因此,节点可以彼此独立设计,当在树的一小部分中添加或删除新节点(甚至子树)时,不需要更改模型的其他部分。
  • 可扩展性好:当一个BT有很多节点时,它可以被分解成小的子树,以保持图形模型的可读性。
  • 可重用性好:由于BT中节点的独立性(节点中不需要维护任何与跳转相关的判断和逻辑),子树也是独立的,这允许在其他节点或子树之间重用。

案例对比

同样的避障功能分别用状态机和行为树来实现,通过对比来感受行为树的优点。

状态机实现

在这里插入图片描述

行为树实现

在这里插入图片描述

怎么用行为树

前言说明

可以把行为树理解成是一种高抽象层次的编程语言(类似于C++/java这样的编程语言,只不过抽象层次更高),如果想用这款编程语言更好地实现我们需要的功能,就必须深入学习其语法,因为只有深入理解了这款编程语言,才能更好地使用它。
注:打个比方,就好比我们需要使用C++/Java编写一个功能,如果只是简单的学习了一下C++/java的基本用法,在编写代码的过程中用到哪学哪,这样虽然也能实现我们需要的功能,但很难写出优美且健壮的代码,更无法确保所实现功能的鲁棒性和最优性能。

我们基于BehaveTree.cpp项目来深入学习如何使用行为树以及这款语言的语法。
github地址:https://github.com/BehaviorTree

行为树(Behavior Tree)tick的概念

学术性的解释

行为树(Behavior Tree)的 “tick” 操作是指对行为树进行一次迭代式的运行步骤,用以驱动实体在特定环境下做出决策和执行行为。Tick 操作对整个行为树的状态以及其中的节点状态进行了更新和推进,以实现实体智能行为的动态调整。(可以把tick理解成行为树的心脏起搏器或者MCU的晶振时钟)
在每次 Tick 操作中,行为树从其根节点开始遍历,并根据节点类型和执行结果执行相应的逻辑。这可能涉及顺序节点按序执行子节点、选择节点依次尝试子节点,以及行为节点执行实际操作等。每个节点在 Tick 操作中更新自身的状态,该状态可能表明节点是成功、失败还是仍在运行中。这些状态的更新会反过来影响父节点的状态,以及整个行为树的状态。
Tick 操作通过迭代、递归的方式,深入遍历行为树的层次结构,直到叶子节点。通过将环境信息、实体状态以及其他输入参数传递到节点中,行为树能够在每次 Tick 操作中根据最新的情境进行决策,使实体能够做出适应性的响应和行为。
总之,Tick 操作是行为树运行的基本单位,为实体提供了一种结构化、分层的方法,用于推动智能行为的生成和调整。它是实现动态、适应性决策和行为的核心机制,使行为树成为多领域中实现智能的有效工具。

通俗易懂的解释

在行为树(Behavior Tree)中,“tick” 是一个重要的概念,表示对整个行为树进行一次执行步骤。行为树的运行是通过不断的 tick 操作来进行的,每次 tick 都会从树的根节点开始,逐级向下遍历节点,根据节点的类型和状态执行相应的逻辑。(可以把tick理解成行为树的心脏起搏器或者MCU的晶振时钟)
具体来说,每次 tick 操作会按照以下方式进行:

  • 从根节点开始: 行为树的 tick 操作始于根节点,也就是整个树的起始点。
  • 逐级遍历: 根据当前节点的类型,执行相应的逻辑。例如,如果是一个顺序节点,会从左到右依次执行其子节点;如果是一个选择节点,会依次尝试执行子节点,直至找到一个成功的节点;如果是一个行为节点,会执行具体的行为逻辑。
  • 更新状态: 在每个节点执行完毕后,节点会根据其执行结果(成功、失败、运行中等)更新自身的状态。
  • 传递结果: 节点的状态可能会影响父节点的状态,从而影响整个行为树的状态。
  • 递归执行: 如果节点有子节点,会递归进行相同的 tick 操作,继续遍历下去,直到叶子节点。
  • 完成执行: 当整个行为树的 tick 操作完成时,行为树的状态可能会发生变化,反映了树中所有节点的执行结果。
    “Tick” 操作是行为树运行的基本单位,它使得整个行为树能够根据实时环境和条件不断地进行决策和行为。每次 tick 可以看作是行为树在瞬间中进行的一次状态更新和逻辑执行,从而实现了实体的智能行为控制。

行为树节点

行为树的节点分为三类:控制节点、装饰器节点、叶子节点(行为节点):

  • 控制节点:可挂载N个子节点,是行为树的结构化组织元素,用于控制其下方子节点的执行流程。其中,最常见的控制节点包括顺序节点(Sequence)、选择节点(Selector)和并行节点(Parallel)。
  • 装饰器节点:只能挂载一个子节点,用于修改其下方子节点的行为。这种节点可以用于添加条件、限制执行次数、翻转结果等,例如,“Inverter” 装饰器节点将子节点的成功状态转换为失败状态,反之亦然;“Repeater” 装饰器节点用于反复执行子节点,可以控制重复的次数。
  • 叶子节点:叶子节点没有子节点,代表实际的行为或动作,是行为树中最基本的组成部分。它们定义了实体在特定情况下应该执行的具体操作。叶子节点可以是移动、攻击、互动等实际的行为。

下面我们先来深入学习控制节点和装饰器节点的“语法”。

注1:节点自身只能将自己设置为running状态,而不能自己变成idle。只有上一级节点经过更高一级的逻辑策略评估后,才能把子节点设为idle状态。
注2:每个流程图描述中的结束框表示彻底终结本次控制流程,重置所有子节点(将所有子节点的状态设置为idle,如果有正在running的子节点则直接强制终止)

控制节点

行为树中的控制节点用于管理和组织子节点的执行顺序和行为。这些控制节点决定了行为树中不同节点之间的关系和流程。以下是几种常见的行为树控制节点:

  • SequenceNode(顺序控制节点): 这个节点用于按照顺序执行其子节点,只有前一个子节点成功完成,才会执行下一个子节点。如果任何一个子节点失败,整个顺序节点会返回失败状态。
  • ParallelNode(并行控制节点): 这个节点用于同时执行多个子节点的任务,根据设定的阈值来决定整个并行节点的状态。它允许实体在同一时间步骤内并发地执行多个任务,然后根据每个任务的执行结果来做出综合判断。
  • FallbackNode(回退控制节点): 这个节点用于在子节点执行失败时,依次尝试执行其他子节点,直到找到一个成功的子节点为止,或者所有子节点都执行失败。它提供了一种备选方案的机制,以应对可能的失败情况。
  • ConditionalNode(条件控制节点): 这个节点根据特定的条件来决定是否执行其子节点。如果条件满足,执行子节点,否则不执行。它用于实现基于条件的行为逻辑,类似于编程语言中的条件判断语句。
    这些控制节点在行为树中起到不同的作用,用于构建复杂的行为逻辑,管理子节点的执行顺序和结果,并根据实际情况做出决策。通过合理地组织和使用这些控制节点,可以构建出灵活、复杂且适应多变情况的行为树。
顺序型节点
基本概念

行为树中的 顺序型节点是一种控制节点,用于按顺序执行其子节点,并且只有在前一个子节点成功完成后才会执行下一个子节点。顺序节点在行为树中通常被用来组织一系列任务的执行顺序,确保它们按照特定顺序被逐一执行。
以下是顺序型节点的一般特点和用法:

  • 顺序节点由多个子节点构成,按照从左到右的顺序依次执行。
  • 在tick第一个子节点之前,控制节点状态变为RUNNING。
  • 只有在前一个子节点成功完成(返回成功状态)时,才会执行下一个子节点。
  • 如果最后一个子进程也返回SUCCESS,那么所有的子进程都停止,序列返回SUCCESS。
  • 如果任何一个子节点失败(返回失败状态),整个顺序节点会立即返回失败状态,不再执行后续节点(SequenceWithMemory除外)。

三种类型的顺序节点,详细请参考下表:
在这里插入图片描述

  • “Restart”意味着下一次序列被tick时,将从列表的第一个子节点开始重新启动整个序列。
  • “Tick again”意味着下一次序列被tick时,将再次tick同一子节点。先前已经返回SUCCESS的子节点不再被重复tick。
SequenceNode
代码描述
// 每次tick该控制节点,从这里开始执行

static bool all_skipped_ = true; // 是否跳过全部子节点标志位

// 第一次tick该节点的时候设置标志位,第一次刚tick该节点的时候节点状态肯定是idle
if(status() == IDLE) {
  all_skipped_ = true; 
}

// 在tick子节点前,自身状态先变成running
setStatus(RUNNING);

// 子节点的个数
const size_t children_count = children_nodes_.size();

// 当前正在tick的子节点
static int current_child_idx = 0;

// 轮询tick子节点
while (current_child_idx < children_count) {
  // 执行选定子节点,并保存子节点执行状态
  NodeStatus child_status = children_nodes_[current_child_idx_]->executeTick();
  
  // 只要有一个子节点没有被跳过,该标志位为false
  all_skipped_ &= (child_status == SKIPPED); 
  
  switch(child_status) {
    case RUNNING: {
      // 该控制节点直接向上级返回running,下次继续tick当前子节点
      return RUNNING;
    }
    case FAILURE: {
      // 子节点执行失败,则终止该控制节点流程的执行,全部重置,控制节点也向上级返回失败
      resetAllChildren(); // 重置全部子节点,子节点状态全部设置为idle,如果子节点中有处于running的,则直接强制终止
      current_child_idx = 0; // 重置子节点的执行序号,下次再tick该控制节点,直接从第一个子节点开始tick
      return FAILURE;
    }
    case SUCCESS: {
      // 子节点执行成功,则继续tick下一个子节点
      current_child_idx++;
    }
    break;
    case SKIPPED: {
      // 子节点跳过执行,也继续tick下一个子节点
      current_child_idx++;
    }
    break;
    case IDLE: {
       // 不允许子节点返回idle,抛异常
       throw LogicError("A children should not return IDLE");
    }
  }  // end switch
}  // end while

if (current_child_idx_ == children_count) {
  // 如果执行完全部子节点,全部重置
  resetAllChildren();  // 重置全部子节点,子节点状态全部设置为idle,如果子节点中有处于running的,则直接强制终止
  current_child_idx = 0; // 重置子节点的执行序号,下次再tick该控制节点,直接从第一个子节点开始tick
}

// 如果跳过全部的子节点,则该控制节点向上级返回SKIPPED,否则返回SUCCESS
return all_skipped_ ? SKIPPED : SUCCESS;
流程图描述

在这里插入图片描述

ReactiveSequence
代码描述
// 每次tick该控制节点,从这里开始执行

// 在tick子节点前,自身状态先变成running
setStatus(RUNNING);

static bool all_skipped_ = true; // 是否跳过全部子节点标志位

// 轮询tick子节点
for (size_t index = 0; index < childrenCount(); index++) {
  // 执行选定子节点,并保存子节点执行状态
  NodeStatus child_status = children_nodes_[index]->executeTick();
  
  // 只要有一个子节点没有被跳过,该标志位为false
  all_skipped &= (child_status == SKIPPED);
  
  switch (child_status) {
    case RUNNING: {
      // 如果某子节点执行结果为running,把该节点之前的所有节点全部重置,下次tick从头开始执行
      for (size_t i = 0; i < index; i++) {
        resetNode(i); // 重置节点,将节点状态重置为idle,如果节点状态为running则直接终止异步执行
      }
      return RUNNING; // 向上级返回running
    }

    case FAILURE: {
      // 子节点执行失败,则终止该控制节点流程的执行,全部重置,控制节点也向上级返回失败
      resetAllChildrenNode();
      return FAILURE;
    }

    // 如果子节点返回成功,则继续tick下一个子节点
    case SUCCESS: break;

    case SKIPPED: {
      // 重置该节点,将节点状态重置为idle,如果节点状态为running则直接终止异步执行
      resetNode(index);
    }
    break;

    case NodeStatus::IDLE: {
       // 不允许子节点返回idle,抛异常
       throw "不允许子节点返回idle"
    }
  }  // end switch
}  // end for

// 如果执行完全部子节点,全部重置
resetAllChildrenNode(); // 重置全部子节点,子节点状态全部设置为idle,如果子节点中有处于running的,则直接强制终止

// 如果跳过全部的子节点,则该控制节点向上级返回SKIPPED,否则返回SUCCESS
return all_skipped ? SKIPPED : SUCCESS;
流程图描述

在这里插入图片描述

SequenceWithMemory
代码描述
// 每次tick该控制节点,从这里开始执行

static bool all_skipped_ = true; // 是否跳过全部子节点标志位

// 第一次tick该节点的时候设置标志位,第一次刚tick该节点的时候节点状态肯定是idle
if(status() == IDLE) {
  all_skipped_ = true; 
}

// 在tick子节点前,自身状态先变成running
setStatus(RUNNING);

// 子节点的个数
const size_t children_count = children_nodes_.size();

// 当前正在tick的子节点
static int current_child_idx = 0;

// 轮询tick子节点
while (current_child_idx < children_count) {
  // 执行选定子节点,并保存子节点执行状态
  NodeStatus child_status = children_nodes_[current_child_idx]->executeTick();

   // 只要有一个子节点没有被跳过,该标志位为false
  all_skipped_ &= (child_status == NodeStatus::SKIPPED);

  switch (child_status) {
    case RUNNING: {
      // 该控制节点直接向上级返回running,下次继续tick当前子节点
      return RUNNING;
    }
    case FAILURE: {
      // 如果子节点返回失败,则把该节点之后的所有节点都重置,并向上级返回失败,下次继续tick当前子节点
      for (size_t i = current_child_idx_; i < childrenCount(); i++) {
        resetNode(i); // 重置节点,将节点状态重置为idle,如果节点状态为running则直接终止异步执行
      }
      return FAILURE;
    }
    case SUCCESS: {
      // 子节点执行成功,则继续tick下一个子节点
      current_child_idx_++;
    }
    break;

    case SKIPPED: {
      // 子节点跳过执行,也继续tick下一个子节点
      current_child_idx_++;
    }
    break;

    case NodeStatus::IDLE: {
      // 不允许子节点返回idle,抛异常
      throw "not allow return idle"
    }
  }   // end switch
}     // end while loop

if (current_child_idx == children_count) {
  // 如果执行完全部子节点,全部重置
  resetAllChildren();  // 重置全部子节点,子节点状态全部设置为idle,如果子节点中有处于running的,则直接强制终止
  current_child_idx = 0; // 重置子节点的执行序号,下次再tick该控制节点,直接从第一个子节点开始tick
}

// 如果跳过全部的子节点,则该控制节点向上级返回SKIPPED,否则返回SUCCESS
return all_skipped_ ? SKIPPED : SUCCESS;
流程图描述

在这里插入图片描述

回退型节点
基本概念

行为树中的 Fallback(回退)节点是一种控制节点,用于构建一种类似于“备选方案”的行为逻辑。当子节点执行失败时,Fallback 节点会依次尝试执行其他子节点,直到找到一个成功的子节点为止,或者所有子节点都执行失败。换句话说,Fallback 节点提供了一种优雅的方式来处理备选方案,以应对计划中的任务或决策的失败情况。
特点和用法:

  • 执行顺序: Fallback 节点从左到右依次尝试执行其子节点,直到找到一个成功的节点。
  • 失败处理: 如果一个子节点失败,Falling back 节点会继续尝试执行下一个子节点,直到成功或所有子节点都失败为止。
  • 应用场景: Fallback 节点适用于处理出现错误、失败或不可预料情况的情况。它可以作为备选方案,确保即使计划的行为无法完成,实体仍然能够尝试其他方案。

两种类型的回退节点,详细请参考下表
在这里插入图片描述

  • “Restart”意味着下一次序列被tick时,将从列表的第一个子节点开始重新启动整个序列。
  • “Tick again”意味着下一次序列被tick时,将再次tick同一子节点。先前已经返回SUCCESS的子节点不再被重复tick。
FallbackNode
代码描述
// 每次tick该控制节点,从这里开始执行

static bool all_skipped_ = true; // 是否跳过全部子节点标志位

// 第一次tick该节点的时候设置标志位,第一次刚tick该节点的时候节点状态肯定是idle
if(status() == IDLE) {
  all_skipped_ = true; 
}

// 在tick子节点前,自身状态先变成running
setStatus(RUNNING);

// 子节点的个数
const size_t children_count = children_nodes_.size();

// 当前正在tick的子节点
static int current_child_idx = 0;

// 轮询tick子节点
while (current_child_idx < children_count) {
   // 执行选定子节点,并保存子节点执行状态
  NodeStatus child_status = children_nodes_[current_child_idx]->executeTick();
  
  // 只要有一个子节点没有被跳过,该标志位为false
  all_skipped_ &= (child_status == SKIPPED);
  
  switch (child_status) {
    case RUNNING: {
      // 该控制节点直接向上级返回running,下次继续tick当前子节点
      return RUNNING;
    }
    case SUCCESS: {
      // 如果子节点返回成功,则重置所有子节点,并向上级返回成功,下次继续tick当前子节点
      resetAllChildren(); // 重置全部子节点,子节点状态全部设置为idle,如果子节点中有处于running的,则直接强制终止
      current_child_idx = 0; // 重置子节点的执行序号,下次再tick该控制节点,直接从第一个子节点开始tick
      return SUCCESS;
    }
    case FAILURE: {
      // 子节点执行失败,则继续tick下一个子节点
      current_child_idx_++;
    }
    break;
    case SKIPPED: {
      // 子节点跳过,继续tick下一个子节点
      current_child_idx_++;
    }
    break;
    case IDLE: {
       // 不允许子节点返回idle,抛异常
      throw "A children should not return IDLE";
    }
  }   // end switch
}  // end while loop

if (current_child_idx == children_count) {
  // 如果执行完全部子节点,全部重置
  resetAllChildren();  // 重置全部子节点,子节点状态全部设置为idle,如果子节点中有处于running的,则直接强制终止
  current_child_idx = 0; // 重置子节点的执行序号,下次再tick该控制节点,直接从第一个子节点开始tick
}

// 如果跳过全部的子节点,则该控制节点向上级返回SKIPPED,否则返回SUCCESS
return all_skipped_ ? SKIPPED : SUCCESS;
流程图描述

在这里插入图片描述

ReactiveFallback
代码描述
// 每次tick该控制节点,从这里开始执行

// 在tick子节点前,自身状态先变成running
setStatus(RUNNING);

static bool all_skipped_ = true; // 是否跳过全部子节点标志位

size_t failure_count = 0;

// 轮询tick子节点
for (size_t index = 0; index < childrenCount(); index++) {
  TreeNode* current_child_node = children_nodes_[index];
  // 执行选定子节点,并保存子节点执行状态
  const NodeStatus child_status = current_child_node->executeTick();

  // 只要有一个子节点没有被跳过,该标志位为false
  all_skipped &= (child_status == SKIPPED);

  switch (child_status) {
    case RUNNING: {
      // 如果某子节点执行结果为running,把该节点之前的所有节点全部重置,下次tick从头开始执行
      for (size_t i = index + 1; i < childrenCount(); i++) {
        resetChild(i); // 重置节点,将节点状态重置为idle,如果节点状态为running则直接终止异步执行
      }
      return RUNNING; // 向上级返回running
    }

    case FAILURE: {
      // 如果子节点返回失败,则继续tick下一个子节点
      failure_count++;
    }
    break;

    case SUCCESS: {
      // 子节点执行成功,则终止该控制节点流程的执行,全部重置,控制节点也向上级返回失败
      resetAllChildren();
      return SUCCESS;
    }

    case SKIPPED: {
      // 重置该节点,将节点状态重置为idle,如果节点状态为running则直接终止异步执行
      resetChild(index);
    }
    break;

    case IDLE: {
      // 不允许子节点返回idle,抛异常
      throw "A children should not return IDLE";
    }
  }   // end switch
}     // end for

// 如果全部子节点都执行失败,重置全部子节点,向上级返回失败
if (failure_count == childrenCount()) {
  resetAllChildren(); // 重置全部子节点,子节点状态全部设置为idle,如果子节点中有处于running的,则直接强制终止
  return FAILURE;
}

return all_skipped ? SKIPPED : FAILURE;
流程图描述

在这里插入图片描述

并行控制节点
基本概念

行为树中的 Parallel(并行)节点是一种控制节点,用于同时执行多个子节点,并根据设定的阈值来决定整个并行节点的状态。并行节点允许在同一次tick内并发地执行多个任务,然后根据每个任务的执行结果来做出综合判断。
特点和用法:

  • 并发性质: 并行节点的子节点会同时启动,它们在同一次tick步骤内被执行,但实际执行仍是顺序进行的。
  • 执行策略: 并行节点会记录子节点的执行状态(成功、失败、运行中等)。根据设定的成功和失败阈值,并行节点会根据所有子节点的执行状态来决定自身的状态。
  • 阈值设置: 并行节点可以设置成功和失败的阈值,用于决定并行节点的最终状态。比如,你可以设置成功阈值为 2(至少有两个子节点成功)和失败阈值为 1(只有一个子节点失败)。
  • 应用场景: 并行节点适用于需要同时执行多个任务,并根据每个任务的结果来做出整体判断的情况。这在处理复杂任务或需要综合多个条件的决策时特别有用。

并行节点和顺序节点的区别
尽管并行节点和顺序节点都涉及到多个子节点的执行,但它们的核心差异在于执行方式和目的。并行节点强调在同一时间步骤内并行执行多个任务,并根据阈值判断整体成功与否。而顺序节点则强调在顺序上执行子节点,保证任务的有序执行。

ParallelNode
代码描述
 setStatus(RUNNING);

 size_t skipped_count = 0;

  // Routing the tree according to the sequence node's logic:
  for (size_t i = 0; i < children_count; i++)
  {
    if(completed_list_.count(i) == 0)
    {
      TreeNode* child_node = children_nodes_[i];
      NodeStatus const child_status = child_node->executeTick();

      switch (child_status)
      {
        case NodeStatus::SKIPPED: {
          skipped_count++;
        } break;

        case NodeStatus::SUCCESS: {
          completed_list_.insert(i);
          success_count_++;
        }
        break;

        case NodeStatus::FAILURE: {
          completed_list_.insert(i);
          failure_count_++;
        }
        break;

        case NodeStatus::RUNNING: {
          // Still working. Check the next
        }
        break;

        case NodeStatus::IDLE: {
          throw LogicError("[", name(), "]: A children should not return IDLE");
        }
      }
    }

    const size_t required_success_count = successThreshold();

    if(success_count_ >= required_success_count ||
        (success_threshold_ < 0 && (success_count_ + skipped_count) >= required_success_count))
    {
      clear();
      resetChildren();
      return NodeStatus::SUCCESS;
    }

    // It fails if it is not possible to succeed anymore or if
    // number of failures are equal to failure_threshold_
    if (((children_count - failure_count_) < required_success_count) ||
        (failure_count_ == failureThreshold()))
    {
      clear();
      resetChildren();
      return NodeStatus::FAILURE;
    }
  }
  // Skip if ALL the nodes have been skipped
  return (skipped_count == children_count) ? NodeStatus::SKIPPED : NodeStatus::RUNNING;
条件控制节点
基本概念

条件控制节点用于根据不同的条件来控制行为的流程,条件控制节点允许行为树根据不同的情况和条件,灵活地调整执行的路径和逻辑,在构建复杂的行为模式和处理多种可能性时非常有用。
这个就跟编程语言中的条件控制语句很类似。

WhileDoElseNode和IfThenElseNode容易混淆,二者的区别:

  • WhileDoElseNode用于创建循环,会在条件满足的情况下重复执行某个操作,而IfThenElseNode则用于根据条件的真假来选择性地执行不同的操作。这两种节点分别适用于不同的场景和逻辑需求。
  • WhileDoElseNode强调循环执行,而IfThenElseNode强调条件选择。
  • 从代码实现层面来看
    • WhileDoElseNode不管当前是否有子节点正在running,也不管是否已经执行过条件判断节点,每次都会执行第一个子节点用来做条件判断,并根据条件判断来选择继续执行哪一个子节点(如果另一个子节点正在running则直接打断)。
    • 而IfThenElseNode 只能执行一次条件判断节点(如果在当前控制流程中,条件判断子节点已经执行完毕,则不会重复执行),然后根据条件判断节点的执行结果来选择继续执行哪个子节点,子节点执行完毕则本次控制流程结束。

SwitchNode可以由外部其他节点通过黑板向其传参动态控制执行流程。

WhileDoElseNode
代码描述
// 每次tick该控制节点,从这里开始执行

const size_t children_count = children_nodes_.size();

// WhileDoElseNode只能有2个或3个子节点
if (children_count != 2 && children_count != 3) {
  throw "WhileDoElseNode must have either 2 or 3 children";
}

// 在tick子节点前先将自身状态设为running
setStatus(RUNNING);

// 执行第一个子节点
NodeStatus condition_status = child_node[0]->executeTick();

NodeStatus status = NodeStatus::IDLE;

if(RUNNING == condition_status) {
    // 如果第一个子节点返回running,则直接向上一级返回running
    return RUNNING;
} else if(SUCCESS == condition_status) {
    // 如果第一个子节点返回成功,则执行第二个子节点,同时重置第三个子节点
    resetChild(2); // 重置该节点,将节点状态重置为idle,如果节点状态为running则直接终止异步执行
    status = child_node[1]->executeTick();
} else if(FAILURE == condition_status) {
    // 如果第一个子节点返回失败,则执行第三个子节点,同时重置第二个子节点
    resetChild(1); // 重置该节点,将节点状态重置为idle,如果节点状态为running则直接终止异步执行
    status = child_node[2]->executeTick();
}

if(RUNNING == status) {
  // 如果动作执行子节(第二个或第三个子节点)点返回running,则向上一级返回running
  return RUNNING;
} else {
  // 如果动作执行子节(第二个或第三个子节点)点执行完毕,则向上一级返回执行状态,并重置全部子节点
  resetAllChildren(); // 重置全部子节点,子节点状态全部设置为idle,如果子节点中有处于running的,则直接强制终止
  return status;
}
流程图描述

在这里插入图片描述

SwitchNode
代码描述
template <size_t NUM_CASES> // case的数量

// 每次tick该控制节点,从这里开始执行

int match_index = int(NUM_CASES); // 设置默认执行的子节点
// 每次都要遍历用户是否有指定某个需要执行的子节点,如果用户没有指定那就执行默认子节点
// 可用来由外部其他节点传参控制该流程
if (getInput("variable", variable)) {
  for (int index = 0; index < int(NUM_CASES); ++index) {
    char case_key[20];
    sprintf(case_key, "case_%d", int(index + 1));
    bool found = static_cast<bool>(getInput(case_key, value));

    if (found && variable == value) {
        // 找到用户指定的子节点
        match_index = index;
        break;
    }
  }
}

static int running_child_ = -1;
// 查看之前是否有其他child节点(非本次选中的)在运行,如果有则终止
if (running_child_ != -1 && running_child_ != match_index) {
  resetNode(running_child_);
}

// 执行选定的子节点
NodeStatus ret = children_nodes_[match_index]->executeTick();
if (ret == NSKIPPED) {
  // 子节点返回skip,则直接向上级返回
  running_child_ = -1;
  return SKIPPED;
} else if (ret == RUNNING) {
  // 子节点返回running,则直接向上级返回running,并保存当前正在执行的子节点的index
  running_child_ = match_index;
  return RUNNING;
} else {
  // 子节点执行完毕,则重置所有,并返回执行结果
  resetAllChildrenNode(); // 重置全部子节点,子节点状态全部设置为idle,如果子节点中有处于running的,则直接强制终止
  running_child_ = -1;
}

return ret;
流程图描述

在这里插入图片描述

IfThenElseNode
代码描述
// 每次tick该控制节点,从这里开始执行
const size_t children_count = children_nodes_.size();

// if-then-else-node只能有两到三个节点
if (children_count != 2 && children_count != 3) {
  throw "IfThenElseNode must have either 2 or 3 children";
}

// 在tick子节点前先将自身状态设为running
setStatus(RUNNING);

static int child_idx_ = 0// 如果第一个子节点(条件判断节点)没有执行完毕,则继续执行条件判断
if (child_idx_ == 0) {
  // 执行条件判断子节点
  NodeStatus condition_status = children_nodes_[0]->executeTick();

  if (condition_status == RUNNING) {
    // 如果条件判断节点返回running,则向上一级返回running
    return RUNNING;
  }
  else if (condition_status == SUCCESS) {
     // 如果条件判断节点返回成功,则执行第二个子节点
    child_idx_ = 1;
  }
  else if (condition_status == FAILURE) {
    // 条件判断节点返回失败,如果有第三个节点则执行第三个节点,如果没有第三个节点则直接向上级返回失败
    if (children_count == 3) {
      child_idx_ = 2;
    }
    else {
      return FAILURE;
    }
  }
}

if (child_idx_ > 0) {
  // 如果有设置执行第二或第三个子节点,则直接执行
  NodeStatus status = children_nodes_[child_idx_]->executeTick();
  if (status == RUNNING) {
    // 如果子节点返回running,则向上一级返回running
    return RUNNING;
  }
  else {
    // 重置所有
    resetAllChildren(); // 重置全部子节点,子节点状态全部设置为idle,如果子节点中有处于running的,则直接强制终止
    child_idx_ = 0;
    return status;
  }
}

// 条件判断节点只能返回成功、失败、running三种类型
throw "Something unexpected happened in IfThenElseNode";
流程图描述

在这里插入图片描述

装饰节点

行为树中的装饰器节点用于修改或影响其子节点的行为。装饰器节点可以理解为对子节点施加一种“修饰”或“装饰”,以实现不同的效果。以下是几种常见的行为树装饰器节点:

  1. InverterNode(反转节点): 这个装饰器会反转其子节点的执行结果。如果子节点返回成功状态,反转节点会返回失败状态,反之亦然。这常常用于对某个行为的结果取反,例如如果原本的行为是失败,通过反转节点后就会变为成功。
  2. SucceederNode(成功节点): 这个装饰器总是返回成功状态,不论其子节点的执行结果如何。它用于将任何子节点的状态转化为成功,常常用于确保某个任务不影响整体行为的成功与否。
  3. RepeaterNode(重复节点): 这个装饰器会重复执行其子节点指定的次数,不论子节点的状态是什么。例如,如果子节点是一个行走操作,重复节点可以用来让实体连续走若干步。
  4. UntilFailNode(直到失败节点): 这个装饰器会重复执行其子节点,直到子节点返回失败状态为止。它通常用于确保某个行为一直执行,直到失败为止。
    这些装饰器节点能够让行为树更加灵活和复杂,通过对子节点的行为进行修饰,实现特定的行为模式和逻辑。它们在构建复杂的行为树时非常有用。
RepeatNode
代码描述
int num_cycles_ = xxx; // 期望重复次数,设为-1则为无限重复
static int repeat_count_ = 0bool do_loop = repeat_count_ < num_cycles_ || num_cycles_ == -1;

setStatus(RUNNING);

while (do_loop) {
  NodeStatus child_status = child_node_->executeTick();

  all_skipped_ &= (child_status == SKIPPED);

  switch (child_status) {
    case SUCCESS: {
      repeat_count_++;
      do_loop = repeat_count_ < num_cycles_ || num_cycles_ == -1;
      resetChild();
    }
    break;

    case FAILURE: {
      repeat_count_ = 0;
      resetChild();
      return FAILURE;
    }
    case RUNNING: {
      return RUNNING;
    }
    case SKIPPED: {
      resetChild();
      return SKIPPED;
    }
    case IDLE: {
      throw "A children should not return IDLE";
    }
  }
}

repeat_count_ = 0;
return all_skipped_ ? SKIPPED : SUCCESS;
流程图描述

在这里插入图片描述

RetryNode
代码描述

  
int max_attempts_ = xx; // 设置最大尝试次数,设为-1表示无限次尝试
static int try_count_ = 0;

bool do_loop = try_count_ < max_attempts_ || max_attempts_ == -1;

if(status() == IDLE) {
  all_skipped_ = true;
}
setStatus(RUNNING);

while (do_loop) {
  NodeStatus child_status = child_node_->executeTick();

  all_skipped_ &= (child_status == SKIPPED);

  switch (child_status) {
    case SUCCESS: {
      try_count_ = 0;
      resetChild();
      return SUCCESS;
    }

    case FAILURE: {
      try_count_++;
      do_loop = try_count_ < max_attempts_ || max_attempts_ == -1;

      resetChild();
    }
    break;

    case RUNNING: {
      return RUNNING;
    }

    case SKIPPED: {
        resetChild();
        return SKIPPED;
    }

    case IDLE: {
      throw "A children should not return IDLE";
    }
  }
}

try_count_ = 0;
return all_skipped_ ? SKIPPED : FAILURE;
流程图描述

在这里插入图片描述

InverterNode
代码描述
setStatus(RUNNING);
const NodeStatus child_status = child_node_->executeTick();

switch (child_status) {
  case SUCCESS: {
    resetChild();
    return FAILURE;
  }

  case FAILURE: {
    resetChild();
    return SUCCESS;
  }

  case RUNNING:
  case SKIPPED: {
    return child_status;
  }

  case IDLE: {
    throw "A children should not return IDLE";
  }
}
流程图描述

在这里插入图片描述

DelayNode
流程图描述

在这里插入图片描述

RunOnceNode
流程图描述

在这里插入图片描述

KeepRunningUntilFailureNode
流程图描述

在这里插入图片描述

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值