行为树详解(3)——多节点实现

【节点实现】

要想行为树的功能更加强大,需要有足够丰富的节点,我们先来实现各类节点:

控制节点

在控制节点中,需要知道当前执行到了哪个节点,需要增加一个字段标识

分别实现SequenceNode、ParallelNode、SelectorNode、RandomSelectorNode

这些是比较常见的控制节点,在前文中我们用顺序节点做了控制,但一些条件判断仍在动作节点中,这是不合适的。

动作节点应当尽量少的做条件判断,我们可以将常见的if else判断方式抽象为节点,这些是特殊的选择节点

结合前文,我们需要两类特殊的选择节点,从两个值做选择的比较节点,从不同值做选择的范围节点

分别实现BoolCompareSelectorNode、BoolCompareSelectorNode、IntCompareSelectorNode、IntRangeSelectorNode、FloatRangeSelectorNode

也可以结合实际需求,实现各类自定义的控制节点

    public class ControlNode:Node 
    { 
        public List<Node> subNodes;

        public int curSubIndex;
        protected override NodeStatus OnUpdate()
        {
            return Update();
        }
    }

    public class SequenceNode:ControlNode
    {
        protected override NodeStatus OnUpdate()
        {
            curSubIndex = -1;
            foreach (var node in subNodes)//成功则继续
            {
                curSubIndex++;
                var status = node.Update();
                if (status != NodeStatus.Success)
                    return status;
            }
            return NodeStatus.Success;
        }
    }

    public class SelectorNode:ControlNode
    {
        protected override NodeStatus OnUpdate()
        {
            curSubIndex = -1;
            foreach (var node in subNodes)
            {
                curSubIndex++;
                var status = node.Update();
                if (status == NodeStatus.Success)//任意成功则返回
                    return status;
            }
            return NodeStatus.Success;
        }
    }

    public class RandomSelectorNode:ControlNode
    {
        protected override NodeStatus OnUpdate()
        {
            curSubIndex = UnityEngine.Random.Range(0, subNodes.Count);
            var status = subNodes[curSubIndex].Update();
            return status;
        }
    }

    public class BoolCompareSelectorNode:ControlNode
    {
        public bool targetValue;
        private bool curValue;
        protected override NodeStatus OnUpdate()
        {
            curSubIndex = targetValue == curValue ? 0 : 1;
            var status = subNodes[curSubIndex].Update();
            return status;
        }
    }

    public class IntCompareSelectorNode : ControlNode
    {
        public int targetValue;
        public Comparison comparison;
        private int curValue;
        protected override NodeStatus OnUpdate()
        {
            bool res = true;
            switch(comparison)
            {
                case Comparison.SmallerThan: res = curValue<targetValue; break;
                case Comparison.GreaterThan: res = curValue>targetValue; break;
                case Comparison.Equal: res = curValue == targetValue;break;
                case Comparison.SmallerThanOrEqual: res = curValue <= targetValue; break;
                case Comparison.GreaterThanOrEqual: res = curValue >= targetValue; break;
            }
            curSubIndex = res?0:1;
            var status = subNodes[curSubIndex].Update();
            return status;
        }
    }

    public class FloatCompareSelectorNode : ControlNode
    {
        public float targetValue;
        public Comparison comparison;
        private float curValue;
        protected override NodeStatus OnUpdate()
        {
            bool res = true;
            switch (comparison)
            {
                case Comparison.SmallerThan: res = curValue < targetValue; break;
                case Comparison.GreaterThan: res = curValue > targetValue; break;
                case Comparison.Equal: res = curValue == targetValue; break;
                case Comparison.SmallerThanOrEqual: res = curValue <= targetValue; break;
                case Comparison.GreaterThanOrEqual: res = curValue >= targetValue; break;
            }
            curSubIndex = res ? 0 : 1;
            var status = subNodes[curSubIndex].Update();
            return status;
        }
    }

    public class IntRangeSelectorNode:ControlNode
    {
        public List<int> intRange;
        private int curValue;

        protected override NodeStatus OnUpdate()
        {
            for (int i = 0;i<intRange.Count;i++)
            {
                if (curValue < intRange[i])
                {
                    curSubIndex = i;
                    break;
                }
            }
            var status = subNodes[curSubIndex].Update();
            return status;
        }
    }

    public class FloatRangeSelectorNode : ControlNode
    {
        public List<float> intRange;
        private float curValue;

        protected override NodeStatus OnUpdate()
        {
            for (int i = 0; i < intRange.Count; i++)
            {
                if (curValue < intRange[i])
                {
                    curSubIndex = i;
                    break;
                }
            }
            var status = subNodes[curSubIndex].Update();
            return status;
        }
    }

    public enum Comparison
    {
        Equal,
        SmallerThan,
        SmallerThanOrEqual,
        GreaterThan,
        GreaterThanOrEqual
    }

    public class ParallelNode:ControlNode
    {
        protected override NodeStatus OnUpdate()
        {
            curSubIndex = -1;
            int successCount = 0;
            int failCount = 0;
            foreach (var node in subNodes)
            {
                curSubIndex++;
                var status = node.Update();
                if(status == NodeStatus.Success)
                {
                    successCount++;
                }
                else if(status == NodeStatus.Failure)
                {
                    failCount++;
                }
            }

            if (successCount == subNodes.Count)
                return NodeStatus.Success;

            if (failCount > 0)
                return NodeStatus.Failure;

            return NodeStatus.Success;
        }
    }

