行为树详解(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;}}
相关文章:
行为树详解(5)——事件驱动
【分析】 如果行为树的节点很多,那么会存在要经过很多节点才会走到动作节点的情况。显然,性能上不如状态机。 每帧都需要重新遍历一系列节点才会走到动作节点,而实际上很多条件节点在数帧内不会有变化,这是造成性能问题的重要原…...
3.若依前端项目拉取、部署、访问
因为默认RuoYi-Vue是使用的Vue2,所以需要另外去下载vue3来部署。 拉取代码 git clone https://gitee.com/ys-gitee/RuoYi-Vue3.git 安装node才能执行npm相关的命令 执行命令npm install 如果npm install比较慢的话,需要添加上国内镜像 npm install --registrhttp…...
Debian操作系统相对于Ubuntu有什么优势吗?
更高的稳定性:Debian 以其出色的稳定性闻名,得益于严格的软件包测试和发布流程。其稳定版经过长时间测试与验证,确保了系统的高度稳定,更适合对稳定性要求极高的长期运行服务器环境。而 Ubuntu 虽有稳定版本,但更新周期…...
【漏洞复现】CVE-2015-3337 Arbitrary File Reading
漏洞信息 NVD - CVE-2015-3337 Directory traversal vulnerability in Elasticsearch before 1.4.5 and 1.5.x before 1.5.2, when a site plugin is enabled, allows remote attackers to read arbitrary files via unspecified vectors. 在安装了具有“site”功能的插件以…...
win10、win11-鼠标右键还原、暂停更新
系统优化 win 10jihuo win 11jihuo鼠标右键还原暂停更新 update 2024.12.28win 10 jihuo winx,打开powershell管理员,输入以下命令,选择1并等待 irm https://get.activated.win | iex参考:https://www.bilibili.com/video/BV1TN411M72J/?sp…...
FFmpeg来从HTTP拉取流并实时推流到RTMP服务器
当使用FFmpeg来从HTTP拉取流并实时推流到RTMP服务器时,你可以使用以下命令: ffmpeg -i http://输入流地址 -c:v copy -c:a copy -f flv rtmp://RTMP服务器地址/应用名称/流名称 这是一个基本的命令示例,其中: - -i http://输入流地…...
Quo Vadis, Anomaly Detection? LLMs and VLMs in the Spotlight 论文阅读
文章信息: 原文链接:https://arxiv.org/abs/2412.18298 Abstract 视频异常检测(VAD)通过整合大语言模型(LLMs)和视觉语言模型(VLMs)取得了显著进展,解决了动态开放世界…...
Rust : tokio中select!
关于tokio的select宏,有不少的用途。包括超时和竞态选择等。 关于select宏需要关注,相关的异步条件,会同时执行,只是当有一个最早完成时,会执行“抛弃”和“对应”策略。 说明:对本文以下素材的来源表示感…...
【hackmyvm】hacked靶机wp
tags: HMVrootkitDiamorphine Type: wp 1. 基本信息^toc 文章目录 1. 基本信息^toc2. 信息收集2.1. 端口扫描2.2. 目录扫描2.3. 获取参数 3. 提权 靶机链接 https://hackmyvm.eu/machines/machine.php?vmHacked 作者 sml 难度 ⭐️⭐️⭐️⭐️️ 2. 信息收集 2.1. 端口扫描…...
MaixBit k210学习记录
开发背景:Window系统主机,在主机上安装了虚拟机(VirtualBoxUbuntu23.04) 目标实现:在虚拟机(Ubuntu)中,实现对Maix bit(k210)开发板的开发 虚拟机的安装参考…...
Wordperss漏洞 DeDeCMS漏洞
Wordperss漏洞 环境搭建 #执⾏命令 cd /vulhub/wordpress/pwnscriptum docker-compose up -d #靶场地址 http://8.155.7.173:8080/wp-admin/ 注册账号 登录 漏洞一:后台修改模板拿WebShell 步骤一:思路是修改其WP的模板写入⼀句话木马后门并访问其文件…...
如何构建有效的AI Agents:从复杂到简约——深度解读Claude实践总结《Building effective agents》(上)
在人工智能技术日新月异的今天,大语言模型(LLM)已经成为技术创新的热点。 然而,在追逐技术前沿的热潮中,我们是否忽视了工程设计的本质? 作为全球人工智能领域的领军企业之一,Anthropic以其在AI安全和伦理方面的深入…...
git status 耗时
某个git库每次status一下就是半小时起步,gc后还是没有效果,后来排查记录发现某笔记录提交几百G的冗余文件,虽然revert了,但是还是存在库中,遂如下清理: # 查找大文件 git verify-pack -v .git/objects/pac…...
C++进阶重点知识(一)|智能指针|右值|lambda|STL|正则表达式
目录 1智能指针1.shared_ptr1.1 shared_ptr的基本用法使用shared_ptr要注意的问题运用 2.unique_ptr独占的智能指针示例:管理动态内存 3.weak_ptr弱引用的智能指针weak_ptr的基本用法lock 的作用:weak_ptr返回this指针weak_ptr解决循环引用问题weak_ptr使…...
OSCP打靶大冒险之Solidstate:多端口获取信息,shell逃逸,计划任务提权
声明! 学习资源来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关&a…...
八股(One Day one)
最近老是看到一些面试的视频,对于视频内部面试所提到的八股文,感觉是知道是什么,但是要说的话,却又不知道该怎么说(要不咋称之为八股文呢),所以就想到写一篇八股文总结的博客,以便进…...
如何快速又安全的实现端口转发【Windows MAC linux通用】
背景 有很多程序是在虚拟机上运行的,返回的url 又是127.0.0.1。在个人电脑上调试需要解决这个问题。端口转发是一个不错的方法 可能的解决办法: 1.修改程序,返回虚拟机的ip (要改代码,换虚拟机还要再改代码…...
LongLLMLingua: 长上下文场景的智能提示压缩框架
LongLLMLingua: 长上下文场景的智能提示压缩框架 一、框架概述 核心目标 解决长上下文场景下的计算成本问题降低处理延迟提升模型性能表现 创新特点 问题感知的粗到细压缩策略文档重排序机制动态压缩比例控制压缩后的子序列恢复策略 二、技术方法详解 粗粒度压缩 (a) 文…...
Django serializers:把ValidationError处理的更优雅
开发中必不可少的会对参数做各种校验,必填、唯一等等,这个时候我们需要在校验失败时抛出友好、清晰的提示。 直接使用serializers做校验,虽然也能抛出自定义的错误提示,但是其信息格式是嵌套,并不适合直接反映在前端页…...
CASA(Carnegie-Ames-Stanford Approach) 模型原理及实践技术
植被作为陆地生态系统的重要组成部分对于生态环境功能的维持具有关键作用。植被净初级生产力(Net Primary Productivity, NPP)是指单位面积上绿色植被在单位时间内由光合作用生产的有机质总量扣除自养呼吸的剩余部分。 专题(一)C…...
DeEAR镜像免配置实战:无需修改config.py,直接运行app.py启用全部功能模块
DeEAR镜像免配置实战:无需修改config.py,直接运行app.py启用全部功能模块 1. 开篇:语音情感识别的技术革新 语音情感识别技术正在改变我们与机器交互的方式。想象一下,你的智能助手不仅能听懂你说什么,还能理解你说话…...
造相 Z-Image 电商提效:淘宝主图/拼多多详情页/小红书种草图量产
造相 Z-Image 电商提效:淘宝主图/拼多多详情页/小红书种草图量产 1. 电商视觉内容生产的痛点与机遇 电商卖家每天面临的最大挑战之一就是视觉内容的生产。无论是淘宝主图、拼多多详情页还是小红书种草图文,都需要大量高质量的图片来吸引用户眼球。传统…...
从零到一:在Simulink中构建SVPWM仿真模型的实践指南
1. 为什么选择Simulink搭建SVPWM模型? 第一次接触电机控制时,我被各种专业术语搞得晕头转向。直到发现Simulink这个可视化工具,才真正理解了SVPWM(空间矢量脉宽调制)的精髓。就像用乐高积木搭建城堡,Simuli…...
Reachy Mini桌面机器人:开源AI机器人开发的终极指南
Reachy Mini桌面机器人:开源AI机器人开发的终极指南 【免费下载链接】reachy_mini Reachy Minis SDK 项目地址: https://gitcode.com/GitHub_Trending/re/reachy_mini Reachy Mini是一款专为开发者和AI研究者设计的开源桌面机器人,通过其精密的六…...
OpenClaw × 88API:不用注册 Anthropic,5 分钟让 AI Agent 接入 Claude 4.6(2026 完整教程)
折腾了两天,最后 5 分钟搞定 上周我想用 OpenClaw 搭一个能自动重构代码的 Agent。选定 Claude 4.6 当大脑——毕竟它在 Tool Use 精准度和长上下文推理上确实是第一梯队。 结果卡在了第一步:Anthropic 官方账号注册要海外手机号,好不容易注…...
# 状态通道实战:用Solidity实现高效链下交易与链上结算 在区块链世界中,**扩展性瓶颈**一直是开发者绕
状态通道实战:用Solidity实现高效链下交易与链上结算 在区块链世界中,扩展性瓶颈一直是开发者绕不开的话题。传统智能合约每笔交互都需上链,不仅成本高昂,还导致网络拥堵。而**状态通道(State Channel)**技…...
造相Z-Image文生图模型快速试用:10秒生成高清图片,简单易用
造相Z-Image文生图模型快速试用:10秒生成高清图片,简单易用 1. 快速体验:10秒生成你的第一张AI画作 1.1 一键部署模型 在CSDN星图镜像市场找到"造相 Z-Image 文生图模型(内置模型版)v2"镜像,点…...
多人对话录音整理神器:ClearerVoice-Studio语音分离功能详细教程
多人对话录音整理神器:ClearerVoice-Studio语音分离功能详细教程 1. 引言:告别混乱的多人录音 你是否经常需要整理会议录音、访谈记录或多人讨论内容?传统的录音文件往往混杂着多个人的声音,背景噪音干扰严重,整理起…...
jsoncpp实战:从配置文件解析到网络数据交换,我的C++项目数据管理方案
JSONCPP实战:从配置文件解析到网络数据交换的C数据管理方案 在C后端服务开发中,JSON数据格式因其轻量级和易读性成为配置文件和API通信的首选。作为从业多年的C开发者,我发现jsoncpp库在项目中的灵活运用能显著提升开发效率。本文将分享我在实…...
别再为‘file must be a file‘报错头疼了!手把手教你用Apifox搞定Dify文件上传接口
深度解析Dify文件上传接口:从报错排查到Apifox高效调试实战 当你正在为Dify AI应用集成文件上传功能时,是否曾在Apifox中反复遭遇file must be a file的报错而束手无策?这种看似简单的接口调试背后,隐藏着文件传输机制、参数组合…...
