行为树BT设计与实现

行为树BT设计与实现

  • 1 介绍
  • 2 汇总
    • From McYY
      • 整体结构
      • 运行逻辑
      • 节点共享数据
      • 中断的实现
    • From AKara
    • From Luyu Huang
    • From 阿高
    • From xiyoo0812
  • 参考

1 介绍

状态机与行为树BT

2 汇总

From McYY

lua行为树设计与实现
LuaBT

项目需要,之前行为树用的是behaviorDesigner,要改成纯lua的。先做了一版用递归实现,代码可读性高但是中断机制实现起来比较复杂,而且创建自定义action重写方法时需要调用父类的方法, 如果忘了调用就会出现问题, 所以改成了用栈模拟递归。

用栈模拟递归好处在于效率高,并且容易控制,用非递归实现后自定义一个行为树节点,那么该节点不用知道父亲的方法,只要做好自己的事情就OK了,完整测试工程已上传到了github:https://github.com/MCxYY/LuaBT

整体结构

行为树BT设计与实现_第1张图片
行为树BT设计与实现_第2张图片
其中BTManager存储着所有行为树BTree,unity每帧调用BTManager的Update,而BTManager调用所有运行中的BTree的Update,BTree管理着自身的节点Task,根据逻辑执行调用Task的OnAwake()、OnStart等
Shared是节点共享数据,在后文中讲述
Task的OnAwake是整颗行为树激活时运行一次
OnStart是进入该Task时运行一次
OnUpdate是该Task执行中时每帧运行一次
OnPause(bPause)是整棵行为树暂停或者从暂停中苏醒时运行,bPause为true则暂停
OnEnd()是该Task退出时运行一次

运行逻辑

行为树(BTree)启动的时候调用BTree.Init()方法先序遍历该树,获得一系列节点数据,比如赋值儿子index,每个节点的儿子index是什么,每个节点的父亲index等。
行为树(BTree)中存储着一个list,这个是运行栈,行为树启动时创建一个运行栈,塞进去树根;每当有并行分支,则创建一个运行栈,塞进去分支第一个运行的节点。

节点(Task)的状态有四种:
1、ETaskStatus.Inactive //未激活
2、ETaskStatus.Failure //失败
3、ETaskStatus.Success //成功
4、ETaskStatus.Running //运行中
运行栈中放的节点都是处于Running状态,update时遍历运行栈,取出栈顶节点执行,如果节点执行完毕后状态不等于running,说明该节点不需要再次运行,那么就出栈。

节点运行的时候
如果该节点是ParentTask类型则需要运行儿子,其状态由儿子执行完毕后的状态来决定;
如果该节点是Task类型没有儿子,那么其状态就是其Update的状态;

节点共享数据

节点共享数据分为三种:一,树之间任意节点全局共享的数据 二,树内任意节点共享的数据 三,节点内不共享数据
节点内数据那就不用说了,在节点内声明的数据都是节点内数据
BehaviorDesigner的共享数据是通过编辑器保存读取的
由于时间不允许,没有做编辑器,所以我就做了个存储的类简单的实现了下
Shared.lua就是存储的类,其实里面就是一个table,对外只提供一个GetData(name)的方法,如果没有name的变量就创建个值为空的table保存起来,返回这个table。之所以用table存,是因为table在lua中属于引用类型。

中断的实现

中断的实现应该是行为树中比较复杂的功能了,涉及到树上的一些算法及运行栈的操作,牵涉到的判断也多,这里会重点讲述。

中断必要的原因是可能存在以下情况(不限于此情况):
比如怪物正在向目标点移动的时候遇到玩家需要攻击,此时移动的节点状态是running,没有中断的时候只能走到目标点的时候返回success停止移动才开始进入其他节点,这时候就错过了攻击玩家,中断的作用就体现出来了,就算是在running状态也能打断运行栈进入攻击节点。
BehaviorDesigner打断的方法是将打断类型分为这么几类:
EAbortType = {
  None = 0, //不打断
  Self = 1, //打断自身
  LowerPriority = 2, //打断低优先级
  Both = 3, //同时包括Self和LowerPriority两种效果
}
其中只有Composite类型的节点可以拥有打断操作。
Self打断类型:指的是Composite节点下面的直系子孙(这个名词是我临时取得。。意思是Composite与Conditional中间可以插入Decorate,可以插入Composite但插入得Composite类型必须是Self或Both)Conditional类型的节点的状态发生变化时,那么打断正在运行且是Composite子孙的分支,重新进入变化的Conditional节点所处的分支中。打断的结构大概如下图所示:
行为树BT设计与实现_第3张图片
(绿色的指正在运行中的节点)

From AKara

使用行为树(Behavior Tree)实现游戏AI

谈到游戏AI,很明显智能体拥有的知识条目越多,便显得更智能,但维护庞大数量的知识条目是个噩梦:使用有限状态机(FSM),分层有限状态机(HFSM),决策树(Decision Tree)来实现游戏AI总有那么些不顺意。试试Next-Gen AI的行为树(Behavior Tree)吧。
行为树BT设计与实现_第4张图片
行为树(Behavior Tree)具有如下的特性:
有4大类型的Node:

  • Composite Node
  • Decorator Node
  • Condition Node
  • Action Node

任何Node被执行后,必须向其Parent Node报告执行结果:成功 / 失败。
这简单的成功 / 失败汇报原则被很巧妙地用于控制整棵树的决策方向。

From Luyu Huang

