行为树详解(5)——事件驱动

【分析】

如果行为树的节点很多,那么会存在要经过很多节点才会走到动作节点的情况。显然,性能上不如状态机。

每帧都需要重新遍历一系列节点才会走到动作节点,而实际上很多条件节点在数帧内不会有变化,这是造成性能问题的重要原因。

行为树每帧的Update其实就是在轮询这些条件,为避免轮询,我们自然而然的希望在一帧直接执行动作节点。

为此,需要将在Running状态的动作节点缓存,直接执行直到成功。

这种方式引起的问题是动作节点不一定总是会执行成功,可能在某一帧,某些条件改变了,动作需要被打断。

为了解决该问题,就需要给动作节点引入一些事件,当特定事件发生时,在动作节点内接收该事件,直接结束动作节点的运行,返回失败。

随后重新开始整个行为树的轮询,找到下一个动作节点。

这就是事件和轮询混合驱动的行为树。

事件本质上就是直接调用,当经过动作节点的路径较长,也即影响该动作节点的条件过多时,不可避免要通过代码调用发多种消息,这就失去了配置的意义。

需要将打断动作节点及节点的逻辑做成单独的Conditional节点,这些节点接收消息或监听变化,以决定是否打断其子节点。

通过轮询这些Conditional节点以简化发多种消息的调用。

当打断发生时,需要重新运行被打断节点的父节点确定新的分支走向。为此,需要将路径上所有节点做缓存。

这里要对控制节点做特殊处理,控制节点会控制自身的子节点运行,其状态受子节点影响,当打断存在时,子节点可能不需要再次运行,因此控制节点在控制子节点运行前需要知道当前是否被打断。

【代码实现】