修饰节点

修饰节点的子节点通常只有一个,常见的修饰有结果取反、重复N次、重复直到失败、重复直到成功、直接返回失败、直接返回成功等

实现部分修饰节点需要知道行为树的状态,其状态由子节点组成,如果行为树得到的状态不是Running,表示当前行为树执行完毕了,需要重置节点数据

分别实现DecoratorNode、InverterNode、UntilFailureNode、UntilSuccessNode、RepeatNode、ReturnFailureNode、ReturnSuccessNode

    public class DecoratorNode : Node
    {
        public Node subNode;
        protected override NodeStatus OnUpdate()
        {
            return subNode.Update();
        }

        protected override void OnEnd()
        {
            subNode.End();
        }
    }

    public class InverterNode: DecoratorNode
    {
        protected override NodeStatus OnUpdate()
        {
            var status = subNode.Update();
            if(status == NodeStatus.Success )
            {
                status = NodeStatus.Failure;
            }
            else if (status == NodeStatus.Failure)
            {
                status = NodeStatus.Success;
            }
            return status;
        }
    }

    public class UntilFailureNode: DecoratorNode
    {
        private NodeStatus lastState = NodeStatus.Success;
        protected override NodeStatus OnUpdate()//失败则表示这个节点成功执行,成功执行后一直成功,也可以成功后重新执行
        {
            if(lastState == NodeStatus.Failure)
            {
                return NodeStatus.Success;
            }
            var status = subNode.Update();
            lastState = status;
            if (status == NodeStatus.Failure)
            {
                status = NodeStatus.Success;
            }
            return status;
        }

        protected override void OnEnd()
        {
            lastState = NodeStatus.Success;
        }
    }

    public class UntilSuccessNode: DecoratorNode
    {
        private NodeStatus lastState = NodeStatus.Failure;
        protected override NodeStatus OnUpdate()
        {
            if (lastState == NodeStatus.Success)
            {
                return NodeStatus.Success;
            }
            var status = subNode.Update();
            lastState = status;
            if (status == NodeStatus.Success)
            {
                status = NodeStatus.Success;
            }
            return status;
        }

        protected override void OnEnd()
        {
            lastState = NodeStatus.Failure;
        }
    }

    public class ReturnFailureNode: DecoratorNode
    {
        protected override NodeStatus OnUpdate()
        {
            return NodeStatus.Failure;
        }
    }

    public class ReturnSuccessNode : DecoratorNode
    {
        protected override NodeStatus OnUpdate()
        {
            return NodeStatus.Success;
        }
    }
    public class RepeatNode : DecoratorNode
    {
        public int maxCount;
        private int curCount;
        protected override NodeStatus OnUpdate()
        {
            if(curCount<maxCount)
            {
                curCount++;
                var status = subNode.Update();
                return status;
            }
            return NodeStatus.Failure;
        }

        protected override void OnEnd()
        {
            curCount = 0;
        }
    }
    public class BehaviorTree
    {
        public GameObject owner;
        private Node rootNode;
        public void Init(GameObject owner)
        {
            this.owner = owner;
        }

        public NodeStatus Update()
        {
            var status = rootNode.Update();
            if(status != NodeStatus.Running)
            {
                rootNode.End();
            }
            return status;
        }

        public void Destroy()
        {

        }
    }

    public enum NodeStatus
    {
        Success,
        Failure,
        Running,
    }

    public class Node
    {
        public string nodeName;
        public int nodeId;
        public NodeStatus status;
        public BehaviorTree owner;

        public virtual void Init(string nodeName, int nodeId, BehaviorTree owner)
        {
            this.owner = owner;
            this.nodeName = nodeName;
            this.nodeId = nodeId;
        }

        public NodeStatus Update()
        {
            return OnUpdate();
        }

        public void End()//方法名字叫Exit也是一样的
        {
            OnEnd();
        }

        protected virtual NodeStatus OnUpdate()
        {
            return NodeStatus.Success;
        }
        
        protected virtual void OnEnd()
        {

        }

        public virtual void Destroy()
        {

        }

    }