行为树BT设计与实现_第5张图片
在笔者的项目中 NPC 要有自动化的行为, 例如怪物的巡逻, 寻敌和攻击, 宠物的跟随和战斗等. 完成这些需求最好的做法是使用行为树(Behavior Tree)。
行为树首先是一棵树, 它有着标准的树状结构: 每个结点有零个或多个子结点, 没有父结点的结点称为根结点, 每一个非根结点有且只有一个父结点。 在行为树中, 每个节点都可以被执行, 并且返回 Success, Failure 或 Running, 分别表示成功, 失败或正在运行。行为树会每隔一段时间执行一下根结点, 称为 tick。 当一个节点被执行时, 它往往会按照一定的规则执行自己的子节点, 然后又按照一定的规则根据子节点的返回在确定它自己的返回值。行为树通常有 4 种控制流节点(Sequence 节点, Fallback 节点, Parallel 节点和 Decorator 节点)和 2 种执行节点(动作节点和条件节点):

  • Sequence 节点(顺序
    每当 Sequence 节点被执行时, 它都会依次执行它的子节点, 直到有一个子节点返回 Failure 或 Running. Sequence 节点的返回值就是最后一个子节点的返回值。
    Sequence 节点有点像逻辑与的操作: 只有所有的节点返回成功它才返回成功。 我们通常用符号 “→” 表示 Sequence 节点.

  • Fallback 节点
    每当 Fallback 节点被执行时, 它都会依次执行它的子节点, 直到有一个子节点返回 Success 或 Running. Fallback 节点的返回值就是最后一个子节点的返回值。
    与 Sequence 节点相反, Fallback 节点有点像逻辑或的操作: 只要有一个节点返回成功它就返回成功. 我们通常用符号 “?” 表示 Fallback 节点。

  • Parallel 节点(并行
    每当 Parallel 节点被执行时, 它都会执行它所有的子节点。如果有至少 M 个节点返回 Success, Parallel 节点就返回 Success; 如果有至少 N - M + 1 个节点返回 Failure, Parallel 节点就返回 Failure, 这里 N 是其子节点的数量; 否则返回 Running。
    我们通常用符号 “⇉” 表示 Parallel 节点。

  • Decorator 节点(修饰
    有的时候会有一些特殊的需求, 需要用自己的方式执行子节点和处理其返回结果。 Decorator 节点就是为此而设计的, 它的行为都是自定义的. 可以说, Sequence, Fallback 和 Parallel 节点都是特殊的 Decorator 节点。 我们通常用 “δ” 表示 Decorator 节点。

  • 动作节点和条件节点
    一般来说, 动作节点和条件节点是行为树中的叶子节点, 它们都是根据具体需求具体实现的. 当动作节点被执行时, 它会执行一个具体的动作, 视情况返回 Success, Failure 或 Running. 当条件节点被执行时, 它会做一些条件判断, 返回 Success 或 Failure. 行为树并不关心一个节点具体做了什么事 – 是所谓的 “执行动作” 或是 “判断条件”, 所以说它们唯一的区别就是动作节点有可能会返回 Running 而条件节点不会。

  • 带记忆的控制流节点
    正如上面我们看到的, 控制流节点在每次 tick 的时候都会依次执行其所有的子节点并获取其返回值. 然而有时对于某些节点, 一旦执行了一次, 就不必再执行第二次了。 记忆节点便是用来解决这一问题的。在控制流节点中, Sequence 节点和 Fallback 节点可以是带记忆的. 当子节点返回 Success 或 Failure 时, 记忆节点总是会把返回值缓存起来; 一旦一个子节点的返回值被缓存了, 就不会执行这个子节点了, 而是直接取缓存中的值; 直到这个节点返回 Success 或 Failure, 便清空缓存。
    记忆节点有一些非常巧妙的应用. 我们通常在节点的右上角加上 * 号表示这个节点是记忆节点. 比如说记忆 Sequence 节点记作 “→∗”。

From 阿高

基于Unity行为树设计与实现的尝试
行为树的设计与实现

1、行为树只是单纯的一棵决策树,还是决策+控制树。为了防止不必要的麻烦,我目前设计成单纯的决策树。
2、什么时候执行行为树的问题,也就是行为树的 Tick 问题,是在条件变化的时候执行一次,还是只要对象激活,就在Update里面一直Tick。前者明显很节省开销,但那样设计的最终结果可能是最后陷入事件发送的泥潭中。那么一直Tick可能是最简单的办法,于是就引下面出新的问题。目前采用了一直Tick的办法。
3、基本上可以明显节点有 Composite Node、 Decorator Node、 Condition Node、 Action Node,但具体细节就很头疼。比如组合节点里的Sequence Node。这个节点是不是在每个Tick周期都从头迭代一次子节点,还是记录正在运行的子节点。每次都迭代子节点,就感觉开销有点大。记录运行节点就会出现条件冗余问题,具体后面再讨论。目前采用保存当前运行节点的办法。
4、条件节点(Condition Node)的位置问题。看到很多设计都是条件节点在最后才进行判断,而实际上,如果把条件放在组合节点处,就可以有效短路判断,不再往下迭代。于是我就采用了这种方法。
行为树BT设计与实现_第6张图片
行为树BT设计与实现_第7张图片

From xiyoo0812

参考

1、McYY–lua行为树设计与实现
2、MCxYY/LuaBT
3、使用行为树(Behavior Tree)实现游戏AI
4、行为树及其实现
5、基于Unity行为树设计与实现的尝试
6、【Godot】行为树(一)了解与设计行为树代码
7、xiyoo0812/luabt/–gitee
8、xiyoo0812/luabt/–github
9、xiyoo0812/luaoop

你可能感兴趣的:(行为树,lua,行为树)