一、学习记录,初级行为树
1.原理结构介绍
行为树是计算机科学、机器人技术、控制系统和视频游戏中使用的计划执行的数学模型。它们以模块化方式描述有限任务集之间的切换。它们的优势在于能够创建由简单任务组成的非常复杂的任务,而无需担心如何实现简单任务。行为树与分层状态机有一些相似之处,关键区别在于行为的主要构建块是任务而不是状态。行为树易于人类理解,因此不容易出错,在游戏开发者社区中非常受欢迎。行为树已被证明可以概括其他几种控制架构。
2.基本概念
-
节点(Node):行为树由一系列的节点组成,每个节点代表一个决策或行为。节点可以是决策节点或行为节点。
-
决策节点(Decorator):这类节点用来修改或控制子节点的行为。它们通常不执行具体的行为,而是通过改变子节点的执行条件或顺序来影响整个树的行为。
-
行为节点(Action):这些节点包含实际的行为逻辑,比如移动、攻击等。
-
复合节点(Composite):这是一类特殊的决策节点,它们包含多个子节点,并且根据子节点的状态来决定自己的状态。常见的复合节点有:
- Sequence:所有子节点必须依次成功完成,Sequence节点才成功。如果任何一个子节点失败,Sequence节点立即失败。
- Selector:尝试执行子节点,一旦有子节点成功,Selector节点就成功。如果所有子节点都失败,Selector节点才失败。
-
叶节点(Leaf):没有子节点的节点,通常包含具体的行动或决策逻辑。
行为树的执行流程
-
初始化:构建行为树,包括所有节点及其连接。
-
评估:从根节点开始,递归地评估每个节点的状态。每个节点的评估可能产生以下几种状态:
SUCCESS
:节点成功执行。FAILURE
:节点执行失败。RUNNING
:节点的行为正在进行中,需要进一步的评估。
-
状态传播:节点的状态会影响其父节点的状态。复合节点根据子节点的状态来决定自己的状态。
-
终止条件:当根节点的状态不再是
RUNNING
时,行为树的当前评估周期结束。 -
更新周期:在每个更新周期(例如,游戏的每个帧),重复上述评估过程。
3.节点介绍
1. 叶节点(Leaf Node):
- 直接执行具体行为的节点,如移动、攻击等。它们是行为树的最末端节点,不包含子节点。
2. 决策节点(Decorator Node):
- 用来修饰或控制其子节点行为的节点。它们可以基于特定的条件来决定是否执行子节点,或者如何执行子节点。
- 常见的决策节点包括:
- Inverter:反转子节点的返回状态,如果子节点返回`SUCCESS`,则返回`FAILURE`,反之亦然。
- Repeater:重复执行子节点,直到子节点返回`FAILURE`或者达到一定的重复次数。
- UntilFailure 或 UntilSuccess:持续执行子节点直到子节点返回`FAILURE`或`SUCCESS`。
3. 复合节点(Composite Node):
- 包含多个子节点的节点,它们根据子节点的状态来决定自己的行为和状态。
- 常见的复合节点包括:
- Sequence:按顺序执行子节点,如果任何一个子节点返回`FAILURE`,则整个序列失败;如果所有子节点都返回`SUCCESS`,则序列成功。
- Selector*或 Fallback:尝试执行子节点,一旦有子节点返回`SUCCESS`,则整个选择器成功;如果所有子节点都返回`FAILURE`,则选择器失败。
- Parallel:并行执行子节点,根据所有子节点的状态来决定自己的状态。
- Random:随机选择一个子节点来执行。
4. 条件节点(Condition Node):
- 用来评估一个条件是否满足的节点。如果条件满足,返回`SUCCESS`;如果不满足,返回`FAILURE`。条件节点通常用作决策节点的子节点。
5. 运行中节点(Running Node):
- 某些行为可能需要多个步骤或多个帧来完成,这种节点在行为执行过程中会返回`RUNNING`状态,直到行为完成。
6. 等待节点(Wait Node):
- 用来实现等待一定时间或等待某个条件满足后再继续执行的节点。
7. 服务节点(Service Node):
- 执行一些不改变行为树状态的服务性任务,如更新AI的感知信息等。
8. 黑板节点(Blackboard Node):
- 访问或修改全局或上下文中的数据,如玩家的位置、怪物的健康状况等。
9. 动态节点(Dynamic Node):
- 根据运行时的数据动态选择子节点执行,例如根据敌人的类型选择不同的攻击策略。
二、代码实战
代码讲解
本文是一个简单的初级行为树,只写了组合节点的 顺序节点 和 选择节点
通过上文,我们知道了,顺序节点,当得到节点返回状态的 是 SUCCESS 则继续执行其它节点,否原路返回,选择节点,当有一个节点返回的状态是 SUCCESS 就直接执行该节点
看到状态,首先,解释一下这个状态,和FSM,HFSM这些的状态有什么不一样的,状态机中的状态,是玩家当前所处的行为的抽象,例如吃,跑,玩,睡等等,是行为的抽象集合,通过条件进行切换状态;
而行为树的状态则是行为结束后动作节点的运行状态,也就是在一个动作节点中,运行完所有的行为逻辑,此时返回一个当前动作节点的状态,是RUNNING,SUCCESS还是FAILURE,行为树的节点(例如组合节点的顺序和选择)通过当前动作节点返回的状态,来判断是否执行对应的动作分支,也就是说,行为树一直在遍历它的所有子节点,通过动作节点的返回状态来判断是否继续执行该动作分支,SUCCESS就继续执行遍历,RUNNING则一直执行该动作节点,FAILURE则返回不再执行
举个例子,例如,玩家根节点为选择节点,根节点的子节点为顺序节点和巡逻动作,顺序节点的子节点为向玩家位置移动并检测玩家是否是否位于攻击位置,攻击玩家
巡逻节点需要一直进行巡逻,所以为RUNNING,检测玩家的顺序节点,检测到玩家返回SUCCESS,否则返回FAILURE,如果检测到玩家,返回了SUCCESS,那么顺序节点顺序执行其它子节点--向玩家位置移动并且检测玩家受否可以攻击,可以攻击返回SUCCESS,继续执行其它子节点--攻击
1.节点和节点状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 节点状态
/// </summary>
public enum NodeState
{
RUNNING,
SUCCESS,
FAILURE
}
/// <summary>
/// 节点类
/// 创建节点数据
/// </summary>
public class Node
{
// 节点状态
protected NodeState state;
// 公共父节点
public Node parent;
// 子节点
protected List<Node> children = new List<Node>();
// 字典,存放数据 名字+数据
private Dictionary<string, object> dataContext = new Dictionary<string, object>();
// 无参构造函数, 将父节点设置为空
public Node()
{
parent = null;
}
// 有参构造函数,传入子节点
public Node(List<Node> children)
{
foreach (Node child in children)
{
Attach(child);
}
}
//
private void Attach(Node node)
{
node.parent = this;
children.Add(node);
}
//
public virtual NodeState Evaluate() => NodeState.FAILURE;
//
public void SetData(string key, object value)
{
dataContext[key] = value;
}
//
public object GetData(string key)
{
object value = null;
if(dataContext.TryGetValue(key, out value))
return value;
Node node = parent;
while (node != null)
{
value = node.GetData(key);
if (value != null)
return value;
node = node.parent;
}
return null;
}
//
public bool ClearData(string key)
{
if (dataContext.ContainsKey(key))
{
dataContext.Remove(key);
return true;
}
Node node = parent;
while (node != null)
{
bool cleared = node.ClearData(key);
if (cleared)
return true;
node = node.parent;
}
return false;
}
}
2.顺序节点
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 顺序节点
/// </summary>
public class Sequence : Node
{
public Sequence() : base(){}
public Sequence(List<Node> children) : base(children){}
public override NodeState Evaluate()
{
bool anyChildIsRunning = false;
// 遍历所有子节点
foreach (Node node in children)
{
// 将子节点的状态作为参数传递
switch (node.Evaluate())
{
// 如果节点状态执行为失败,那么将该节点状态作为结果返回给父类
case NodeState.FAILURE:
state = NodeState.FAILURE;
return state;
// 如果节点状态执行为成功,那么跳过该次循环,继续执行
case NodeState.SUCCESS:
continue;
// 执行中
case NodeState.RUNNING:
anyChildIsRunning = true;
continue;
default:
state = NodeState.SUCCESS;
return state;
}
}
// 默认返回为 执行中 或者是 成功
state = anyChildIsRunning ? NodeState.RUNNING : NodeState.SUCCESS;
// 返回节点状态
return state;
}
}
3.选择节点
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 选择节点
/// </summary>
public class Selector : Node
{
public Selector() : base(){}
public Selector(List<Node> children) : base(children){}
public override NodeState Evaluate()
{
foreach (Node node in children)
{
switch (node.Evaluate())
{
// 节点执行失败 跳过
case NodeState.FAILURE:
continue;
// 节点执行成功 返回当前
case NodeState.SUCCESS:
return state;
// 节点正在执行中 设置状态为 执行中 返回执行中
case NodeState.RUNNING:
state = NodeState.RUNNING;
return state;
default:
continue;
}
}
// 默认为执行失败
state = NodeState.FAILURE;
return state;
}
}
4.树,用来构建行为树
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 树,构建行为树
/// </summary>
public abstract class Tree : MonoBehaviour
{
private Node root = null;
protected void Start()
{
root = SetupTree();
}
private void Update()
{
if (root != null)
root.Evaluate();
}
protected abstract Node SetupTree();
}
5.应用
1.巡逻任务
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
public class TaskPatrol : Node
{
//
private Animator _animator; // 动画组件
private Transform[] _transforms; // 所有巡逻点
private Transform _transform; // 组件
//
private int _currentWaypointIndex = 0; // 巡逻点
// 巡逻计时
private bool _isWaiting; // 是否等待
private float _waitCounter; // 等待计时
private float _waitingTime = 1f; // 等待时间
// 构造函数
public TaskPatrol(Transform[] transforms, Transform transform)
{
_transforms = transforms;
_transform = transform;
_animator = transform.GetComponent<Animator>();
}
//
public override NodeState Evaluate()
{
//节点执行的行为逻辑操作
if (_isWaiting)
{
_waitCounter += Time.deltaTime;
if (_waitCounter > _waitingTime)
{
_isWaiting = false;
_animator.Play("Walk");
}
}
else
{
Transform nextPatrolPoint = _transforms[_currentWaypointIndex];
// 当和目标位置距离小于0.1f时,更新位置,并且进入等待计时
if (Vector3.Distance(_transform.position, nextPatrolPoint.position) < 0.1f)
{
_transform.position = nextPatrolPoint.position;
_waitCounter = 0f;
_isWaiting = true;
// 更新巡逻目标位置
_currentWaypointIndex = (_currentWaypointIndex + 1) % _transforms.Length;
// 播放Idle动画
_animator.Play("Idle_Battle");
}
else
{
_transform.position = Vector3.MoveTowards(_transform.position, nextPatrolPoint.position, PlayerTree.playerSpeed * Time.deltaTime);
_transform.LookAt(nextPatrolPoint.position);
}
}
// 返回状态,running,执行完继续执行
state = NodeState.RUNNING;
return state;
}
}
2.构建行为树 应用
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerTree : Tree
{
//
public Transform[] _Transforms;
//
public static float playerSpeed = 5f;
//
protected override Node SetupTree()
{
Node root = new Sequence(new List<Node>
{
new TaskPatrol(_Transforms, transform)
});
return root;
}
}