欢迎光临散文网 会员登陆 & 注册

给猫看的游戏AI实战(六)行为树和Behavior Designer插件(上篇)

2017-10-11 18:21 作者:皮皮关做游戏  | 我要投稿

本系列第三节花了很长篇幅介绍怎样用有限状态机(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的核心。希望读者们能理解方法,体会乐趣,不要过早陷入技术细节之中。

咱们下一节还是继续行为树,下期再见。


工程地址:https://github.com/mayao11/PracticalGameAI/tree/master/AI_Enemy3_Behavior_basic



对游戏开发感兴趣的同学,欢迎围观我们:【皮皮关游戏开发教育】 ,会定期更新各种教程干货,更有别具一格的线下小班教育。

我们的官网地址:http://levelpp.com/

我们的游戏开发技术交流群:610475807

我们的微信公众号:皮皮关


给猫看的游戏AI实战(六)行为树和Behavior Designer插件(上篇)的评论 (共 条)

分享到微博请遵守国家法律