本系列第三节花了很长篇幅介绍怎样用有限状态机(FSM)实现一个AI敌人。当时实现的敌人状态较少,智商也很捉急。本来我打算在第四节改进那个敌人,为它加上寻找掩体等策略。但是由于逻辑过于复杂,不利于讲解,就改为讨论其他内容了。
其实不继续讲状态机还有个原因——用状态机实现敌人AI之后,我又尝试了行为树的模式以及Behavior Tree插件,花了几天时间才摸索出了大概,将AI逻辑重新实现了一遍。如果你能和我一起对比着用状态机和行为树实现同样的AI,那么收获会非常大,也会比较有成就感。
本文将再次探讨AI逻辑的核心问题,(不像前两章那样在外围问题上划水了( ̄ y ̄))。那么,和我一起来进入AI的世界吧!
1、行为树与状态机对比
举个简单的例子,还是咱们第二节讲过的问题。设计一个简单的AI士兵,他具有以下特性:
- 他具有真实视野,发现敌人则进攻。
- 进攻时会瞄准敌人并射击,同时跑向敌人。
- 离初始位置过远,有可能是被调虎离山了,这时要回到初始位置。
- 进攻时离敌人过远,代表敌人逃跑成功,也要回到初始位置。
画一个简单的状态机:
很简单吧,我们之前在Update函数中用一些 if else 条件判断,就实现了这个状态机。后来我想改进这个状态机,但是改进的AI士兵过于复杂了,咱们看看到底能复杂到什么程度:
对于游戏设计师来说,我们只增加了一个简单的功能:当AI士兵被远距离狙击时,就跑到建筑内部的指定位置,以躲避狙击手的攻击。这个功能会增加两个状态,就是上图左边的:跑向掩体和掩体待机。
问题是——数一数连线,最早我们有4条有效线段(去掉2个“无”的连线就是4条),代表4种状态转移的条件。而改进后的AI士兵,多出了5条线段,比原来的复杂度扩大了一倍还多。
9种状态转移并不多,问题是:这样子的AI士兵还远远达不到设计要求,为了让AI能应付各种情况,我们最终可能需要12种状态。那么这12种状态如果用状态机管理,线段会有多少条呢?去掉某些不可能的状态转移,少说需要几十条线段吧……这可不是什么好消息。
下面我们直观的看一下用行为树解决同样的问题,是什么样子的:
是不是有一种耳目一新的感觉? ( ̄ y ̄)~*
从直观感受上来说,状态机是以多个状态为核心,以状态转移为线索的一种图表。
而行为树是以行为逻辑为框架,以具体行动作为节点的一种树状图。
上图我们简单改变一下写法,就变成了更容易理解的形式:
黄色节点别我翻译为大家容易理解的 and &&、or || 和while循环,暂且可以这么理解。而红色节点,是一种判断行为(相当于if语句),绿色节点,是真正的行动Action节点。这个图是从示例工程中实际用到的行为树简化而来的,非常具有说服力。
而实际使用中树的结构远远没有这么简单,行为树会引出很多新的概念与使用要点,而且我们会用Behavior Designer这款大名鼎鼎的行为树插件来作为示范,在后面我们会详细解释。
2、Behavior Designer 简单介绍
借用Behavior Designer官方文档的介绍:
Behavior Designer 是一个行为树插件!是为了让设计师,程序员,美术人员方便使用的可视化编辑器!Behavior Designer 提供了强大的 API 可以让你轻松的创建 tasks(任务),配合 uScript 和 PlayMaker 这样的插件,可以不费吹灰之力就能够创建出强大的 AI 系统,而无需写一行代码!
其实,按照我的理解,Behavior Designer的主要作用并非可以不写代码(还是要写不少代码的),而是能让游戏中逻辑最混乱的模块——AI模块能更有序的组织,方便查看、调试和修改。
可以打开我们的示例工程,或者安装Behavior Designer插件。该插件是以Package的形式提供的,可以任意拷贝,本文不提供盗版下载( ̄工 ̄lll) 。
1、打开Behavior Designer窗口的方法如图:
2、为任意对象添加Behavior组件:
如图,在上面的菜单里选择“Add Behavior Tree”即可。观察该GameObject的属性,可以在下图中可以看到这个组件实际上是一个脚本,默认参数目前不需要任何修改。
下面开始按步骤实现并讲解一个基本的行为添加过程,如有懵圈的情况,及时询问或查找网上详细的Behavior Designer资料。
3、现在可以编辑这个对象的Behavior Tree了:
要点1、按照步骤1可以打开Behavior Designer编辑窗口,在窗口打开的情况下点击包含了BTree组件的对象,就可以对它进行编辑:
这里添加一个Task -> Decorators -> UntilSuccess节点,它是一种Decorator即修饰器,修饰器在行为树中起到骨架的作用,就像是程序里的循环和判断一样不可或缺。我现在要实现视野范围的功能,在发现敌人以后,把敌人信息记下来。这就需要一个定制化的动作——判断敌人是否在视野中,这个动作起名为WithInSight。
先添加一个WithInSight.cs脚本才能加入到Behavior Designer窗口里,代码如下,已经加上了详细注释:
public class WithinSight : Conditional
{
// 视野角度
public float fieldOfViewAngle;
// 目标物体的Tag
public string targetTag;
// 发现目标时,将目标对象设置到BahaviorTree共享变量里面去
public SharedTransform target;
public SharedVector3 targetPos;
// 所有指定Tag的物体的数组
private Transform[] possibleTargets;
// 重载函数,Behavior Designer专用的Awake
public override void OnAwake()
{
// 根据Tag查找到所有物体,全部加入数组
var targets = GameObject.FindGameObjectsWithTag(targetTag);
possibleTargets = new Transform[targets.Length];
for (int i = 0; i < targets.Length; ++i)
{
possibleTargets[i] = targets[i].transform;
}
}
// 重载函数,Behavior Designer专用的Update
public override TaskStatus OnUpdate()
{
// 判断目标是否在视野内,这个返回值TaskStatus很关键,会影响树的执行流程
for (int i = 0; i < possibleTargets.Length; ++i)
{
if (withinSight(possibleTargets[i], fieldOfViewAngle, 10))
{
// 将目标信息填写到共享变量里面,这样其它Action就可以访问它们了
target.Value = possibleTargets[i];
targetPos.Value = target.Value.position;
Debug.Log("Find Target" + targetPos.Value);
// 成功则返回 TaskStatus.Success
return TaskStatus.Success;
}
}
// 没找到目标就在下一帧继续执行此任务
return TaskStatus.Running;
}
// 判断物体是否在视野范围内的方法
public bool withinSight(Transform targetTransform, float fieldOfViewAngle, float distance)
{
Vector3 direction = targetTransform.position - transform.position;
if (direction.magnitude > distance)
{
return false;
}
return Vector3.Angle(direction, transform.forward) < fieldOfViewAngle;
}
}
WithInSight是一种判断条件,而不是实际的行为动作,所以继承了Conditional,这种继承表示了对Behavior的扩展。重点函数是OnAwake和OnUpdate,这个有别于MonoBehavior,是Behavior Designer插件专用的。写好了这段代码之后,再到行为树窗口里去,就多了一个选项:
顺理成章,把该连的线连起来。
现在你大概知道Behavior Designer的基本玩法了,简单来说就是用Decorator(修饰器)和Composites(组合器)搭逻辑框架,然后自定义Conditional(条件)和Action(动作)来实际判断和实际做出行为,仅此而已。
现在进行测试,在玩家靠近AI时,应该能触发Debug.Log,如果OK的话,说明你迈出了第一步。
这还没完,咱们处理一下代码中的2个shared变量。
4、关于Shared变量的介绍
注意代码中的这两个变量:
// 发现目标时,将目标对象设置到BahaviorTree共享变量里面去
public SharedTransform target;
public SharedVector3 targetPos;
SharedXXXX类型代表这个变量虽然是在这个类中定义的,但是在正确绑定以后,其他Action或者Conditional也可以访问到。简单来说,它们就是专门用在Behavior Designer内部的变量。对这种变量不仅要在代码里声明,还要在Behavior Designer窗口里进行正确设置。
之前咱们直接画图了,没有用到这里的四个窗口。介绍一下
(1) Behavior 状态树整体的名称和属性,对咱们的小项目来说默认就行,不用管。
(2) Tasks所有Conditional(条件)和Action(动作)的列表,按照我的开发习惯,较少用到内置的条件和动作。理由是内置动作功能太单一,组合起来树状图会变得极其复杂,还不如用代码清晰。这个问题见仁见智了。
(3) Variables变量窗口,接下来咱们主要介绍这个。
(4) 行为树节点的Inspector,是针对某个节点的详细属性。在这里面不仅可以设置参数,还能绑定变量,接下来也要用到。
5、添加Shared变量
只要切换到Variables变量窗口,输入变量名称,选择咱们脚本里定好的类型,然后Add即可。Add之后如下图:
已经添加好了,其他参数都不要变,红圈也不要改,因为这个变量的赋值是由脚本负责的。
添加好另一个变量TargetPos:
我们要让AI角色发现敌人时,记住他的transform和位置,以便后续处理,这些变量就和他的大脑记忆一样。所以,要把WithInSight节点和这些变量关联起来。
6、关联节点和变量
点击WithInSight节点,点击Inspector,可以看到这个节点的所有属性。普通的属性就直接设定初始值就ok了,比如第一个属性可视范围是45。重点是对Shared变量进行绑定操作,现在是红色的None。
如果这里和我的截图不一致,就点击黑色圆点,切换一下绑定方式。如下图操作:
这样就能把Variables窗口里刚才新建的Target变量,和WithinSight判断中的Target变量彻底联系起来。同理对TargetPos也要做同样操作。
7、试着加一个动作,进行试验
增加一个动作AimAction瞄准动作,实际上就是转向Target的方向即可,如果AI能转向敌人方向,就代表我们的绑定成功了。先按咱们第3步也就是创建WithinSight的方法,创建一个脚本叫AimAction.cs,内容如下:
public class AimAction : Action
{
public SharedTransform target;
// 是否正在面对入侵者,即已经正确瞄准
bool IsFacingTarget()
{
if (target.Value == null)
{
return false;
}
Vector3 v1 = target.Value.position - transform.position;
v1.y = 0;
if (Vector3.Angle(transform.forward, v1) < 1)
{
return true;
}
return false;
}
// 转向入侵者方向,每次只转一点,速度受turnSpeed控制
void RotateToTarget()
{
if (target.Value == null)
{
return;
}
Vector3 v1 = target.Value.position - transform.position;
v1.y = 0;
Vector3 cross = Vector3.Cross(transform.forward, v1);
float angle = Vector3.Angle(transform.forward, v1);
transform.Rotate(cross, Mathf.Min(2, Mathf.Abs(angle)));
}
public override void OnAwake()
{
}
public override TaskStatus OnUpdate()
{
if (IsFacingTarget())
{
// 返回值不同对状态树会产生巨大影响,可以对比测试
//return TaskStatus.Success;
return TaskStatus.Running;
}
RotateToTarget();
return TaskStatus.Running;
}
}
然后修改行为树的图,添加AimAction和一些联系用的Composite节点,改为下面的形式:
中间的Sequence代表下面的两个子节点依次执行,UntilSuccess构成一个局部的反复执行逻辑,AI会在左边的子节点重复,直到发现敌人,Until节点中断,执行Aim瞄准动作。
别忘了给AimAction节点也绑定两个Shared变量。如果发现像下图这样,和之前说好的不一样,就点击黑色圆点,切换一下绑定方式。点击黑点实际上是切换两种不同的变量使用方式。
现在,如果你操作没错,那么播放游戏,看看敌人是否在发现你之后,就瞄准你:
如上图,不仅AI角色能正确瞄准主角,而且在Behavior Designer窗口中,还能实时看到目前逻辑进行的状态,这个是Behavior Designer插件威力最大的功能之一——查看逻辑进展状态,将AI思考过程可视化(符合咱们第四节讲的原则 :D)。
8、扩展实现所有功能
老师领进门,修行在个人。所有基本功能都介绍完毕,至于实际的使用方法需要大家自己分析一下了。
我用Behavior Designer重现了咱们第三节讲过的内容,得到了很好的效果,而且很好修改。
不要被吓着了哦,这是一步一步做了很久的最终效果。而且虽然节点很多,我加上注释之后,其实也就分三大块而已,看起来还是有状态机的影子,不难理解。
如上图,和咱们讲行为树原理时用的概念图基本是一致的。某些Composite前面没讲,下一篇文章会继续深入,当然自己查资料试验才是最好的学习方法。
3、总结
首先,读者如果做下来的话,建议再回到本文开头,看一下我画的状态机和行为树对比图,加深一下理论印象,而且如果我的概念图不好的话,欢迎提出自己的看法写在评论里。状态机和行为树的问题,属于工程问题,不是科学问题,每个人会找到自己的理解,而且还有可能发现更厉害的抽象方法。
对于工程问题来说,就和写代码一样,有很多要点:
1、细节多且杂,函数返回值、Composite的使用这些细节的设计决定了成败。
2、除非自己动手试验,发现问题、解决问题,否则不可能掌握。
所以,我会在之后再次深入讨论Behavior Designer。
我自己开始写这个例子的时候,光比较Unity不同的行为树插件之间的区别(现在Asset Store里面的同类插件非常多),就花费了两天时间,最终根据资料数量、插件功能确定了Behavior Designer是工程的首选方案。然后翻官方文档和前人总结的博客资料,才慢慢理解了行为树的大致用法。
在探索过程中会走很多弯路,现在将我的经验总结成此文,为初学者解决刚开始的那种迷茫的状态会非常有用。
行为树的实际使用博大精深,远远不是几篇文章能覆盖到的。毕竟AI是游戏开发中最庞大的系统之一,而行为树又是AI的核心。希望读者们能理解方法,体会乐趣,不要过早陷入技术细节之中。
咱们下一节还是继续行为树,下期再见。
对游戏开发感兴趣的同学,欢迎围观我们:【皮皮关游戏开发教育】 ,会定期更新各种教程干货,更有别具一格的线下小班教育。
我们的官网地址:http://levelpp.com/
我们的游戏开发技术交流群:610475807
我们的微信公众号:皮皮关
我觉得你的ID没取好,一开始我还以为是做儿童教育软件的。。。你们应该叫游戏制作培训才对。。。竟然是成都的培训公司。。。哈哈哈
如果做成系列的视频,效果会更好,文章太长不容易看。