cs 复制代码
    public class BehaviorTree
    {
        public string btName;
        public int btId;
        public UpdateType updateType;
        public DrivenType drivenType;
        public float updateTime;
        private float curTime;
        public GameObject owner;
        private Node rootNode;

        private List<Node> allNodes = new List<Node>();
        private Dictionary<int,Node> id2Node = new Dictionary<int,Node>();
        private Dictionary<int,NodeInfo> id2NodeInfo = new Dictionary<int,NodeInfo>();
        private Dictionary<Type, Dictionary<string, Delegate>> eventDic = new Dictionary<Type, Dictionary<string, Delegate>>();

        private Node curActionNode;

        private Stack<Node> activeNodes = new Stack<Node>();
        private List<ConditionalNode> conditionalNodes = new List<ConditionalNode>();  

        public static Func<string, BehaviourTreeData> loadFunc = null;
        public void Init(GameObject owner, string path, Func<string, BehaviourTreeData> loadFunc)
        {
            this.owner = owner;
            //这里省略部分边界情况检查的逻辑,
            if (!string.IsNullOrEmpty(path))
            {
                var data = loadFunc?.Invoke(path);
                btId = owner.GetInstanceID();
                updateType = data.updateType; 
                updateTime = data.updateTime;
                drivenType = data.drivenType;
                //通常初始化有两种写法,一种是在管理者中完成管理者自身以及被管理者的数据初始化;另一种是管理者完成自身的初始化后将被数据传递给被管理者,被管理者自身完成初始化
                //第二种方式更加灵活可变,且不需要关注行为树结构是怎么样的,只需每个节点做好自身的初始化
                //在第二种方式下涉及如何将数据递归传递的问题,为统一获取,将数据记录在管理者上,被管理者根据Id从管理者中获取,初始化后可选择将数据释放
                foreach (var item in data.nodes)
                {
                    id2NodeInfo[item.nodeId] = item;
                }
                var rootData = data.nodes[0];//默认第一个是rootNode数据
                rootNode = new RootNode();
                rootNode.Init(rootData, this);
                id2NodeInfo.Clear();//也可以保留
            }
            
        }

        public void Update(float time)
        {
            if(drivenType == DrivenType.Polling)
            {
                PollingUpdate(time);
            }
            else if(drivenType == DrivenType.PollingAndEvent)
            {
                if (curActionNode != null)
                {
                    var actionStatus = curActionNode.Update();
                    if(actionStatus != NodeStatus.Running)
                    {
                        curActionNode = null; 
                        PollingUpdate(time);
                    }
                    else
                    {
                        if (updateType == UpdateType.FixedTime)
                        {
                            curTime += time;
                        }
                    }
                }
                else
                {
                    PollingUpdate(time);
                }
            }
            else if(drivenType == DrivenType.EventDriven)
            {
                EventUpdate(time);
            }

        }

        private void PollingUpdate(float time)
        {
            if (updateType == UpdateType.EveryFrame)
            {
                Update();
            }
            else if (updateType == UpdateType.FixedTime)
            {
                curTime += time;
                if (curTime > updateTime)
                {
                    curTime = 0;
                    Update();
                }
            }
        }

        private void EventUpdate(float time)
        {
            int index = -1;
            for (int i = 0; i < conditionalNodes.Count; i++)
            {
                if (conditionalNodes[i].ConditionChanged()) //当条件节点条件改变,其后所有节点都需要重新计算,动作节点需要被打断
                {
                    index = i;
                    break;
                }
            }
   
            if (index != -1)
            {              
                while(activeNodes.Count> 0 && activeNodes.Peek().nodeId != conditionalNodes[index].nodeId)//移除其后所有Active节点
                {
                    PopNode(activeNodes.Peek());
                }

                conditionalNodes[index].Update();//重新运行该节点
                if (conditionalNodes[index].StatusChanged(out var curStatus))
                {                    
                    var curNode = (Node)conditionalNodes[index];
                    var parent = conditionalNodes[index].parent;
                    while (parent is ControlNode)//如果父节点是控制节点,其状态会与子节点相关,这里简化处理,直接重新运行
                    {
                        curNode = parent;
                        parent = parent.parent;
                    }
                    if(curNode != conditionalNodes[index])
                    {
                        while (activeNodes.Count > 0 && activeNodes.Peek().nodeId != curNode.nodeId)
                        {
                            PopNode(activeNodes.Peek());
                        }
                        curNode.Update();
                    }
                }

            }
            else
            {
                var state = activeNodes.Peek().Update();//没有打断,再次运行末尾的动作节点
                if(state != NodeStatus.Running)//运行结束,重新从根节点开始运行,也可以选择回退到上一个节点开始运行
                {
                    activeNodes.Clear();
                    conditionalNodes.Clear();
                    rootNode.Update();
                }
            }

        }

        public void PushNode(Node node)
        {
            if (drivenType != DrivenType.EventDriven)
                return;

            activeNodes.Push(node);
            if(node is ConditionalNode conditionalNode)
            {
                conditionalNodes.Add(conditionalNode);
            }
        }

        private void PopNode(Node node)
        {
            if (drivenType != DrivenType.EventDriven)
                return;

            node.End();
            activeNodes.Pop();
            if(node is ConditionalNode conditionalNode)
            {
                conditionalNodes.Remove(conditionalNode);
            }
        }

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

        public void Destroy()
        {
            foreach (var item in allNodes)
            {
                item.Destroy();
            }
            allNodes.Clear();
            id2Node.Clear();
            id2NodeInfo.Clear();
        }

        public NodeInfo GetNodeInfo(int nodeId)
        {
            return id2NodeInfo[nodeId];
        }

        public void AddNode(Node node)
        {
            allNodes.Add(node);
            id2Node[node.nodeId] = node;
        }

        public void SetActionNode(Node node)
        {
            curActionNode = node;
        }

        private void RegisterEvent(string eventName, Delegate handler)
        {
            if (drivenType == DrivenType.Polling)
                return;

            if (!eventDic.TryGetValue(handler.GetType(), out var dic))
            {
                dic = new Dictionary<string, Delegate>();
                eventDic[handler.GetType()] = dic;
            }

            if (!dic.TryGetValue(eventName, out var value))
            {
                dic[eventName] = handler;
            }
            else
            {
                dic[eventName] = Delegate.Combine(value, handler);
            }
        }

        public void RegisterEvent(string eventNmae,Action action)
        {
            RegisterEvent(eventNmae, (Delegate)action);
        }

        public void RegisterEvent<T>(string eventNmae, Action<T> action)
        {
            RegisterEvent(eventNmae, (Delegate)action);
        }

        public void RegisterEvent<T1,T2>(string eventNmae, Action<T1,T2> action)
        {
            RegisterEvent(eventNmae, (Delegate)action);
        }

        public void RegisterEvent<T1, T2,T3>(string eventNmae, Action<T1, T2,T3> action)
        {
            RegisterEvent(eventNmae, (Delegate)action);
        }

        private Delegate GetDelegate(string eventName,Type type)
        {
            if (drivenType == DrivenType.Polling)
                return null;

            if (eventDic.TryGetValue(type, out var dic))
            {
                if (dic.TryGetValue(eventName, out var handler))
                {
                    return handler;
                }
            }
            return null;
        }
        public void SendEvent(string eventName)
        {            
            if(GetDelegate(eventName, typeof(Action)) is Action action)
            {
                action();
            }
        }

        public void SendEvent<T>(string eventName, T t)
        {
            if (GetDelegate(eventName, typeof(Action<T>)) is Action<T> action)
            {
                action(t);
            }
        }

        public void SendEvent<T1,T2>(string eventName, T1 t1,T2 t2)
        {
            if (GetDelegate(eventName, typeof(Action<T1,T2>)) is Action<T1,T2> action)
            {
                action(t1,t2);
            }
        }

        public void SendEvent<T1, T2,T3>(string eventName, T1 t1, T2 t2,T3 t3)
        {
            if (GetDelegate(eventName, typeof(Action<T1, T2,T3>)) is Action<T1, T2,T3> action)
            {
                action(t1, t2,t3);
            }
        }
    }

   public enum DrivenType
   {
       Polling,
       PollingAndEvent,
       EventDriven
   }
