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

【分析】

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

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

行为树每帧的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.mzph.cn/news/891018.shtml

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

相关文章

ESP-IDF学习记录(2)ESP-IDF 扩展的简单使用

傻瓜式记录一个示例的打开&#xff0c;编译&#xff0c;运行。后面我再一个个运行简单分析每个demo的内容。 1.打开示例代码 2.选择项目&#xff0c;文件夹 3.选择串口 4.选择调试方式 5.根据硬件GPIO口配置menuconfig 6.构建项目 7.烧录设备&#xff0c;选择串口UART方式 运行…

SpringMVC学习(一)——请求与响应处理

目录 一、SpringMVC简介 二、RequestMapping&#xff1a;请求路径映射 三、RestController 四、请求限定 五、请求处理 1.使用普通变量&#xff0c;收集请求参数 2.使用RequestParam明确指定获取参数 3.目标方法参数是一个pojo 4.RequestHeader&#xff1a;获取请求…

数据分析的分类和EDIT思维框架

为了服务于企业不同层次的决策&#xff0c;商业数据分析过程需要提供相应的数据科学产出物。 一般而言&#xff0c;数据分析需要经历从需求层、数据层、分析层到输出层四个阶段。 第一个阶段是需求层——确定目标&#xff0c;具体目标需要依据具体的层次进行分析&#xff1a…

实验五 时序逻辑电路部件实验

一、实验目的 熟悉常用的时序逻辑电路功能部件&#xff0c;掌握计数器、了解寄存器的功能。 二、实验所用器件和仪表 1、双 D触发器 74LS74 2片 2、74LS162 1片 3、74194 1片 4、LH-D4实验仪 1台 1.双…

iOS 中的 nil、Nil、NULL、NSNull 僵尸对象和野指针

iOS 中的 nil、Nil、NULL、NSNull 僵尸对象和野指针-CSDN博客 类型含义使用场景示例nil表示一个指向 Objective - C 对象的空指针。在 Objective - C 和 Swift&#xff08;与 Objective - C 交互时&#xff09;中用于表示对象不存在。当一个对象变量没有指向任何有效的对象实例…

JS面试题|[2024-12-28]

1.JS的设计原理是什么&#xff1f; JS引擎 运行上下文 调用栈 事件循环 回调 执行流程&#xff1a; JS引擎将代码解析为电脑可以执行的代码&#xff0c;调用一些API&#xff08;运行上下文&#xff09;让浏览器执行 JS是单线程的&#xff0c;每次从调用栈里面取出来的代码进行调…

全面了解 SQL Server:功能、优势与最佳实践

SQL Server 是微软公司推出的一款关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;广泛应用于企业级数据存储、数据分析、应用开发等领域。作为全球最受欢迎的数据库管理系统之一&#xff0c;SQL Server 提供了强大的功能和工具&#xff0c;支持从小型应用到大型…

jdk动态代理和cglib动态代理对比

jdk动态代理和cglib动态代理对比&#xff1a; CGLIB 和 JDK 动态代理都可以用来在运行时生成代理对象 1. 基本概念 JDK 动态代理&#xff1a;只代理接口&#xff08;interface&#xff09;&#xff0c;无法代理类。它使用 java.lang.reflect.Proxy 类和 java.lang.reflect.I…

攻破 Kioptix Level 1 靶机

找教程然后自己练习&#xff0c;论菜狗的自我修养 基本步骤 1.确定目标IP 2.扫描端口&#xff0c;服务&#xff0c;版本信息&#xff0c;漏洞信息 3.查找漏洞可利用脚本 4.运行脚步 一、信息获取 arp-scan -l nmap -sS -p- -sV -sC -A --min-rate5000 192.168.5.130 二、查…

b站ip属地评论和主页不一样怎么回事

在浏览B站时&#xff0c;细心的用户可能会发现一个有趣的现象&#xff1a;某些用户的评论IP属地与主页显示的IP属地并不一致。这种差异引发了用户的好奇和猜测&#xff0c;究竟是什么原因导致了这种情况的发生呢&#xff1f;本文将对此进行深入解析&#xff0c;帮助大家揭开这一…

