当前位置: 首页 > news >正文

Unity学习日志番外:简易行为树

Unity简单行为树

      • 参考与代码来自b站-ANVER-大佬
      • 教学视频
      • 以下都是一种固定模板结构,便于外部以及新项目引用。
      • 1.BehaviorTree类
      • 2.Node类
      • 3.composite
      • 4.Sequence
      • 5.Selector
      • 6.Task
      • 7.Blackboard
      • 8.实例
        • ①兔子行为树
        • ②巡逻任务
        • ③探测萝卜任务
        • ③吃萝卜任务
      • 个人对行为树的理解

参考与代码来自b站-ANVER-大佬

教学视频

以下都是一种固定模板结构,便于外部以及新项目引用。

1.BehaviorTree类

一个BehaviorTree应该包括:
1.Node节点的定义与声明。

2.Blackboard充当大脑用于存储string映射到object的键值对,因为object是所有类型的基类,在转化的过程中会产生很多的拆装箱影响性能,不过由于行为树本身就不是什么庞大(1e6甚至更多那种)底层由哈希表实现在没有哈希冲突的前提下是O(1)的时间复杂度,又由于数据范围很小所以基本不会哈希碰撞,哈希碰撞会导致时间复杂度提高到O(n)。
3.类似状态机结构中的玩家脚本,行为树脚本也是直接挂载在主物体身上的,所以在结构上需要有Blackboard,以及任务或者树的初始状态,类似状态机一开始是处于IdleState,并且在awake的时候获取它和声明它们。
5.在Update里初始化的方法应该是根节点的评估方法。


using JetBrains.Annotations;
using UnityEngine;

namespace BehaviourTrees
{
    [RequireComponent(typeof(Blackboard))]
    public class BehaviourTree : MonoBehaviour
    {
        private Node root;

        public Node Root
        {
            get => root;
            protected set => root = value;
        }

        private Blackboard blackboard;

        [UsedImplicitly]
        private void Awake()
        {
            blackboard = GetComponent<Blackboard>();
            OnSetup();
        }

        public Blackboard Blackboard
        {
            get => blackboard;
            set => blackboard = value;
        }

        [UsedImplicitly]
        // Update is called once per frame
        void Update()
        {
            root?.Evaluate(gameObject.transform, blackboard);
        }

        protected virtual void OnSetup()
        {
        }
    }
}

2.Node类

一个Node类应该包括:
1.currentState目前的状态{Failure = 0, Success, Running}。
2.该节点的父节点以及该节点的子节点。
3.评估节点状态逻辑函数。

using System;
using System.Collections.Generic;
using UnityEngine;

namespace BehaviourTrees
{
    public enum Status
    {
        Failure = 0,
        Success,
        Running
    }

    public abstract class Node
    {
        protected Node parent;
        protected List<Node> children = new List<Node>();
        
        public Status status { get; protected set; }


        public Status Evaluate(Transform agent, Blackboard blackboard)
        {
            status = OnEvaluate(agent, blackboard);
            return status;
        }

        protected abstract Status OnEvaluate(Transform agent, Blackboard blackboard);
    }

}

3.composite

复合结构直接继承于Node用于被Sequence和Selector继承

4.Sequence

Sequence:Sequence的逻辑是AND,要求当前队列下的所有子节点都要是Success才返回success否者返回Failure,如果还在执行返回Running。

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace BehaviourTrees
{
    public class Sequencer : Composite
    {
        public Sequencer(List<Node> children)
        {
            this.children = children;
        }

        protected override Status OnEvaluate(Transform agent, Blackboard blackboard)
        {
            bool isRunning = false;
            bool success = children.All((child) =>
            {
                Status status = child.Evaluate(agent, blackboard);
                switch (status)
                {
                    case Status.Failure:
                        return false;
                    case Status.Running:
                        isRunning = true;
                        break;
                }

                return status == Status.Success;
            });

            return isRunning ? Status.Running : success ? Status.Success : Status.Failure;
        }
    }
}