cs 复制代码
    public class Node
    {
        public string nodeName;
        public int nodeId;
        public NodeStatus status;
        public BehaviorTree owner;
        public Node parent;

        public void Init(NodeInfo nodeInfo, BehaviorTree owner)
        {
            this.owner = owner;
            this.nodeName = nodeInfo.nodeName;
            this.nodeId = nodeInfo.nodeId;
            OnInit(nodeInfo);
            owner.AddNode(this);
            //对于字段的配置可以通过字段名反射获取字段然后设置值,但这里允许的值是有限的,我们直接在每个节点写对应的处理
        }


        public NodeStatus Update()
        {
            owner.PushNode(this);
            return OnUpdate();
        }

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

        protected virtual void OnInit(NodeInfo nodeInfo)
        {

        }

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

        protected virtual void OnEnd()
        {

        }

        public virtual void Destroy()
        {

        }
    }

    public class ConditionalNode:Node
    {
        public Node subNode;

        protected override void OnInit(NodeInfo nodeInfo)
        {
            if (nodeInfo.subNodes.Count > 0)
            {
                var subNodeInfo = owner.GetNodeInfo(nodeInfo.subNodes[0]);
                Type type = Type.GetType(subNodeInfo.nodeType);
                subNode = (Node)Activator.CreateInstance(type);
                subNode.Init(subNodeInfo, owner);
                subNode.parent = this;
            }
        }

        protected override NodeStatus OnUpdate()
        {
            return subNode.Update();
        }

        public virtual bool ConditionChanged()
        {
            return false;
        }

        public virtual bool StatusChanged(out NodeStatus status)
        {
            status = NodeStatus.Success;
            return false;
        }

    public class ActionNode:Node
    {
        private bool start;
        private bool stop;
        protected override NodeStatus OnUpdate()
        {
            if (!start)
            {
                Start();
                start = true;
            }
            var res = base.OnUpdate();
            if(stop)
            {
                res = NodeStatus.Failure;
            }
            if (res == NodeStatus.Running)
            {
                owner.SetActionNode(this);
            }
            return res;
        }

        private void Start()
        {
            OnStart();
        }

        protected virtual void OnStart()
        {
            //各动作节点自定义注册事件
        }

        protected override void OnEnd()
        {
            start = false;
            stop = false;
        }
    }
相关推荐
ue星空1 天前
UE5行为树浅析
人工智能·ai·ue5·行为树
永恒星22 天前
行为树详解(4)——节点参数配置化
行为树·参数配置
Flamesky1 个月前
MMORPG技能管线设计经验总结
行为树·可视化·rpg·skill·mmo·战斗系统·技能编辑器·技能管线·mmorpg·arpg
kim56592 个月前
android studio 轮询修改对象属性(修改多个textview的text)
android·ide·android studio·轮询
清灵xmf2 个月前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询
一只Black5 个月前
Spring Boot解决循环注入问题
java·spring boot·事件驱动·循环依赖注入
蔗理苦5 个月前
2024-07-22 Unity AI行为树1 —— 框架介绍
unity·c#·游戏引擎·行为树·游戏ai
智慧的牛8 个月前
Netty核心组件介绍
netty·事件驱动·网络应用程序框架
赢乐8 个月前
Web实时通信的学习之旅:轮询、WebSocket、SSE的区别以及优缺点
websocket·sse·轮询·通信机制·单向通信·双向通信·推送数据