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{找萝卜 -> 吃萝卜} -> 巡逻} -> 循环