【复杂需求】

现在有了一定数量的条件节点,我们让需求变得更为复杂来看一看。

NPC围绕固定的点50米内巡逻,当有多个敌人进入巡逻范围内或者和NPC距离小于20米时,会选择其中距离更近的敌人追击,如果和敌人距离小米3米,释放1技能攻击,如果在追击中距离敌人大于30米,NPC速度提高一倍,距离敌人小于3米,释放2技能攻击,如果和敌人距离大于50米或者距离中心点超过100米,则回到重新开始巡逻

按照步骤,首先做行为拆解:

  • 固定50米巡逻
  • 巡逻范围内有敌人或者和NPC距离小于20米可以追击
  • 距离更近的敌人被选择
  • 未加速时,距离敌人小于3米,使用1技能攻击
  • 加速时,距离敌人小于3米,使用2技能攻击
  • 距离敌人大于30米,移速加倍
  • 距离敌人大于50米,或者中心点大于100米,回归巡逻

其次,划分状态,分为巡逻、追击、加速、未加速四个状态,可以发下加速和未加速时追击下的子状态

再次,对每个状态下的行为划分优先级,优先级高的行为先判断

最后,画出行为树导图,如下:

可以更明显的看到,行为逻辑是配置出来的,配置一般由策划负责,相对一般的数值配置来说,行为树的配置增添了逻辑性,更加考验策划。

而在以往的,一些逻辑,尤其是针对各类case的往往是程序负责实现的,而在行为树中,程序只需要负责实现具体的动作。两者分工更加明确。

【动作节点实现】

