AI行为树的工作原理

最近在项目中应用到了行为树,在网上看了不少关于行为树的文章,其中有一篇文章我觉得写得非常到位,它的原文是英文。在这篇文章里我会它的大致意思给翻译出来(注:由于原文有不少啰嗦的地方,所以没有逐句的翻译),以它的行为结构为标准,并会在译文中穿插一些自己的理解来谈谈我眼中的行为树。

原文链接

一、介绍



尽管网上已经有大量关于行为树的教程,但是在开发游戏中的 AI 时还是遇到了不少关于行为树的问题。很多教程不关注于代码的实现,而是给出一些非常笼统的节点图,就像这样:

这些节点看起来非常的抽象,这对于我了解行为树的运作机制没有什么帮助,因为这样完全建立不起一个完整行为树的概念。自然也就更不知道如何在代码中进行行为树的实现。

行为树的概念见名知意,它是一棵具有层级结构的树,主要用来控制 AI 的决策,树的末端(注:叶子节点)就是 AI 实际要去做的事情。连接树枝的是各种类型的节点(注:这些节点将在后面讲解),这些节点决定了如何一个 AI 如何从根节点走到某一个叶子节点,并执行相应的命令操作。

二、树的遍历



行为树的一个特点就是它会一层一层的对节点依次进行检查,而每一层都要花费一个 Tick 的时间,所以一棵树要花费很多 Tick 的时间才能完成遍历,这和一般代码的实现有很大的区别。这是一个很没有效率的方式,尤其是当树变得很深的时候。我认为行为树的实现最好要在一次 Tick 里面完成整个行为树的逻辑判断,也就是说最好能在一次 Tick 时间里完成整棵树的遍历。

三、工作流



行为树由多种不同类型的节点组成,这些节点都会返回三种状态中的一种作为节点的运行结果。三种状态分别是:

  • 成功 - Success
  • 失败 - Failure
  • 运行中 - Running

前两个,正如它们的名字一样,是用来向它们的父节点通知运行的成功或失败。第三种是指还在运行中,结果还未确定,会在下一个 Tick 的时候再去检查这个节点的运行结果。这个功能非常重要,它可以让一个节点持续运行一段时间来维持某些行为。比如一个 Walk 节点会在计算寻路和让角色保持行走的过程中持续返回 Running 来让 AI 保持这一状态。如果寻路因为某些原因失败,或是除了某些状况让行走的行为不得不中止,那么这个节点会返回 Failure 来告诉它的父节点;如果这个角色走到了指定的目的地,那么节点返回 Success 来表示这个行走的指令已经成功完成。这些状态可以用来决定行为树的走向,确保 AI 可以按照我们预期的方式来以某些顺序去执行行为树里的行为。

上面是节点的 3 种状态。说完了节点的 3 种状态,下面来说一说行为树中节点的类型。行为树节点类型分为下面 3 种:

  • 组合节点 - Composite
  • 修饰节点 - Decorator
  • 叶子结点 - Leaf

这些节点类型可以说是行为树最精华的内容,下面将分别对这 3 种类型的节点做说明。

1、组合节点

我们先来看看行为树中最常见的组合节点。组合节点通常可以拥有一个或更多的子节点。这些子节点会按照一定的次序或是随机地执行,并会根据执行的结果向父节点返回 Success 、Failure,或是在未执行完毕时返回 Running 这样的结果值。

通过使用组合节点,我们就可以构造出复杂的行为树。其中组合节点又包括下面几种节点:

  • 次序节点
  • 选择节点
  • 并行节点

(1)次序节点

次序节点会按从左往右的顺序依次执行子节点,每个子节点成功之后便轮到下一个执行,直到最后执行完该节点下的全部子节点。如果所有子节点都返回 Success,则向次序节点的父节点返回 Success;其间任何一个子节点返回 Failure,就会立即向次序节点的父节点返回 Failure 的结果。这种性质和与门(AND gate) 是一致的。次序节点有很多的用处,其中最常用的做法就是执行一连串有前后依存关系的行为,其中一个的失败必然导致后续的动作没有进行的意义,比如下面这个例子:

这个次序节点让 AI 角色实现了从走向门、进门、关门等一系列连续动作。整个过程可以描述成这样:次序节点 ->Walk to Door (Success) ->次序节点(Running) ->Open Door (Success)  ->次序节点(Running) ->Walk through Door (Success) ->次序节点(Running) ->Close Door (Success) ->次序节点(Running) -> 向次序节点的父节点返回 Success。如果 AI 因为某些原因未能成功走到门前,比如路被挡住了之类的,那么试图开门这些动作都没有意义了。即当 Walk to Door 这个动作失败后,次序节点就会向其父节点返回 Failure。注意这里不是向次序节点返回 Success,可以将次序节点理解成就是一个桥梁的作用,真正想得到这一连串执行结果的是该次序节点的父节点。