音视频入门基础:MPEG2-PS专题(2)——使用FFmpeg命令生成ps文件

通过FFmpeg命令可以将mp4文件转换为ps文件。由于ps文件对应的FFInputFormat结构为&#xff1a; const FFInputFormat ff_mpegps_demuxer {.p.name "mpeg",.p.long_name NULL_IF_CONFIG_SMALL("MPEG-PS (MPEG-2 Program Stream)"),.p.flags …

【数据结构与算法】单向链表

一、什么是链表 链表由一系列节点组成&#xff0c;每个节点都包含一个 data 域&#xff08;存放数据&#xff09;和一个 next 域&#xff08;指向下一节点&#xff09;。链表中的节点可以按照任意顺序存放在内存中&#xff0c;它们之间并不连续。每个节点都记录了下一个节点的地…

【计组】例题课后题

第一章 计算机如何区分指令和数据&#xff1f; 一般来讲&#xff0c;在取指周期中从内存读出的信息是指令流,它流向控制器&#xff1b;而执行周期中从内存读出的信息流是数据流,它由内存流向运算器。 从存放位置看&#xff0c;从代码段取出的是指令流&#xff0c;从数据…

一网多平面

“一网多平面”是一种网络架构概念&#xff0c;具体指的是在一张物理网络之上&#xff0c;逻辑划分出“1N”个平面。以下是对“一网多平面”的详细解释&#xff1a; 定义与构成 01一网多平面 指的是在统一的物理网络基础设施上&#xff0c;通过逻辑划分形成多个独立的网络平面…

跨语言数据格式标准化在 HarmonyOS 开发中的实践

文章目录 前言数据格式标准化的意义数据传递中的痛点标准化的优势 JSON 与 Protocol Buffers 的比较JSONProtocol Buffers HarmonyOS 跨语言数据传递示例示例代码&#xff1a;定义 Protocol Buffers 消息格式生成 Java 和 C 代码示例代码&#xff1a;Java 端序列化与传递数据C …

【Python】基于界面库PyQt5+Qt Dsigner的环境配置和界面绘制

目录 一 安装PyQt5以及PyQt5-tools 二 配置外部开发工具 三 使用Qt Designer设计界面 四 使用PyUIC将ui文件转换为py文件 五 CU分离实现逻辑代码 一 安装PyQt5以及PyQt5-tools 之前做的一些Python脚本、软件都是基于 Tkinter 实现的&#xff0c;其中界面的设计布局是很头疼…

Java读取InfluxDB数据库的方法

本文介绍基于Java语言&#xff0c;读取InfluxDB数据库的方法&#xff0c;包括读取InfluxDB的所有数据库&#xff0c;以及指定数据库中的measurement、field、tag等。 首先&#xff0c;创建一个Java项目&#xff0c;用于撰写代码。如果大家是基于IDEA来创建项目&#xff0c;则可…

嵌入式驱动开发详解7(并发、竞争、中断)

文章目录 前言并发和竞争原子操作自旋锁信号量互斥体 中断中断简介中断API上半部和下半部设备树分析中断号获取源码 后续参考文献 前言 中断会引起线程的切换&#xff0c;并发和竞争也是对线程切换的一种灵活保护和处理&#xff0c;因此这里将中断和并发与竞争放在一块讲解说明…

11.MySQL视图特性

目录 视图基本使用视图规则和限制 视图 视图是一个虚拟表, 其内容有查询定义. 同真实的表一样, 视图包含一系列带有名称的列和行数据. 视图的数据变化会影响到基表, 基表的数据变化也会影响到视图, 这个视图和刚刚讲的ReadView之间没有任何关系. 基本使用 创建视图: create vi…

【RabbitMQ高级篇】消息可靠性问题(1)

目录 1.消息可靠性 1.1.生产者消息确认 1.1.1.修改配置 1.1.2.定义Return回调 1.1.3.定义ConfirmCallback 1.2.消息持久化 1.2.1.交换机持久化 1.2.2.队列持久化 1.2.3.消息持久化 1.3.消费者消息确认 1.3.1.演示none模式 1.3.2.演示auto模式 1.4.消费失败重试机制…