经过修改后NPC类和自定义实现的Action节点如下

    public class NPC:MonoBehaviour
    {

        private BehaviorTree bt;
        public bool idle;
        public bool fastSpeed;
        public Vector3 idleCenterPos;
        public GameObject target;
        public List<GameObject> enemys = new List<GameObject>();
        public float speed;
        void Start()
        {
            bt = new BehaviorTree();
            bt.Init(this.gameObject);
            //初始化bt数据
        }

        void Update()
        {
            bt.Update();
        }

        void OnDestroy()
        {
            bt.Destroy();
        }

        public void SearcheEnemys()
        {

        }
    }
    public class IdleNode : ActionNode
    {
        protected override NodeStatus OnUpdate()
        {
            var npc = owner.owner.GetComponent<NPC>();
            var res = NodeStatus.Failure;
            if (npc != null)
            {
                if (npc.idle)
                {
                    Debug.Log("执行巡逻时的动作");
                    res = NodeStatus.Success;
                }
            }
            return res;
        }
    }

    public class SearchEnemyNode: ActionNode
    {
        protected override NodeStatus OnUpdate()
        {
            var npc = owner.owner.GetComponent<NPC>();
            var res = NodeStatus.Failure;
            if (npc != null)
            {
                if (npc.idle)
                {
                    npc.enemys.Clear(); 
                    npc.SearcheEnemys();
                    if(npc.enemys.Count > 0)
                    {
                        res = NodeStatus.Success;
                    }                   
                }
            }
            return res;
        }
    }

    public class SelectEnemyNode:ActionNode
    {
        protected override NodeStatus OnUpdate()
        {
            var npc = owner.owner.GetComponent<NPC>();
            var res = NodeStatus.Failure;
            if (npc != null)
            {
                npc.target = null;
                float dis = float.MaxValue;
                foreach( var item in npc.enemys )
                {
                    var _dis = (item.transform.position - npc.gameObject.transform.position).sqrMagnitude;
                    if(_dis < dis)
                    {
                        dis = _dis;
                        npc.target = item;
                    }
                }
                if( npc.target != null )
                {
                    res = NodeStatus.Success;
                    npc.idle = false;
                }
            }
            return res;
        }
    }

    public class ReturnIdleNode : ActionNode
    {
        protected override NodeStatus OnUpdate()
        {
            var npc = owner.owner.GetComponent<NPC>();
            var res = NodeStatus.Failure;
            if (npc != null)
            {
              
                Debug.Log("执行回归巡逻的动作");
                res = NodeStatus.Running;
                //如果回归了                
                //npc.idle = false;
                //npc.fastSpeed = false;
            }
            return res;
        }
    }


    public class AddSpeedNode: ActionNode
    {
        protected override NodeStatus OnUpdate()
        {
            var npc = owner.owner.GetComponent<NPC>();
            var res = NodeStatus.Failure;
            if (npc != null)
            {
                if(!npc.fastSpeed)
                {
                    npc.speed *= 2;
                    npc.fastSpeed = true;
                }
               
                Debug.Log("执行寻路追踪的目标的动作");
                res = NodeStatus.Success;
            }
            return res;
        }
    }

    public class SkillOneAttackNode : ActionNode
    {
        protected override NodeStatus OnUpdate()
        {
            var npc = owner.owner.GetComponent<NPC>();
            var res = NodeStatus.Failure;
            if (npc != null)
            {
                Debug.Log("执行技能1攻击");
                res = NodeStatus.Success;
                //如果回归了                
                //npc.state = NPC.NPCState.Idle;
            }
            return res;
        }
    }

    public class SkillTwoAttackNode : ActionNode
    {
        protected override NodeStatus OnUpdate()
        {
            var npc = owner.owner.GetComponent<NPC>();
            var res = NodeStatus.Failure;
            if (npc != null)
            {
                Debug.Log("执行技能2攻击");
                res = NodeStatus.Success;
                //如果回归了                
                //npc.state = NPC.NPCState.Idle;
            }
            return res;
        }
    }

    public class PursueNode:ActionNode
    {
        protected override NodeStatus OnUpdate()
        {
            var npc = owner.owner.GetComponent<NPC>();
            var res = NodeStatus.Failure;
            if (npc != null)
            {
                Debug.Log("执行追踪敌人动作");
                res = NodeStatus.Running;
                //如果回归了                
                //npc.state = NPC.NPCState.Idle;
            }
            return res;
        }
    }

这样的实现包含了行为树基本的内核,但还需要一些其他功能让行为树更加完善,例如当前的实现面临以下问题:

  1. 通用节点参数配置化:对于存在参数的节点,其参数应该是配置的,作为一个通用节点,不应当插入业务代码去设置或获取具体的值
  2. 动作节点逻辑实现位置:实现既可以在动作节点内,也可以仅仅是调用下方法,哪种方式比较好
  3. 运行时参数获取:每个动作节点都要重复调用相同的代码获取参数的值,有没有一个通用的获取参数的方式
  4. 节点间通信:我们将一些数据放在NPC类中,但对于该类而言,有些数据不是必须的,这些放在一块势必导致NPC类不断膨胀,例如搜索敌人和选择敌人之间的通信。

我们在后文讨论如何实现更为通用的行为树

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值