一、重点提示
1、行为树是一种逻辑工具,对工具的学习方法肯定是实用优先。
特地说这个是因为Behavior Designer提供的功能其实比我们要用的多。作为使用者,务必记住要先把基本功能搞清楚,在初期那些不必要的高级功能只会把我们的思路搞乱而已。设计AI本身已经是很烧脑的工作,不建议使用一些很不直观的修饰器和组合器给自己添乱。而且基本功能已经足够我们组合出非常复杂而强大的行为树了。
2、行为树中的节点,会在某一帧中被调用,然后立即得到一个结果:成功Success、失败Failure、运行中Running,只能取三者其一。然后组合器和修饰器会根据返回值进行下一步,这是行为树的基本逻辑。3、节点不是多线程并行的,被调用的节点都必须迅速执行完毕并返回Running、Success或者Failure。所有事件就算是同时发生,也总有先后之分。
二、组合器的详细介绍
注:本段先略读一遍,然后下一段咱们会做实验,边实验边阅读效果更佳。
Sequence 串行的AND
Sequence 类似于编程语言中的"&&"符号,它从左到右,每帧只执行一个子节点。
1、如果当前子节点返回Running,那么Sequence也返回Running。下一帧继续执行当前这个子节点。
2、如果当前子节点返回失败,那么Sequence节点本身返回失败。
3、如果当前子节点返回成功,如果还有下一个子节点,那么Sequence本身返回Running,下一帧会切换到下一个子节点; 如果所有子节点都完毕了,则Sequence节点返回成功,整个节点结束。
Selector 串行的OR
Selector与Sequence执行顺序相同,逻辑正巧是“||”的逻辑。它也是从左到右,每帧只执行一个子节点。
1、如果当前子节点返回Running,那么Selector也返回Running。下一帧继续执行当前这个子节点。
2、如果当前子节点返回失败,那么Selector节点本身返回Running,下一帧执行下一个子节点;如果所有子节点都失败了,就返回失败。
3、如果当前子节点返回成功,那么Selector返回成功。
Parallel 并行的AND
Parallel 从返回值来看它是 “&&” 逻辑。与Sequence的区别是,在每一桢,它都执行所有子节点一次
1、所有子节点都Running,那么Parallel节点也返回Running。
2、有任何一个节点返回失败,那么Parallel立刻结束,返回失败。还处于Running的子节点也会终止(从界面上可以看出,正在Running的被假设为失败)。
3、有任何一个节点返回成功,那么该子节点下一帧就不会被调用了,但是Parallel本身仍然返回Running,直到所有子节点都返回成功,Parallel才返回成功。
Parallel Selector 并行的OR
Parallel Selector 从返回值来看是 “||” 逻辑。它是并行的,每一桢执行所有子节点一次
1、所有子节点都Running,那么Parallel Selector节点也返回Running。
2、有任何一个节点返回失败,那么Parallel Selector 本身返回Running,直到所有子节点都失败了,它才返回失败。
3、有任何一个节点返回成功,Parallel Selector 直接返回成功。
好的,我们解释了四种最基本的节点,只需它们就足够组成行为树的骨架。下图是Composites全图,我给它们分了组,前面介绍的就是最上面一排基本组。
image.png其它节点就容易了,我们继续看看:
Random Sequence 变体的Sequence(串行)
Sequence是从左到右串行,Random Sequence 也是串行完全一样,只是它从还没执行过的N个子节点中随机挑选一个执行。
Priority Selector, Random Selector 变体的Selector(串行)
这二者是Selector的变体,也都是串行。分别是根据优先级挑选、随机挑选、自定义挑选顺序。
★ 再强调一下,串行情况下,如果有节点还在running,那么肯定先执行running的节点。“挑选”的意思是说,在没有running的节点时,从还没执行过的节点中,根据规则挑出一个。
Selector Evaluator, Utility Selector 特殊顺序的Selector
这两种类型的特殊之处在于:在每一帧,都要重新计算子节点的优先级或者效用,就算节点正在running,也有可能因为优先级变化而切换节点。它们既不是并行也不是串行。Utility Selector 是一种选择器,它是基于“Utility”也就是“效用”进行选择,用在《模拟人生》这种游戏中会非常有效,就是当你面对吃法、睡觉、上厕所这三件事时,你选效用最大的那一件事去做即可,而且如果有必要可以随时终止当前正在做的事情。Selector Evaluator 涉及到优先级的问题,暂且不表。
三、组合器和返回值实验+详解
前面的讲解过于抽象,咱们可以做下面这样的一个行为树,挂在任意一个GameObject上面,直观感受各种组合器的特性:
image.png
说明:新建GameObject到场景中,并为它创建一个行为树如上图。四个动作节点是Wait节点,在行为树的Inspector窗口里,把Wait节点的等待时间分别改为2、1、2、3秒。然后执行效果如下:
image.gif
通过一些简答的例子,瞬间就对组合器功能有了直观认识。之后再看前面的介绍,就可以掌握这些组合器的用法了。
image.jpeg
小技巧:用Replace功能即可快速替换节点类型。
还有修饰器的作用就不再详细表述了,大概列举如下:
1.Inverter:条件判断或动作的返回结果取反,成功变失败,失败变成功,Running不变。
2.Reapter:循环执行,可以调节循环次数等参数。
3.Return Failure:返回值无论成功或失败都返回失败,但Running还是Running。
4.Return Success:同上,相反。Until Failure:循环直到失败,换句话说如果成功就再次执行子节点。5.Until Success:同上,相反。
四、变量应当保存在行为树中,还是角色脚本中?
思考这样一个问题:如果我们不用Bahavior Designer插件,那么脚本中也有各种角色相关的参数和变量,而如果用了插件,那么也可以把变量放在行为树里面,就好比之前用过的SharedTransform等等变量。选择多了麻烦也多,到底把变量放在角色脚本中还是行为树里面呢?好在有办法可以在行为树中访问角色的变量,这样一来还是比较方便的。例如,AI角色开火动作的脚本:
public class FireAction : Action{
// The transform that the object is moving towards
CharacterData chaData;
public override void OnAwake()
{
chaData = gameObject.GetComponent<CharacterData>();
}}
如意上面的CharacterData chaData,这个就是我的NPC角色身上的一个脚本组件。用上面的写法,就可以在动作脚本中访问其他脚本的变量或者调用角色的函数了:
// 还是FireAction的OnUpdate函数
public override TaskStatus OnUpdate()
{ // 调用角色的方法
chaData.Fire();
return TaskStatus.Success;
}
到底哪些函数和变量放在角色脚本中,哪些函数和变量放在行为树的动作脚本中?这是一个工程问题了,没有统一的方法。
个人建议:所有角色通用的变量和方法,放在角色脚本中。因为即使不用Behavior Designer插件,这些变量和方法也是有用的。而只用于AI的变量,放在行为树的Variables里面即可,由专门负责AI设计的人员维护。例如:角色移动速度、开火CD时间,都是角色本身的变量。而发现敌人的transform、距离敌人的距离,如果只在AI逻辑中用到,就应该放在行为树中。
★ 也有办法在角色脚本中访问行为树的变量,可以查阅官方文档和资料。但是我们尽可能避免这种用法,这种反向耦合对项目整洁不利。
五、复杂一些的行为树实战
为了综合运用所有行为树的知识,我又做了一个复杂的例子。我们的目标是在之前的例子的基础上,给AI增加如下功能:
1、灵活利用建筑进行防御。当玩家从右侧接近,则走到预定地点1防御;如果玩家从左侧接近,则走到预定地点2防御;如果玩家从中间接近,走到预定地点3防御。
image.jpeg
2、处理被狙击的特殊情况:如果玩家从超远距离狙击到AI,如果AI不做出反应,则会被玩家慢慢消灭掉。所以AI必须对狙击情况做出回应——也就是进攻。
image.jpeg
实现这两点之后,咱们的AI虽然精简,但也算是麻雀虽小五脏俱全了,哈哈。抽象地说,我们的AI现在既能利用环境做出掩护动作,又能处理特殊情况防止玩家利用BUG。下图是我最终做完的行为树截图:
image.jpeg
感谢皮皮关的分享,转载于皮皮关的>https://zhuanlan.zhihu.com/p/29598709
网友评论