次序节点除了非常自然地用于进行一系列前后依存的动作之外,还可以用来做一些其他的事情,比如:

在上面这个例图中,次序节点的子节点不是一系列动作而是一系列的检查。这些子节点会检查 AI 角色是不是饿了,有没有食物,是不是在安全的地点,只有在它们都返回 Success 时,角色才会吃东西。这样使用次序节点可以实现类似于代码中 IF 判断和与门(AND gate)的效果。这些用于判断的子节点可以是其他的组合节点或是修饰节点等等来实现更丰富的效果。比如下面这个使用了逆变节点(注:逆变节点是一种装饰节点,后文阐述)的例子:

尽管功能和前面的例子完全一样,但是通过逆变节点我们在这里创建了一个非门(NOT gate),只有在“Enemies Around(敌人在周围)”这个条件返回 Failure 时,这一步才会返回 Success,从而让角色继续进行吃东西的动作。这意味着这些节点的组合可以减少很多不必要的开发量。

(2)选择节点

选择节点就是次序节点的反面。作为与门(AND gate)的次序节点要求子节点都返回 Success 来让自己返回 Success,选择节点则会在任何一个子节点返回 Success 时就返回 Success 并且不再继续运行后续的子节点。相应的,当所有子节点都 Failure 时,选择节点才会返回 Failure。选择节点其实可以被理解为一个或门(OR gate)。

它的主要作用在于它可以用来表示一个行为的多种方式,从最高优先级到最低,任何一个方式的成功都会让这个动作 Success,比如 Attack(攻击) 节点下的有 Hack、Cut,Chop三个子节点,每当这个 AI 尝试攻击时,都会从这三个中选择一个来执行,只要任何一个子节点执行成功,该 Attack 节点就返回成功。

(3)并行节点

我们每个节点都会有一个运行状态,来表示当前行为是否结束。对于组合节点来说,它的运行状态就是其子节点的运行状态,选择节点和次序节点比较好处理,因为对于这两种控制节点来说,每时刻,只会有一个子节点在运行,只要返回在运行的这个子节点的状态即可。但对于并行节点来说,它同时刻会有多个子节点运行。

2、修饰节点

修饰节点也可以拥有子节点,但是不同于组合节点,它只能拥有一个子节点。取决于修饰节点的类型,它的功能要么是修改子节点返回的结果、终止子节点,或是重复执行子节点等等。常见的修饰节点有以下几种:

  • 逆变节点
  • 成功节点
  • 重复节点

(1)逆变节点

Inverter(逆变节点)可以将子节点的结果倒转,比如子节点返回了 Failure,则这个修饰节点会向上返回 Success,就像上面的例子里所说的一样。

(2)成功节点

成功节点不管它的子节点向其返回的结果为何,它总是向它的父节点返回 Success 的结果。这个往往用在当你知道一个子节点一定会返回 Failure 的结果,而它的父节点是次序节点,会因为子节点的 Failure 而终止,那么你可以强行让这个子节点返回 Success,来避免这一情况的发生。我们并不需要一个专门的失败节点,因为一个逆变节点加上成功节点就可以达到这一效果。

(3)重复节点

重复节点会在它的子节点返回结果后反复继续执行它。重复节点常常被用在一棵树的最顶部来确保树的持续运行。另外重复节点也可以被设定重复执行的次数。

3、叶子节点

叶节点是最低层的节点,它们不会拥有子节点。叶节点是最强大的节点类型,它们是真正让你行为树做具体事情的节点。通过与组合节点和修饰节点的配合,再加上你自己对叶子节点功能的定义,你可以实现非常复杂的、智能的 AI 行为逻辑。拿代码作为类比的话,组合节点和修饰节点就好比那些改变代码 Flow( 工作流程 ) 的 If 判断和 While 循环等等,而叶节点就是那些真正起作用的被调用的方法,去让角色做具体的事情。

比如一个 Walk 叶子节点可以包含一个具体将要移动到的位置参数,而这个参数可以从其他变量里获得,比如角色将要前往的一个地点参数可以被 GetSafeLocation 这个节点所决定,存入一个变量里,然后 Walk 节点可以使用这个变量来定义它的目的地。在行为树中, 这样将一个节点的数据用在其他节点上的行为, 用好了是非常强大的, 行为树的运行中,这些不同的节点通过数据上下文来共同储存或使用一些持久数据(persistent data),使得行为树的功能变得强大。另一种叶节点的类型是调用其他的行为树并把当前行为树的数据传给对方,可见一种是获得数据另一种是产生数据。

原文地址:https://www.cnblogs.com/KKSJS/p/9690098.html

时间: 2024-10-09 18:45:49

AI行为树的工作原理的相关文章

How Javascript works (Javascript工作原理) (十四) 解析,语法抽象树及最小化解析时间的 5 条小技巧

