行为树详解(2)——最简单的行为树

news2024/11/26 14:07:41

【需求分析】

结合具体的实际需求来:假设有这样一个NPC,绕着固定的某个点以半径20米随机巡逻,让主角进入时,开始朝着主角攻击,距离主角3米时,开始攻击主角,如果距离主角超过10米,则重新巡逻 

需求描述起来很简单,但实现起来不简单,对于这样一个需求,首先做行为拆解:

  1. 固定20米巡逻
  2. 进入巡逻范围开始追击
  3. 距离主角3米时开始攻击
  4. 追击时距离主角超过10米开始回归巡逻

行为基本就是条件+动作,动作很简单,复杂的是条件。

其次,划分行为所属的状态。

如何做状态划分是任意的,划分的原则是尽可能让状态更少,这里将NPC的四个行为分成巡逻和追击两个状态。

我们明明说的是行为树,为何这里又涉及到状态了:

  • 举例不复杂,行为很少
  • 这里的状态和状态机中的状态不是一个意思,行为树中也需要状态,这些状态隐含于条件之下,是不同层次的状态,而状态机中的状态都是同一层次下的
  • 可以将行为树看作是具有不同层次状态的分层状态机,其没有脱离状态机的范畴

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

【一般实现】

因为条件是实时变化的,所以我们需要在Tick中不断做条件判断。

我们专注于条件,动作先省略。

    public class SimpleNPCAI:MonoBehaviour
    {
        private GameObject owner;
        private GameObject target;
        private Vector3 idleCenterPos;

        private bool idle;
        private void Update()
        {
            if(idle)
            {
                if(Vector3.Distance(idleCenterPos, target.transform.position)<20)
                {
                    Debug.Log("执行巡逻时的动作");
                }
                else
                {
                    idle = false;
                }
                
            }
            else
            {
                var dis = Vector3.Distance(owner.transform.position, target.transform.position);
                if(dis<3)
                {
                    Debug.Log("执行攻击的动作");
                }
                else if(dis>10)
                {
                    Debug.Log("执行回归巡逻的动作");
                    idle = true;
                }
                else
                {
                    Debug.Log("执行寻路追踪的目标的动作");
                }
            }
        }
    }

【行为树实现】

根据前文的描述,可以确认出如下几点,这些点构成了我们需要创建的类、类的字段、类的方法:

  • 有行为树和节点两个概念,对应两个类,有不同种类的节点,继而继承出不同类型的节点。
  • 行为树需要有一个进入的根节点
  • 行为树和节点之前要互相持有,确定归属关系
  • 行为树和主体要互相持有,确定归属关系
  • 不同节点要有ID、Name、Type等做区分
  • 父节点要有子节点
  • 没有做编辑配置,需要有添加删除各类节点的方法
  • 每个节点有自己的执行逻辑
  • 等等

