zoukankan      html  css  js  c++  java
  • Unity BehaviorDesigner行为树基础总结

    BehaviorDesigner——行为树,用于控制和实现AI逻辑,类似于这样:

    上面这个行为树实现了这样的逻辑:

    当Player有Input时按照Input值来移动,无Input时查找最近的可攻击目标,如果能找到就执行攻击;当既没有Input也没有找到攻击目标时,那就一直处于Idle状态。

    下面总结BehaviorDesigner最常见的基础知识:

    首先要明确一个行为树必须有一个依赋对象,它诠释的是该对象的一系列行为模式。

    这些行为模式由Task节点构成,图中的每一个可执行的方框就是一个Task节点,将这些节点按照设计的逻辑进行连接,就组成了该对象的行为树。

    行为树从根节点开始,从上至下,从左至右依次执行其下每一Task节点,任何被执行的Task将返回一种状态,当根节点Task返回成功(或失败)状态时,意味着该行为树单次执行结束。

    Task有以下状态:

    正在执行,执行成功,执行失败,未激活。

    任何Task都处于这几种状态之一。

    Task分为不同类型,每种类型的Task作用各不相同,首先必须弄清楚每类Task的大致作用:

    Composites(复合类):主要用于控制行为树的走向,也是用的最多最重要的一类,任何一个相对复杂的行为树都包含这类Task节点,但它本身不做任何具体行为,所以它们一般位于父节点或根节点。

    Decorators(装饰类):多用于对其下的子Task节点执行额外操作,例如反转结果,重复执行等。

    Actions(行为类):数量最多,为具体执行行为的Task,一般位于行为树的叶子节点右侧,该类Task可能并非单帧就能完成。没必要每个Action都搞清楚,因为可以很容易的自己扩展Action。后面会具体介绍如何扩展。

    Conditionals(条件类):一般放在Action节点左侧进行约束,只有当条件满足(或不满足)时才继续往下执行,单帧内完成一次判断。更多时候配合复合节点进行打断或任务跳转,这一点后面会详细说明。

    最基础最常用的复合类Task是下面两个:

    Selector(选择):相当于Or操作,下面的子Task节点只要有一个返回成功了它就返回成功,只有当所有的都返回失败了才返回失败。

    Sequence(序列):相当于And操作,下面的子Task节点只要有一个返回失败了它就返回失败,只有当所有的都返回成功了才返回成功。

    其余的只是在这两个的基础上变形,例如子节点同时执行或随机顺序执行等,具体可看官方文档说明。

    复合类Task的优先级和打断:

    这一点非常重要,是复合类Task的唯一特殊属性:分为四种——不打断,可打断自身,可打断低于该Task优先级的其他Task,既可以打断自身也可以打断低于其优先级的。

    需要注意的是,该复合节点的打断条件是其下子节点必须有条件节点,此时该条件节点的判断一直处于运行状态,一旦该条件节点在某一刻发生改变,此时行为树将重新跳转到该复合节点位置继续运行,从而打断其他正在运行的低优先级节点。

    例如最上面的行为树中,Player通过判断是否接入Input移动指令可以打断比它优先级低的攻击节点和Idle节点的运行,而攻击节点可以打断Idle节点。

    这意味着,当Player处于Idle状态时一旦成功找到敌人,那么就变为攻击状态,攻击或Idle状态时一旦接受到Input指令又跳转到移动;只有当既没有Input指令输入又没有查找到敌人时,才Idle。

    一旦某一时刻比它优先级高的节点满足了前置条件,就会跳转执行优先级高的。

    所以在设计行为树时,一般会把优先级高的Task节点置于行为树的左侧,将优先级低的置于右侧,因为复合节点并不能打断比该它优先级高的Task节点。

    开启了Abort Type后Task方框的左上角会出现向右或向下的箭头作为标志提示。

    自定义Task任务:

    一般复合类和装饰类的Task是够用的,甚至有些根本用不到,而具体的行为类Task和条件类Task从来都不能满足我们的需求,而且自己写这类Task可以很大程度的简化整个行为树结构。

    自己写Task的步骤如下:

    1.引入命名空间:

    using BehaviorDesigner.Runtime;
    using BehaviorDesigner.Runtime.Tasks;
    

    2.明确继承的Task类型:

    public class MyInputMove : Action
    public class MyIsInput : Conditional
    

    3.知晓Task内部函数的执行流程:(可以看官方文档,写的很清楚,这里把最重要的一张图贴上来)

    观察上图就会发现和Unity中编写脚本大同小异,不一样的地方就是这里的Update有返回值,要返回该任务的执行状态,只有在Running状态时才每帧调用。

    using UnityEngine;
    using BehaviorDesigner.Runtime;
    using BehaviorDesigner.Runtime.Tasks;
    
    public class MyInputMove : Action
    {
        public SharedFloat speed = 5f;
        public override TaskStatus OnUpdate()
        {
            float inputX = Input.GetAxis("Horizontal");
            float inputZ = Input.GetAxis("Vertical");
            if (inputX != 0 || inputZ != 0)
            {
                Vector3 movement = new Vector3(inputX, 0, inputZ);
                transform.Translate(movement*Time.deltaTime*speed.Value);
                return TaskStatus.Running;
            }
            return TaskStatus.Success;
        }
    }

    还有一点不同,就是Task中封装了一层Share的类型的属性,它和非Share类型有何区别呢?

    下面是对比:

    using UnityEngine;
    using BehaviorDesigner.Runtime.Tasks;
    using BehaviorDesigner.Runtime;
    
    public class MyLog : Action
    {
        public string staticLog;
        public SharedString shareLog;
    
        public override TaskStatus OnUpdate()
        {
            Debug.Log(staticLog + shareLog.Value);
            return TaskStatus.Success;
        }
    }

    可以看到,这里的Share的类型就是一个方便在行为树中传递和修改的变量,因为Task之间是不方便直接修改其他Task变量的,那怎么办呢,于是就增加一种Share的类型变量在行为树的各个Task之间进行交流。

    比如这里,每次找到的最近的敌人是不一样的,要根据上一个Task返回的值去执行下一个Task的攻击或打印结果,这时固定的属性就无法满足要求,但直接调用别的Task又增加了耦合性,于是就单独用Share变量来传递值。这样也方便在面板中统一查看管理。

    另外Share变量也可以增加自定义类型,全局的和本地变量的区别就是一个在所有的行为树中有,一个只有这棵树中有。

    上面就是将查找到的最近的敌人和名字返回,其他Task例如攻击和打印时直接就可以取到这里返回的值。在取Share变量值时需要.Value。

    using UnityEngine;
    using BehaviorDesigner.Runtime;
    using BehaviorDesigner.Runtime.Tasks;
    
    public class MyCanFindLatestTarget : Conditional
    {
        public SharedGameObjectList origins;
        public bool bUseTag = false;
        public SharedString tag;
        public SharedGameObject result;
        public SharedString resultName;
    
        public override void OnStart()
        {
            if (bUseTag)
            {
                origins.Value.Clear();
                origins.Value.AddRange(GameObject.FindGameObjectsWithTag(tag.Value));
            }
        }
        public override TaskStatus OnUpdate()
        {
            if (origins.Value.Count > 0)
            {
                float minDistance=Mathf.Infinity;
                GameObject minDistanceObj=null;
                foreach(var item in origins.Value)
                {
                    if (item == null)
                        continue;
                    float sqrDistance = (item.transform.position - transform.position).sqrMagnitude;
                    if (sqrDistance < minDistance)
                    {
                        minDistance = sqrDistance;
                        minDistanceObj = item;
                    }
                }
                result.Value = minDistanceObj;
                resultName.Value = minDistanceObj.name;
                return TaskStatus.Success;
            }
            return TaskStatus.Failure;
        }
    }
    using UnityEngine;
    using BehaviorDesigner.Runtime.Tasks;
    using BehaviorDesigner.Runtime;
    
    public class MyAttack : Action
    {
        public SharedGameObject target;
    
        public SharedFloat damage=10f;
        public SharedFloat attackSpeed = 10f;
    
        private Enemy enemy;
        private float timer;
    
        public override void OnStart()
        {
            if (target.Value != null)
            {
                enemy = target.Value.GetComponent<Enemy>();
            }
            timer = 0f;
        }
        public override TaskStatus OnUpdate()
        {
            if (enemy == null)
                return TaskStatus.Failure;
            timer += Time.deltaTime;
            if (timer > (10f /attackSpeed.Value))
            {
                timer = 0f;
                enemy.Hit(damage.Value);
                Debug.Log(target.Value.name + "剩余HP:" + enemy.hp);
                if (enemy.hp <= 0)
                {
                    enemy.Dead();
                    return TaskStatus.Success;
                }
            }
            return TaskStatus.Running;
        }
    }
  • 相关阅读:
    遗传算法求解旅行商(TSP)问题 -- python
    office 小技巧
    oracle创建dblink
    c# equals与==的区别
    两人之间的一些参数
    .net 枚举(Enum)使用总结
    SQL Server 日期的加减函数: DATEDIFF DATEADD
    jquery操作select
    AS3帮助手册
    Razor和HtmlHelper的使用意义
  • 原文地址:https://www.cnblogs.com/koshio0219/p/11734423.html
Copyright © 2011-2022 走看看