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

news2025/1/4 9:54:21

【分析】

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

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

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

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

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

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

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

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

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

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

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

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

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

【代码实现】

    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
   }
    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;
        }
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2269146.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode - 初级算法 数组(删除排序数组中的重复项)

免责声明:本文来源于个人知识与公开资料,仅用于学术交流。 删除排序数组中的重复项 这篇文章讨论如何从一个非严格递增的数组 nums 中删除重复的元素,使每个元素只出现一次,并返回新数组的长度。因为数组是排序的,只要是相同的肯定是挨着的,所以我们需要遍历所有数组,然…

2024 年度总结

时光荏苒&#xff0c;2024 年即将画上句号&#xff0c;回顾这一年的写博历程&#xff0c;有付出、有收获、有成长&#xff0c;也有诸多值得回味与反思的瞬间。 一、内容创作 主题涉猎&#xff1a;这一年&#xff0c;我致力于探索多样化的主题&#xff0c;以满足不同读者群体的…

基于STM32位单片机的腕式运动体力监测装置设计

本设计基于STM32位单片机的腕式运动体力状态诊断系统装置。本系统内的使用的STM32单片机包含了心率检测电路、呼吸频率检测电路、OLED液晶显示电路、电源电路、蓝牙电路。通过心率传感器以及手指脉搏波动放大过后发送给比较器&#xff0c;结果处理后发送给单片机进行信息的收集…

LunarVim安装

LunarVim以其丰富的功能和灵活的定制性&#xff0c;迅速在Nvim用户中流行开来。它不仅提供了一套完善的默认配置&#xff0c;还允许用户根据自己的需求进行深度定制。无论是自动补全、内置终端、文件浏览器&#xff0c;还是模糊查找、LSP支持、代码检测、格式化和调试&#xff…

前端超大缓存IndexDB、入门及实际使用

文章目录 往期回顾项目实战初始化表获取列表新增表的数据项获取详情根据ID获取详情根据其他字段获取详情 删除数据 总结 往期回顾 在之前的文章中&#xff0c;我们介绍了IndexDB vs Cookies vs Session这几个的对比&#xff0c;但是没有做实际项目的演示&#xff0c;今天我们用…

面试题解,JVM的运行时数据区

一、请简述JVM运行时数据区的组成结构及各部分作用 总览 从线程持有的权限来看 线程私有区 虚拟机栈 虚拟机栈是一个栈结构&#xff0c;由许多个栈帧组成&#xff0c;一个方法分配一个栈帧&#xff0c;线程每执行一个方法时都会有一个栈帧入栈&#xff0c;方法执行结束后栈帧…

WAV文件双轨PCM格式详细说明及C语言解析示例

WAV文件双轨PCM格式详细说明及C语言解析示例 一、WAV文件双轨PCM格式详细说明1. WAV文件基本结构2. PCM编码方式3. 双轨PCM格式详细说明二、C语言解析WAV文件的代码示例代码说明一、WAV文件双轨PCM格式详细说明 WAV文件是一种用于存储未压缩音频数据的文件格式,广泛应用于音频…

QT------模型/视图

一、模型/视图结构概述 基本原理&#xff1a; Qt 的模型/视图&#xff08;Model/View&#xff09;架构将数据的存储和显示分离&#xff0c;提高了代码的可维护性和复用性。模型&#xff08;Model&#xff09;&#xff1a;负责存储和管理数据&#xff0c;提供数据的访问接口&am…

vue3+ts+element-plus 表单el-form取消回车默认提交

问题描述&#xff1a;在表单el-form中的el-input中按回车后&#xff0c;页面会刷新&#xff0c;url也会改变&#xff0c; 回车前&#xff1a; 回车后&#xff1a; 相关代码&#xff1a; 解决方法1&#xff1a;在 el-form 上阻止默认的 submit 事件&#xff0c;增加 submit.pre…

掌握大数据处理利器:Flink 知识点全面总结【上】

1.Flink的特点 Apache Flink 是一个框架和分布式处理引擎&#xff0c;用于对无界和有界数据流进行状态计算。 Flink主要特点如下&#xff1a; 高吞吐和低延迟。每秒处理数百万个事件&#xff0c;毫秒级延迟。结果的准确性。Flink提供了事件时间(event--time)和处理时间(proces…