5.Selector

Selector:一个Selector的逻辑是or,要求当前队列下的所有子节点都要是failure才返回Failure否者返回Success,如果还在执行返回Running。

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace BehaviourTrees
{
    public class Selector : Composite
    {
        public Selector(List<Node> children)
        {
            this.children = children;
        }

        protected override Status OnEvaluate(Transform agent, Blackboard blackboard)
        {
            bool isRunning = false;
            bool failed = children.All((child) =>
            {
                Status status = child.Evaluate(agent, blackboard);
                if (status == Status.Running) isRunning = true;
                return status == Status.Failure;
            });

            return isRunning ? Status.Running : failed ? Status.Failure : Status.Success;
        }
    }
}

6.Task

Task继承于Node应该对Node中的评估方法Evaluate进行重写,用于描述挂载脚本物体的逻辑。

7.Blackboard

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting.YamlDotNet.Core.Tokens;
using UnityEngine;

namespace BehavioursTree
{
    public class Blackboard : MonoBehaviour
    {
        private Dictionary<string, object> data = new Dictionary<string, object>();

        public T Get<T>(string key)
        {
            if (data.TryGetValue(key, out object value)) return (T)value;
            return default(T);
        }

        public void Add<T>(string key, T value)
        {
            data.Add(key, value);
        }
        public bool Remove<T>(string key)
        {
            if (data.ContainsKey(key))
            {
                data.Remove(key);
                return true;
            }
            return false;
        }
    }
}

8.实例

①兔子行为树
using BehaviourTrees;
using System.Linq;
using UnityEngine;

public class RabbitBehaviourTree : BehaviourTree
{
    [SerializeField] private Transform[] waypoints = null;
    [SerializeField] private float speed = 10.0f;

    protected override void OnSetup()
    {
        Blackboard.Add("speed", speed);
        var patrolTask = new PatrolTask(waypoints);

        var seeCarrotTask = new SeeCarrotTask();
        var catchCarrotTask = new CatchCarrotTask();

        Node[] sequencerChildren = { seeCarrotTask, catchCarrotTask };
        var sequencer = new Sequencer(sequencerChildren.ToList());

        Node[] selectorChildren = { sequencer, patrolTask };
        var selector = new Selector(selectorChildren.ToList());

        Root = selector;
    }
}
AWAKE
        private void Awake()
        {
            blackboard = GetComponent<Blackboard>();
            OnSetup();
        }
UPDATE
		void Update()
		{
		    root?.Evaluate(gameObject.transform, blackboard);
		}

在这里插入图片描述

这里的兔子行为树定做了:
1.声明巡逻任务。
2.声明探测到萝卜任务。
3.声明吃萝卜任务。
4.声明一个Sequence包含两个子节点①探测到萝卜②吃萝卜。
5.声明一个Seletor包含两个子节点①Sequence②巡逻。

②巡逻任务
using BehaviourTrees;
using UnityEngine;

public class PatrolTask : Task
{
    private int currentIndex;
    private Transform[] waypoints;


    public PatrolTask(Transform[] waypoints)
    {
        this.waypoints = waypoints;
        currentIndex = 0;
    }

    protected override Status OnEvaluate(Transform agent, Blackboard blackboard)
    {
        float speed = blackboard.Get<float>("speed");
        Transform currentWaypoint = waypoints[currentIndex];
        bool arrived = Vector2.Distance(agent.position, currentWaypoint.position) < 0.1f;
        if (arrived)
        {
            // update current index
            ++currentIndex;
            currentIndex %= waypoints.Length;
        }

        agent.position = Vector2.MoveTowards(agent.position, currentWaypoint.position, speed * Time.deltaTime);

        return Status.Running;
    }
}

巡逻任务包括1:n个巡逻点可以通过Inspector拖动赋值。
2:评估的主要逻辑。

③探测萝卜任务
using System.Collections;
using System.Collections.Generic;
using BehaviourTrees;
using UnityEngine;