个人总结:读完这篇文章需要15分钟,文章介绍了抽象语法树与js引擎解析这些语法树的过程,提到了懒解析--即转换为AST的过程中不直接进入函数体解析,当这个函数体需要执行的时候才进行相应转换.(因为有的函数体只是声明了,并没有实际被调用) 解析,语法抽象树及最小化解析时间的 5 条小技巧 这是 JavaScript 工作原理的第十四章. 概述 我们都知道运行一大段 JavaScript 代码性能会变得很糟糕.代码不仅仅需要在网络中传输而且还需要解析,编译为字节码,最后运行.之前的文章讨论了诸如 J

(转)AJAX工作原理及其优缺点

1.什么是AJAX?AJAX全称为"Asynchronous JavaScript and XML"(异步JavaScript和XML),是一种创建交互式网页应用的网页开发技术.它使用:使用XHTML+CSS来标准化呈现:使用XML和XSLT进行数据交换及相关操作:使用XMLHttpRequest对象与Web服务器进行异步数据通信: 使用Javascript操作Document Object Model进行动态显示及交互: 使用JavaScript绑定和处理所有数据. 2.与传统的we

Android学习笔记View的工作原理

自定义View,也可以称为自定义控件,通过自定义View可以使得控件实现各种定制的效果. 实现自定义View,需要掌握View的底层工作原理,比如View的测量过程.布局流程以及绘制流程,除此之外,还需要掌握View常见的回调方法.而对于那些具有滑动效果的自定义View,我们还需要处理View的滑动,如果遇到滑动冲突则需要处理相应的滑动冲突. 下面是View的常见回调方法: 构造方法 onAttach onVisibilityChanged onDetach onFinishInflate on

SQL索引工作原理

SQL 当一个新表被创建之时,系统将在磁盘中分配一段以8K为单位的连续空间,当字段的值从内存写入磁盘时,就在这一既定空间随机保存,当一个8K用完的时候, SQLS指针会自动分配一个8K的空间.这里,每个8K空间被称为一个数据页(Page),又名页面或数据页面,并分配从0-7的页号,每个文件的第0页记录引导信息,叫文件头(File header):每8个数据页(64K)的组合形成扩展区(Extent),称为扩展.全部数据页的组合形成堆(Heap). SQLS 规定行不能跨越数据页,所以,每行记录的

Java HashMap工作原理及实现

原文出处: Yikun 1. 概述 从本文你可以学习到: 什么时候会使用HashMap?他有什么特点? 你知道HashMap的工作原理吗? 你知道get和put的原理吗?equals()和hashCode()的都有什么作用? 你知道hash的实现吗?为什么要这样实现? 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办? 当我们执行下面的操作时: 1 2 3 4 5 6 7 8 9 10 11 12 HashMap<String, Integer> map =

Struts2(二):工作原理

struts可查看源码:https://github.com/apache/struts 在学习struts2之前,我先看了一些比较早版本对struts2的工作原理相关的介绍,顺便抄写过来,用来帮助自己要弄清这个框架的工作原理. struts2.1.3之前使用的FilterDispatcher,之后的版本使用StrutsPrepareAndExecuteFilter,而我这里还是以Dispatcher为例来记录的. 依据: Since Struts 2.1.3, use org.apache.s

[转]AJAX工作原理及其优缺点

1.什么是AJAX?AJAX全称为“Asynchronous JavaScript and XML”(异步JavaScript和XML),是一种创建交互式网页应用的网页开发技术.它使用:使用XHTML+CSS来标准化呈现:使用XML和XSLT进行数据交换及相关操作:使用XMLHttpRequest对象与Web服务器进行异步数据通信: 使用Javascript操作Document Object Model进行动态显示及交互: 使用JavaScript绑定和处理所有数据. 2.与传统的web应用比较

hdfs的伪分布式的工作原理

[简介] 1.hdfs架构 hdfs伪分布式架构只需要有三个部分即可,NameNode是老大,DataNode是小弟,Secondary NameNode是助理. 客户端Client跟NameNode通信(RPC通信机制,后面会介绍),Secondary NameNode负责数据的同步. 2.元数据的存储细节 NameNode的元数据是存放在内存当中的. 数据解读:有一个文件/test/a.log,保存了3个副本,一共被切分成了两块,第一块分别存放在了那几个地方,第二块存放在了那几个地方. 客户

Linux系统开机启动时的工作原理

Linux系统开机启动时的工作原理也是深入了解Linux系统核心工作原理的一个很好的途径. 启动第一步--加载BIOS 当你打开计算机电源,计算机会首先加载BIOS信息,BIOS信息是如此的重要,以至于计算机必须在最开始就找到它.这是因为BIOS中包含了CPU的相关信息.设备启动顺序信息.硬盘信息.内存信息.时钟信息.PnP特性等等.在此之后,计算机心里就有谱了,知道应该去读取哪个硬件设备了.在BIOS将系统的控制权交给硬盘第一个扇区之后,就开始由Linux来控制系统了. 启动第二步--读取MB