国产数据库-崖山使用介绍

本文档基于崖山数据库23.3 个人版本&#xff0c;单机&#xff08;主备&#xff09;部署模式的情况下的使用介绍。 数据库实例状态&#xff1a; NOMOUNT&#xff1a;仅读取参数文件&#xff0c;不加载数据库 MOUNT&#xff1a;读取控制文件&#xff0c;加载数据库&#xff…

Pytest基础01: 入门demo脚本

目录 1 Pytest接口测试 1.1 最简单版hello world 1.2 pytest.ini 2 pytest兼容unittest 3 封装pytest执行入口 1 Pytest接口测试 Pyest是一个可以用于接口测试的强大框架&#xff0c;开源社区也有非常多的pytest插件。 按江湖传统&#xff0c;学习一个新语言或者新框架&…

如何在没有 iCloud 的情况下将数据从 iPhone 传输到 iPhone

概括 您可能会遇到将数据从 iPhone 转移到 iPhone 的情况&#xff0c;尤其是当您获得新的 iPhone 15/14 时&#xff0c;您会很兴奋并希望将数据转移到它。 使用iCloud最终可以做到这一点&#xff0c;但它的缺点也不容忽视&#xff0c;阻碍了你选择它。例如&#xff0c;您需要…

streamlit、shiny、gradio、fastapi四个web APP平台体验

streamlit、shiny、gradio、fastapi四个web APP平台体验 经常被问的问题就是&#xff1a;web APP平台哪个好&#xff1f;该用哪个&#xff1f;刚开始只有用streamlit和shiny&#xff0c;最近体验了一下gradio和fastapi&#xff0c;今天根据自己的体会尝试着回答一下。 使用R语…

HTML5滑块(Slider)

HTML5 的滑块&#xff08;Slider&#xff09;控件允许用户通过拖动滑块来选择数值。以下是如何实现一个简单的滑块组件的详细说明。 HTML5 滑块组件 1. 基本结构 使用 <input type"range"> 元素可以创建一个滑块。下面是基本实现的代码示例&#xff1a; <…

探索 .idea 文件夹:Java Maven 工程的隐形守护者

一、.idea文件夹深度解析&#xff1a;IntelliJ IDEA项目配置的核心 在Java Maven工程的开发环境中&#xff0c;.idea文件夹扮演着举足轻重的角色。这是IntelliJ IDEA项目特有的一个配置文件夹&#xff0c;它包含了项目所需的各种配置信息&#xff0c;以确保项目能够在不同的开发…

遥感图像车辆检测-目标检测数据集

遥感图像车辆检测-目标检测数据集&#xff08;包括VOC格式、YOLO格式&#xff09; 数据集&#xff1a; 链接: https://pan.baidu.com/s/1XVlRTVWpXZFi6ZL_Xcs7Rg?pwdaa6g 提取码: aa6g 数据集信息介绍&#xff1a; 共有 1035 张图像和一一对应的标注文件 标注文件格式提供了…

[Qt] Qt介绍 | 搭建SDK

目录 1. Qt 简介 什么是 Qt&#xff1f; 1.1 引入 1.2 GUI 1.3 Qt 介绍 2. Qt 发展史 3. Qt 支持的平台 4. Qt 版本信息 5. Qt 的优点 6. Qt 应用场景 7. Qt 成功案例 8. Qt 发展前景及就业分析 二. Qt 开发环境搭建 1. 开发工具概述 2.Qt SDK 安装 3.使用 1. …

【机器学习】机器学习的基本分类-自监督学习-对比学习(Contrastive Learning)

对比学习是一种自监督学习方法&#xff0c;其目标是学习数据的表征&#xff08;representation&#xff09;&#xff0c;使得在表征空间中&#xff0c;相似的样本距离更近&#xff0c;不相似的样本距离更远。通过设计对比损失函数&#xff08;Contrastive Loss&#xff09;&…

xterm + vue3 + websocket 终端界面

xterm.js 下载插件 // xterm npm install --save xterm// xterm-addon-fit 使终端适应包含元素 npm install --save xterm-addon-fit// xterm-addon-attach 通过websocket附加到运行中的服务器进程 npm install --save xterm-addon-attach <template><div :…