Behavior Designer插件(上)


## 1、行为树与状态机对比

举个简单的例子。设计一个简单的AI士兵,他具有以下特性:

1.他具有真实视野,发现敌人则进攻。

2.进攻时会瞄准敌人并射击,同时跑向敌人。

3.离初始位置过远,有可能是被调虎离山了,这时要回到初始位置。

4.进攻时离敌人过远,代表敌人逃跑成功,也要回到初始位置。

画一个简单的状态机:

在Update函数中用一些 if else 条件判断,就实现了这个状态机,比较简单。改

进这个状态机,但是改进的AI士兵过于复杂了,到底能复杂到什么程度:


对于游戏设计师来说,我们只增加了一个简单的功能:当AI士兵被远距离狙击时,就跑到建筑内部的指定位置,以躲避狙击手的攻击。这个功能会增加两个状态,就是上图左边的:跑向掩体和掩体待机。问题是——数一数连线,最早我们有4条有效线段(去掉2个“无”的连线就是4条),代表4种状态转移的条件。而改进后的AI士兵,多出了5条线段,比原来的复杂度扩大了一倍还多。9种状态转移并不多,问题是:这样子的AI士兵还远远达不到设计要求,为了让AI能应付各种情况,我们最终可能需要12种状态。那么这12种状态如果用状态机管理,线段会有多少条呢?去掉某些不可能的状态转移,少说需要几十条线段吧……这可不是什么好消息。下面我们直观的看一下用行为树解决同样的问题,是什么样子的:


是不是有一种耳目一新的感觉?从直观感受上来说,**状态机是以多个状态为核心,以状态转移为线索的一种图表。而行为树是以行为逻辑为框架,以具体行动作为节点的一种树状图。**上图我们简单改变一下写法,就变成了更容易理解的形式:


黄色节点翻译为大家容易理解的 and &&、or || 和while循环,暂且可以这么理解。而红色节点,是一种判断行为(相当于if语句),绿色节点,是真正的行动Action节点。这个图是从示例工程中实际用到的行为树简化而来的,非常具有说服力。而实际使用中树的结构远远没有这么简单,行为树会引出很多新的概念与使用要点,而且我们会用Behavior Designer行为树插件来作为示范。

## 2、Behavior Designer 简单介绍

借用Behavior Designer官方文档的介绍:Behavior Designer 是一个行为树插件!是为了让设计师,程序员,美术人员方便使用的可视化编辑器!Behavior Designer 提供了强大的 API 可以让你轻松的创建 tasks(任务),配合 uScript 和 PlayMaker 这样的插件,可以不费吹灰之力就能够创建出强大的 AI 系统,而无需写一行代码!

其实,按照我的理解,Behavior Designer的主要作用并非可以不写代码(还是要写不少代码的),而是能让游戏中逻辑最混乱的模块——AI模块能更有序的组织,方便查看、调试和修改。

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插件专用的。写好了这段代码之后,再到行为树窗口里去,就多了一个选项:

顺理成章,把该连的线连起来。


简单来说就是用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之后如下图:


我们要让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思考过程可视化。

8、扩展实现所有功能

老师领进门,修行在个人。所有基本功能都介绍完毕,至于实际的使用方法需要大家自己分析一下了。下面是实现开头状态机的功能,得到了很好的效果,而且很好修改。


而且虽然节点很多,加上注释之后,其实也就分三大块而已,看起来还是有状态机的影子,不难理解。


如上图,和咱们讲行为树原理时用的概念图基本是一致的。某些Composite前面没讲,下一篇文章会继续深入,当然自己查资料试验才是最好的学习方法。

## 3、总结

状态机和行为树的问题,属于工程问题,不是科学问题,每个人会找到自己的理解,而且还有可能发现更厉害的抽象方法。对于工程问题来说,就和写代码一样,有很多要点:

1、细节多且杂,函数返回值、Composite的使用这些细节的设计决定了成败。

2、除非自己动手试验,发现问题、解决问题,否则不可能掌握。所以,我会在之后再次深入讨论Behavior Designer。

行为树的实际使用博大精深,远远不是几篇文章能覆盖到的。毕竟AI是游戏开发中最庞大的系统之一,而行为树又是AI的核心。希望读者们能理解方法,体会乐趣,不要过早陷入技术细节之中。咱们下一节还是继续行为树,下期再见。

感谢皮皮关的分享,转载于皮皮关的给猫看的游戏AI实战(六)行为树和Behavior Designer插件(上篇) - 知乎

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容