public class SeeCarrotTask : Task
{
    private float radius = 2.0f;

    protected override Status OnEvaluate(Transform agent, Blackboard blackboard)
    {
        var colliders = Physics2D.OverlapCircleAll(agent.position, radius);
        if (colliders == null) return Status.Failure;

        foreach (Collider2D collider in colliders)
        {
            if (!collider.CompareTag("Carrot")) continue;
            blackboard.Add("carrot", collider.gameObject);
            return Status.Success;
        }

        return Status.Failure;
    }
}

探测萝卜任务应该包括:
1.探测半径。

③吃萝卜任务
using System.Collections;
using System.Collections.Generic;
using BehaviourTrees;
using UnityEngine;

public class CatchCarrotTask : Task
{
    protected override Status OnEvaluate(Transform agent, Blackboard blackboard)
    {
        var carrot = blackboard.Get<GameObject>("carrot");
        var speed = blackboard.Get<float>("speed");

        if (carrot == null) return Status.Failure;

        if (Vector2.Distance(agent.position, carrot.transform.position) <= 0.01f)
            return Status.Success;

        Vector2 position = Vector2.MoveTowards(agent.position, carrot.transform.position, speed * Time.deltaTime);
        position.y = agent.position.y;
        agent.position = position;
        return Status.Running;
    }
}

吃萝卜逻辑:
检测之前在探测萝卜任务中加入到大脑的萝卜object,使用Movetowards进行两点间的移动。

个人对行为树的理解

**行为树的核心机制就是每一帧从根节点开始遍历其子节点,并根据子节点的状态调用对应的 Evaluate 方法。这种行为树的运行方式是典型的深度优先遍历,并且是基于帧的更新(Frame-based Update)。**在这个案例中Selector含有对Sequence和巡逻Task的引用,遍历的顺序就是:注意箭头顺序
在这里插入图片描述
Selector->{Sequence{找萝卜 -> 吃萝卜} -> 巡逻} -> 循环

相关文章:

  • XML Schema 实例
  • 孔夫子根剧关键字获取在售商品 API
  • iOS开发,SQLite.swift, Missing argument label ‘value:‘ in call问题
  • Docker(认识且会基础操作)
  • LeetCode 解题思路 15(Hot 100)
  • IDEA 一键完成:打包 + 推送 + 部署docker镜像
  • 面试题之webpack file-loader和url-loader
  • 前端面试:cookie 可以实现不同域共享吗?
  • 2025年渗透测试面试题总结-阿里巴巴-阿里云安全 一面、三面(题目+回答)
  • 低光图像增强新突破!HVI 色彩空间 + CIDNet 网络如何攻克红黑噪声难题?
  • 【Linux】进程间通信:匿名管道与进程池
  • 全面解析与实用指南:如何有效解决ffmpeg.dll丢失问题并恢复软件正常运行
  • java数据处理:Map<String, Object>、Map<String, List<Room>>、Map<String, Integer>
  • VBA+FreePic2Pdf 找出没有放入PDF组合的单个PDF工艺文件
  • 【vue3学习笔记】(第150-151节)computed计算属性;watch监视ref定义的数据
  • LeetCode 力扣热题100 单词拆分
  • RocketMQ性能优化篇
  • 深度学习 bert流程
  • Spring Boot3.3.X整合Mybatis-Plus
  • 【Azure 架构师学习笔记】- Azure Databricks (18) --Delta Live Table 架构
  • 商务部:对原产于美国、欧盟、台湾地区和日本的进口共聚聚甲醛征收反倾销税
  • 中国新闻发言人论坛在京举行,郭嘉昆:让中国声音抢占第一落点
  • 没有握手,采用翻译:俄乌三年来首次直接会谈成效如何?
  • 高新波任西安电子科技大学校长
  • 昔日千亿房企祥生集团约2.03亿元债权被拍卖,起拍价8000万元
  • 泉州围头湾一港区项目炸礁被指影响中华白海豚,官方:已叫停重新评估