欢迎捉虫!
之前我研究了一下基于switch case语句的FSM状态机的使用,后来遇到了很多问题。
比如当角色的行为很多时,代码结构相当混乱(你需要考虑每一种状态之间的联系)。
所以,当角色的行为愈发的复杂,状态机的设计图就越像一坨蜘蛛网,维护是状态机所需的成本也就越高,这对于开发者来说显然很麻烦。
所以,在查找了许多资料后,我发现了行为树这一利器,于是好好学习了一番。然后发现,这玩意不仅是游戏开发的利器,对于游戏策划而言也是必不可少。
行为树到底是个啥?他的运作机制是什么?我该如何利用行为树来设计AI和人物运动脚本?
0 前言
更准确的说,行为树其实是一种反应型AI,这种AI人为控制性非常高,也意味着开发者要将AI的行为规划好,而这种规划方式之一便是行为树。(还有一种就是我们之前讲的FSM有限状态机)
比如这个就是一种行为树,显而易见的是,行为树的结构相当清晰,比一坨状态机好维护多了。
上图是一个有向无环图,也就是他会顺着箭头的指向走,而不会循环。箭头指向的各个动作会自动判定执行。
当然,实际上行为树的实现逻辑可没有这么简单
准备好,我们开始。
1.0 行为树的组成
1.1最简单的行为树
我们先来画一个只有两个元素组成的行为树
在上图中,红色的点叫做父节点(Parent),蓝色的点叫做子节点(Children)。
对于任意一个节点,指向它的节点就是他的父节点,它指向的节点就是他的子节点。
明白父子节点后,我们要思考,父子节点之间是通过何种关系来协作运作的。于是,我们就要介绍一下——行为树的工作流。
1.2行为树的工作流
为什么要引入工作流呢?这与行为树之间的关联有关。只有通过工作流,我们才能把行为树中的各个部分给联系起来。
工作流这个名词非常高级,但是在行为树里面就比较通俗了,其实质就是一种布尔值(true和false)不过另有区别,分为下面三种:
成功(Success)
失败(Failure)
运作中(Running)
根据他们的名字就非常好理解了,子节点会向其父节点返回前两种(Success)和失败(Failure)状态,告诉父节点其结果是成功还是失败。
第三种(Running)则有点特殊,他会让子节点始终保持这一状态。
了解了工作流之后,我们可以进行行为树各个部分的学习了。
1.2行为树的组成部分
首先我们来看一个比较完整的行为树
现在我们肯定是不能理解行为树里的内容具体是啥的,所以我们不妨一个一个来进行理解。
1.2.1根节点(Root)
根节点只有子节点没有父节点,行为树的每一次启用都从根节点开始。
1.2.2叶节点(Leaf)
和根节点(Root) 相反,叶节点只有父节点,没有子节点。
叶节点里是所需要执行的命令。
1.2.3复合节点之顺序节点(Sequence)
顺序节点可以用于决定多个叶节点之间的执行顺序。
比如我们有这个顺序节点及其子节点
可以看到两个子节点的执行会需要一定顺序,而这个顺序受到顺序节点的控制,当他进行工作时,他的运作是这样的。
次序节点 ->走到门旁边(成功)->次序节点->打开门(成功)->次序节点
如果中途某一个动作因为某些原因失败了,那么就会直接返回给顺序节点Failure,后续动作都不会执行,而是其进行其他动作。
1.2.4复合节点之选择节点(Selector)
选择节点会对当前游戏状态进行一个评定 ,评定后选择接下来所需要进行的子节点。
只有当所有子节点都返回Failure时,选择节点才会返回Failure,否则若有返回Success的子节点,选择节点就会返回Success
比如我们有这个选择节点及其子节点
我们试图利用这样的逻辑来设计一个会主动抓住玩家,并在无法抓住玩家时自爆的敌人行为AI。
在这套行为树下,敌人会先试图直接抓住玩家,如果成功了,那么其他的什么跑向玩家和自爆就没有执行的必要;
如果敌人无法直接抓住玩家,那么选择节点就会执行顺序节点里的内容;如果最后顺序节点都返回失败(注意:顺序节点中只要子节点有一项返回失败,顺序节点就会返回失败),那么选择节点就会执行到自爆,可怜的敌人结束了自己的生命。
当然,如果自爆都无法做到……那是后话了。或许我们可以再为他添加一个父节点或子节点来解决这个问题。
1.2.5随机复合节点(Random Nodes)
也就是随机顺序节点或随机选择节点。这两种节点的运作模式与他们的非随机版本一模一样,除了其在选择执行的叶节点上随机。
在此不再赘述。
1.2.6装饰节点(Decorator Nodes)
装饰节点非常特殊……
好吧好吧,行为树里有什么东西不特殊吗?那请问装饰节点到底是干啥的捏?
您先别急,装饰节点是一个大类,我们分开来看。
1.2.6.1逆变节点(Interver)
逆变,顾名思义,他会把叶节点返回的值倒置。
比如说叶节点千辛万苦返回了成功,然后逆变节点就会说:哦哦,你成功了啊,很不错!然后报告失败给他的父节点。
1.2.6.2成功节点(Succeeder)
成功节点比较官僚主义,不管他的下属叶节点返回失败还是成功,他都会返回成功这一结果给他的上级父节点。赢!
1.2.6.3重复节点(Repeat)
顾名思义,重复节点会在子节点返回结果后决定是否反复执行他。通常我们会把重复节点放在一个树的最顶部用来保证行为树会不断运行。
而且还可以设定重复节点的重复次数。
1.2.6.4重复至失败节点(Repeat until Fail)
和上面的重复节点类似,不同的是他会在子节点返回Failure时给他的父节点返回Failure,这是个实话实说的好官呢!
很好,我们虽然还是不清楚整个行为树的逻辑,但我们起码已经知道他的每一个节点是用来干嘛的了。
2.0行为树的执行
2.1行为树的遍历(tick)
在行为树刚被发明出来时,行为树会在每一帧都进行一次遍历,也就是从根节点开始,逐层检测子节点的活跃状态,最后根据叶节点的活跃状态决定其执行内容。
这样的一个逐层过程叫做一个tick
2.2定义叶节点
尽管我们已经拥有了一颗行为树,我们也还是不能让这颗行为树运作起来——我们还没有告诉行为树的叶节点他的任务呢!所以为了让行为树运作起来,我们必须定义每一个叶节点的功能。
绝大多数的行为树都会包含以下两个功能。
初始化,执行。
2.3.1初始化(Init)
这个可以参考Unity中的Start()方法,其功能有些相似。
在这个节点首次被其父节点访问时,初始化程序就会被调用,在父节点的所有子节点都完成流程之前,初始化程序都不会再调用。
通过初始化,节点可以获取各种参数用于功能的执行。
你看,是不是很像Start()方法。
2.3.2执行(Process)
和初始化不同的是,执行方法会在这个节点每一次被访问时调用(每一个tick都会调用)。
比如如果某个节点不断返回Running的结果,那么这个节点就会不断的进行Process,节点的功能会持续执行直到Success或者Failure。
除了这两种功能上的定义需求以外,参数的传入也很重要
2.3.3参数传入
和定义方法的传入数据一样,传入类似于Charactor Tag,Velocity这样的数据用于节点的执行,在此不再赘述。
好的,我们已经知道了行为树的组成成分和行为树的执行原理,但是我们要如何搭建一个行为树呢?用代码撸一个吗?能不能像图示一样可视化操作呢?
当然是能!接下来,我们来了解下什么叫黑板(Blackboard)
3.0黑板(Blackboard)
并非所有行为树都有黑板?我也不懂人工智能,所以我不是很清楚,但我这里的黑板指的是UE或者Unity中Behaviour Tree的黑板
3.1黑板(Blackboard)是什么
这个黑板与我们上高数课和线代课的哪个黑板不同。
在上文中,我们介绍了行为树的参数传入,我们说他的传输模式类似于方法的参数传入,其实这样说并不准确。
我们可以这样想,如果我们有一个比较庞大的行为树,那么他的数据传入借口是否有些过多过于复杂了?所以我们就要思考,能不能用一个结构体或者什么其他的从行为树的各个模块中提取数据出来,然后实现行为树之间的数据共享?
于是,黑板(Blackboard)来了。
我们可以用一个黑板从行为树的各个模块中获取各个参数,然后又反过来为行为树提供所需参数。这样就能实现行为树的参数获取和行为树中各个模块之间的数据通信了!
以上便是我对于游戏AI行为树运作原理的理解了,具体的使用方法要分不同的行为树插件而定(README里通常会有介绍)
如果有什么地方理解有误的话,欢迎指出!