合起来代码如下:

    public class SimpleBT
    {

        private GameObject owner;
        private Node rootNode;
        public void Init(GameObject owner)
        {
            this.owner = owner;
        }

        public NodeStatus Update()
        {
            var res = rootNode.Update();
            return res;
        }

        public void Destroy()
        {

        }
    }

    public enum NodeStatus
    {
        Success,
        Fail,
        Running,
    }

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

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

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

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

        public virtual void Destroy()
        {

        }

    }

    public class RootNode:Node 
    {
        public Node subNode;

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

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

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

    public class SequenceNode:ControlNode
    {
        protected override NodeStatus OnUpdate()
        {
            foreach (var node in subNodes)
            {
                var status = node.Update();
                if (status != NodeStatus.Success)
                    return status;
            }
            return NodeStatus.Success;
        }
    }

    public class ActionNode:Node
    {
        protected override NodeStatus OnUpdate()
        {
            return base.OnUpdate();
        }
    }

基本逻辑如下:

  • 主体调用SimpleBT的Init,添加节点方法,节点的Init方法完成整个行为树的数据初始化
  • 主体调用SimpleBT的Update方法,继而层层调用到不同节点的Update方法
  • 主体调用SimpleBT的Destroy方法,不同节点的Destroy方法销毁整个行为树

【几点说明】

是否继承MonoBehaviour的区别

继承的好处在于可以将脚本直接挂载在GameObject上,同时也可以很方便的在Inspector中做数据显示

不继承时数据显示也能做,会麻烦些,同时在代码中添加行为树。一般来说,做通用的东西,其核心不会继承MonoBehaviour

如何添加数据

我们没有在示例代码中给出添加节点的方法,这些方法的实现比较容易

添加节点方法本质是为了给定行为树的初始化数据,通常,初始化数据要支持编辑配置

我们这里假定初始化数据已经有了

【如何使用】

基本的调用如下:

public class NPC:MonoBehaviour
{
    private SimpleBT bt;
    public bool idle;
    public Vector3 idleCenterPos;
    public GameObject target;
    void Start()
    {
        bt = new SimpleBT();
        bt.Init(this.gameObject);
        //初始化bt数据
    }

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

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

在数据初始化中,RootNode的subNode是一个SequenceNode

SequenceNode的subNode依次是不同的ActionNode。这里简单来说就两个Node,每个状态下一种,Node实现如下

这里为了保真逻辑的正常执行,节点返回的NodeStatus有些奇怪。实际上应该有更多的节点来保证逻辑的正常,我们再后面的文章中结合实际来添加更多节点。

    public class IdleNode:ActionNode
    {
        protected override NodeStatus OnUpdate()
        {
            var npc = owner.owner.GetComponent<NPC>();
            var res = NodeStatus.Fail;
            if(npc != null)
            {
                if (npc.idle)
                {
                    if (Vector3.Distance(npc.idleCenterPos, npc.target.transform.position) < 20)
                    {
                        Debug.Log("执行巡逻时的动作");
                        res = NodeStatus.Fail;
                    }
                    else
                    {
                        npc.idle = false;
                        res = NodeStatus.Success;
                    }
                }
                else
                {
                    res = NodeStatus.Success;                   
                }
            }
            return res;
        }
    }

    public class AttackNode : ActionNode
    {
        protected override NodeStatus OnUpdate()
        {
            var npc = owner.owner.GetComponent<NPC>();
            var res = NodeStatus.Fail;
            if(npc != null)
            {
                var dis = Vector3.Distance(owner.owner.transform.position, npc.target.transform.position);
                if (dis < 3)
                {
                    Debug.Log("执行攻击的动作");
                }
                else if (dis > 10)
                {
                    Debug.Log("执行回归巡逻的动作");
                    npc.idle = true;
                }
                else
                {
                    Debug.Log("执行寻路追踪的目标的动作");
                }
                res = NodeStatus.Success;
            }
            return res;
        }

    }

前后对比可以发现:真正的执行动作的代码区别不大,执行时所需参数的来源和之前一样

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

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

相关文章

车载测试核心知识点和面试题

今天为大家分享一下车载测试岗位面试的时候&#xff0c;一定会问的相关技术。这些工具在测试的工作中会用到&#xff0c;在面试中也会经常被问到。所以同学们一定要去实战操作&#xff0c;这样理解和吸收才会更加深刻。 一、车载仪表台架测试CANoe工具实战 我们知道&#xff…

C/C++逆向:虚函数逆向分析

虚函数&#xff08;Virtual Function&#xff09;是C中实现多态的一种机制&#xff0c;它允许在运行时通过基类的指针或引用调用派生类中的函数&#xff0c;而不是基类中的版本。虚函数通常与继承和多态结合使用。通过在基类中使用 virtual 关键字声明函数&#xff0c;允许派生…

es写入磁盘的过程以及相关优化

数据写入到内存buffer同时写入到数据到translog buffer,这是为了防止数据不会丢失每隔1s数据从buffer中refresh到FileSystemCache中,生成segment文件,这是因为写入磁盘的过程相对耗时,借助FileSystemCache,一旦生成segment文件,就能通过索引查询到了refresh完,memory bu…

linux部署Whisper 视频音频转文字

github链接&#xff1a;链接 我这里使用anaconda来部署&#xff0c;debian12系统&#xff0c;其他linux也同样 可以使用gpu或者cpu版本&#xff0c;建议使用n卡&#xff0c;rtx3060以上 一、前期准备 1.linux系统 链接&#xff1a;debian安装 链接&#xff1a;ubuntu安装 …

论文阅读:A Software Platform for Manipulating theCamera Imaging Pipeline

论文代码开源链接&#xff1a; A Software Platform for Manipulating the Camera Imaging Pipelinehttps://karaimer.github.io/camera-pipeline/摘要&#xff1a;论文提出了一个Pipline软件平台&#xff0c;可以方便地访问相机成像Pipline的每个阶段。该软件允许修改单个模块…

【科研绘图】Matplotlib 教学

以下是一个针对 Matplotlib 教学 的博客结构&#xff0c;按照分步骤教学方式撰写&#xff0c;以帮助读者从基础到高级逐步掌握 Matplotlib。 Matplotlib 教学&#xff1a;从基础到进阶绘图 Matplotlib 是 Python 中功能强大的数据可视化库&#xff0c;可以用来绘制多种类型的图…

【网络系统管理】2023年全国职业院校技能大赛:组策略--10套题组合--4

16、只有域管理员和IT部门员工可以登陆服务器 (1)计算机配置\策略\Windows设置\安全设置\本地策略\用户权限分配 17、创建ChinaSkills23为GPO管理员,加入到企业管理、域控管理员组 (1)gpmc.msc\林\域\%domain%--在这个域中创建GPO 18、为所有域用户设置漫游文件 (1)用…

钉钉授权登录

一.找开钉钉开发平台【钉钉开放平台 (dingtalk.com)】 二。点击菜单【应用开发】->左边【钉钉应用】->【创建应用】 三。创建应用-》保存成功后&#xff0c;点击自己【新建的应用】&#xff0c;进入详细页面 四。进入应用详细页面。左边【分享设置】 注意&#xff1a;进…

应用系统开发(14) 涡流检测系统硬件设计

涡流检测整体系统架构 涡流检测系统整体结构如上图 所示,DAC 转换与功率放大电路将数字正弦信号转 换为模拟正弦信号,为涡流探头提供正弦激励。互感式探头由两个线圈组成,一个作为 激励,另一个接收检测信号,AD 转换电路将传感器探头感应到的电压滤波放大,将电 压值调整到…

介绍一下strupr(arr);(c基础)

hi , I am 36 适合对象c语言初学者 strupr(arr)&#xff1b;函数是把arr数组变为大写字母 格式 #include<string.h> strupr(arr); 返回值为arr 链接分享一下arr的意义(c基础)(必看)(牢记)-CSDN博客 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #incl…

人工智能之数学基础:向量的基本知识

本文重点 向量的基本性质是线性代数和向量空间理论的核心,它们为向量运算提供了坚实的基础,并在物理、工程、计算机图形学等领域有着广泛的应用。本文对向量的一些基本知识进行介绍,帮助大家快速理解向量。 向量的定义与表示 向量是一个既有大小又有方向的量,通常用带箭…

Redis中的zset底层实现

文章目录 Redis中的zset底层实现一、引言二、zset的底层实现1、ziplist编码1.1、代码示例 2、skiplist编码2.1、代码示例 三、总结 Redis中的zset底层实现 一、引言 Redis的有序集合&#xff08;zset&#xff09;是一种非常强大的数据结构&#xff0c;它不仅能够存储元素&…

TSmaster CAN/CANFD 诊断(Diagnostic_CAN)

文章目录 1、Diagnostic TP 参数配置1.1 传输层参数&#xff1a;1.2 服务层参数1.3 Seed&Key 2、基础诊断配置2.1 添加/删除 服务2.2 配置 BasicDiagnostic 服务参数 3、诊断控制台4、自动诊断流程4.1 流程用例管理4.2 配置诊断流程&#xff08;UDS Flow&#xff09;4.2.1 …

大语言模型---LoRA中损失值的计算

文章目录 概要损失计算流程小结 概要 Llama-7B模型的LoRA微调训练中&#xff0c;通过使用Cross-Entropy Loss来度量模型输出的预测分布和真实标签分布之间的距离&#xff0c;来衡量模型的准确性。 本文主要介绍LoRA中损失值的计算流程。 Cross-Entropy Loss作用&#xff1a;是…

Linux笔记--基于OCRmyPDF将扫描件PDF转换为可搜索的PDF

1--官方仓库 https://github.com/ocrmypdf/OCRmyPDF 2--基本步骤 # 安装ocrmypdf库 sudo apt install ocrmypdf# 安装简体中文库 sudo apt-get install tesseract-ocr-chi-sim# 转换 # -l 表示使用的语言 # --force-ocr 防止出现以下错误&#xff1a;ERROR - PriorOcrFoundE…

使用 Nginx 在 Ubuntu 22.04 上安装 LibreNMS 开源网络监控系统

#LibreNMS 是一个功能强大的开源网络监控系统&#xff0c;它能够为你的网络性能和设备提供全面的监控。本文将引导你通过一系列步骤&#xff0c;在 Ubuntu 22.04 服务器上安装和配置 LibreNMS&#xff0c;使用 Nginx 作为 Web 服务器。 简介 LibreNMS 提供了对网络设备和性能…

elementUI非常规数据格式渲染复杂表格(副表头、合并单元格)

效果 数据源 前端代码 (展示以及表格处理/数据处理) 标签 <el-table :data"dataList" style"width: 100%" :span-method"objectSpanMethod"><template v-for"(item, index) in headers"><el-table-column prop"…

使用脚本实现hadoop-yarn-flink自动化部署

本文使用脚本实现hadoop-yarn-flink的快速部署&#xff08;单机部署&#xff09;。 环境&#xff1a;①操作系统&#xff1a;CentOS 7.6&#xff1b;②CPU&#xff1a;x86&#xff1b;③用户&#xff1a;root。 1.前置条件 把下面的的脚本保存到“pre-install.sh”文件&#x…

Linux系统编程之进程基础知识

概述 在Linux系统中&#xff0c;进程是指一个正在运行的程序实例。每个进程都有一个唯一的进程标识符&#xff0c;即PID&#xff0c;操作系统通过这个PID来唯一识别和管理各个进程。进程不仅仅是程序代码的运行实例&#xff0c;它还包含了程序运行时所需的各种资源&#xff0c;…

H.264/H.265播放器EasyPlayer.js网页全终端安防视频流媒体播放器关于iOS不能系统全屏

在数字化时代&#xff0c;流媒体播放器已成为信息传播和娱乐消遣的主流载体。随着技术的进步&#xff0c;流媒体播放器的核心技术和发展趋势不断演变&#xff0c;影响着整个行业的发展方向。 EasyPlayer播放器属于一款高效、精炼、稳定且免费的流媒体播放器&#xff0c;可支持…