一.行为树是什么
行为树(Behavior Tree)是一种用于建模和实现复杂行为的结构,广泛应用于游戏开发、机器人控制和人工智能等领域。我们在游戏开发时有时会利用行为树来设计具有复杂决策的智能AI,因为它提供了一种清晰、可扩展的方式来组织和管理角色或系统的行为。
二.行为树的优点
1. 可复用性
行为树的节点可以被复用,这意味着同一个行为可以在不同的上下文中使用。这种复用性使得行为树在复杂系统中的管理和扩展变得更加高效。
2. 结构清晰
行为树通常可以通过图形化工具(或者直接梳理决策脉络得到结构图)进行可视化,便于开发者和设计师理解、修改和调试角色的行为。其清晰的结构不会造成像状态机中某些“交际花“状态,让你眼花撩乱的情况。
3. 易于扩展
由于行为树的层次结构和模块化设计,当需要添加新行为或修改现有行为时,开发者可以轻松地插入或替换节点,而无需重构整个系统。
4. 状态管理
行为树能够有效地管理同一状态下的决策切换,也可以允许系统在不同的之间切换。例如,角色可以在“巡逻”、“追逐”和“攻击”状态之间切换,而每个状态都有其特定的行为树。
三.行为树的原理
1. 结构
行为树由节点组成,节点主要分为以下几类:
叶子节点(Action Nodes):每片叶子记录一种策略,执行具体的行为,如移动、攻击、播放动画等行为逻辑。
条件节点(Condition Nodes):检查某些条件是否满足,以决定是否执行某个行为。
控制节点(Control Nodes):负责控制行为的执行流程,主要包括
并行节点(Parallel Node):同时执行多个子节点,根据设定的条件决定成功或失败。
顺序节点(Sequence Node):按顺序执行子节点,直到一个子节点失败
选择节点(Selector Node):按顺序执行子节点,直到一个子节点成功
2. 执行流程
#行为树的执行是自上而下(可以理解从根部出发,延至尽头的一片叶子上,这一条通路就是一种决策)的。控制节点会根据其子节点的状态(成功Success、失败Faliure、运行中Running)来决定接下来的执行步骤。行为树的执行允许动态调整行为。
#当你的节点内存在优先级时,会有打断关系,指当AI行为发生变化,行为树的高优先级节点会打断低优先级节点的执行。
(注:这里的打断关系包括:打断自身,打断低优先级,打断自身及低优先级,不打断)
行为树的执行通常遵循以下步骤:
-
启动执行:从根节点开始执行,根节点会根据其类型决定如何处理其子节点。
-
节点评估:每个节点在执行时会被评估其状态,通常有三种状态:
- 成功(Success):节点成功完成其任务。
- 失败(Failure):节点未能完成其任务。
- 运行中(Running):节点正在进行中,尚未完成。
-
状态传播:控制节点会根据其子节点的状态来决定自身的状态。例如:
- 在顺序节点中,如果一个子节点失败,顺序节点立即返回失败;如果所有子节点成功,顺序节点返回成功。
- 在选择节点中,如果一个子节点成功,选择节点返回成功;如果所有子节点失败,选择节点返回失败。
-
状态更新:行为树的状态可以在每一帧更新,允许角色的行为根据环境变化动态调整。
四.尝试实现
1.思路梳理:
我们先设计一下各个组成部分的功能 以便 推出可能需要的字段或方法
0.Status(节点状态的枚举):
用来描述节点的执行状态进程
enum Status { Running,Success,Faliure }
1.Node(节点基类):
1.记录本节点的名称 => string name
2.记录自身的所有子节点 => list<Node> children
3.记录执行中当前节点的序号 => int currentChild
4.记录本节点的优先级 => int priority
5.描述当前节点自身的状态 => Status status
6.添加子节点的方法 => void AddChild(Node child)
7. 获取当前状态更新的方法 => Status Process()
8.回溯方法 (当前的策略被打断时,重新开启对子节点的遍历) => void Reset()
(可选,主要是考虑后面控制节点里的选择节点(Selector)的功能优化)
2.ConditionNode (条件节点)
1.具有一个判断方法用于进入节点的逻辑
2.具有打断当前序列节点执行的逻辑
3.ControlNode (控制节点)
1.Sequence Node:
按顺序执行子节点,直到一个子节点失败
2.Selector Node:
按顺序执行子节点,直到一个子节点成功
* Parallel Node:同时执行多个子节点,(在unity里用的较少,本人不是很熟悉,先不过多介绍)
4.LeafNode(叶节点)
1.记录本叶节点的策略 ,我们这里选择让这些节点继承一个接口 IStrategy
(我们就是通过这个接口变量来为叶节点分配实现 状态更新方法)
2.代码实现:
0.Status(节点状态枚举)
public enum Status
{
Running, //节点运行中
Success, //节点运行成功
Failure, //节点运行失败
}
1.Node(节点基类)
public class Node
{
public Status status; //节点当前状态
public List<Node> children=new List<Node>();//子节点列表
public int priority; // 优先级
public int currentChild; //当前执行的节点序号
public string name; //节点名称
//使用构造函数进行初始化
public Node(string name,int priority=0)
{
this.name = name;
this.priority = priority;
}
//添加子节点
public virtual void AddChild(Node child) => children.Add(child);
//状态更新
public virtual Status Process() => children[currentChild].Process();
//重置回溯
public virtual void Reset()
{
currentChild = 0; //返回初始节点重新执行
children.ForEach(child => child.Reset());
}
}
2.BehaviourTree(行为树脚本)
可以理解是一个异化的Sequence,当前子节点必须Succes才能进入下一子节点
public class BehaviourTree : Node
{
public BehaviourTree(string name) : base(name) { }
public override Status Process()
{
while (currentChild < children.Count)
{
var status = children[currentChild].Process();
if (status != Status.Success)
{
return status;
}
//当前节点成功,再进入下一节点
currentChild++;
}
return Status.Success;
}
}
3.Condition(条件节点)
首先实现策略接口 IStrategy
public interface IStrategy
{
Status Process();
void Reset() { }
}
一般条件节点作为一种修饰节点,是只有一个子节点的,所以我们不妨直接不继承Node,直接来做内部的判断逻辑,Condition 节点实现代码如下
public class Condition : IStrategy
{
readonly Func<bool> predicate; //判断逻辑
public Condition(Func<bool> predicate)
{
this.predicate = predicate;
}
//满足条件才能进入下面的节点,否则就打断
public Status Process() => predicate() ? Status.Success : Status.Failure; /
}
4.ControlNode(控制节点)
1.Sequence Node(序列节点):
public class SequenceNode : Node
{
public SequenceNode(string name, int priority = 0) : base(name, priority) { }
public override Status Process()
{
//遍历子节点
if (currentChild < children.Count)
{
switch (children[currentChild].Process())
{
case Status.Running:
return Status.Running;
case Status.Failure:
Reset(); //如果找到有运行失败的子节点,会直接打断Sequence节点的执行
return Status.Failure;
default:
currentChild++;
return currentChild == children.Count ? Status.Success : Status.Running;
}
}
Reset();
return Status.Success;
}
}
2.Selector Node(选择节点):
public class SelectorNode : Node
{
public SelectorNode(string name) : base(name) { }
public override Status Process()
{
if (currentChild < children.Count)
{
switch (children[currentChild].Process())
{
case Status.Running:
return Status.Running;
case Status.Success:
Reset(); //如果发现有运行成功的节点,会打断Selector执行并进入该子节点
return Status.Success;
default:
currentChild++;
return Status.Running;
}
}
//如果没有找到运行成功的子节点,返回失败并重置重新遍历子节点
Reset();
return Status.Failure;
}
}
3.PrioritySelector Node(优先级选择节点):
(优化Selector,依据各个子节点的优先级进行选择)
public class PrioritySelectorNode:Node
{
List<Node> sortedChildren;
public PrioritySelectorNode(string name, int priority = 0) : base(name, priority) { }
List<Node> SortedChildren => sortedChildren ??= SortCildren();
// ??=:检查左侧的变量是否为null,如果是则将右侧的值赋给左侧的变量
protected virtual List<Node> SortCildren()=>children.OrderByDescending(child=>child.priority).ToList();
//OrderByDescending(child=>child.priority)表示依据数据child的priority字段来进行升序排列,并返回该升序列表
public override void Reset()
{
base.Reset();
sortedChildren = null;
}
public override Status Process()
{
foreach (var child in SortedChildren)
{
switch (child.Process())
{
case Status.Running:
return Status.Running;
case Status.Success:
return Status.Success;
default:
continue;
}
}
Reset() ;
return Status.Failure;
}
}
5.LeafNode(叶节点)
public class LeafNode : Node,IStrategy
{
readonly IStrategy strategy; //自身的策略
public LeafNode(string name, IStrategy strategy, int priority = 0) : base(name, priority)
{
this.strategy = strategy;
}
public override Status Process()=>strategy.Process();
public override void Reset()=>strategy.Reset();
}
实际运用
既然我们已经实现了基本的节点,接下来我们就尝试应用我们的行为树来实现一种简单的AI行为吧!
1.实验目标:
创造一个测试AI的控制器脚本Enemy,实现当在场景中有激活的Player时,打印目标信息,当有多个Player目标,则选择优先级更高的Player,打印目标信息
2.代码实现:
public class Enemy : MonoBehaviour
{
BehaviourTree tree;
public GameObject Player1;
public GameObject Player2;
private void Awake()
{
tree = new BehaviourTree("Enemy");
Sequence goToPlayer1 = new Sequence("ToPlayer1",2);
goToPlayer1.AddChild(new Leaf("PreasentPlayer1", new Condition(() => Player1.activeSelf)));
goToPlayer1.AddChild(new Leaf("MoveToPlayer1", new ActionStrategy(() => { Debug.Log("我发现了玩家1,我打!"); })));
Sequence goToPlayer2 = new Sequence("ToPlayer2",1);
goToPlayer2.AddChild(new Leaf("PreasentPlayer2", new Condition(() => Player2.activeSelf)));
goToPlayer2.AddChild(new Leaf("MoveToPlayer2", new ActionStrategy(() => { Debug.Log("我发现了玩家2,我打!!"); })));
PrioritySelector playerSelector = new PrioritySelector("playerSelector");//子节点node的priority越大优先级越高
playerSelector.AddChild(goToPlayer1);
playerSelector.AddChild(goToPlayer2);
tree.AddChild(playerSelector);
}
private void Update()
{
tree.Process();
}
}
这里的ActionStrategy只是举例的一种示例策略,在其他应用场景你可以创建自定义的策略(比如某一状态的不同策略,以巡逻为例,patrol状态下就可能有idle巡视和walk巡视两种不同的策略)
public class ActionStrategy:I_Strategy
{
readonly Action doSomthing; //接收含具体逻辑的方法
public ActionStrategy(Action doSomthing)
{
this.doSomthing = doSomthing;
}
public Node.Status Process()
{
doSomthing();
return Node.Status.Success;
}
}
在编辑器内新建空物体添加测试脚本,并创建两个游戏物体分别代表Player1和Player2
3.测试结果:
1.当开始运行时 Player1和Player2均失活,无打印信息,
将Player2 激活,打印“我发现了玩家2,我打!!”
2.当开始运行时 Player1和Player2均失活,无打印信息,
将Player1激活,打印“我发现了玩家1,我打!!”
3.当开始运行时 Player1和Player2均激活,则会选择优先级更高的玩家一节点,打印“我发现了玩家1,我打!!”
可以见到实验目标基本实现!
注:如果你想实现在几个节点间循环切换的效果,请在对应的策略中实现打断逻辑,使达成某些条件的情况下从叶节点返回并进入新的叶节点。后面我还会写一篇具体实现行为树回溯节点的应用。
尾声
这几天正巧国庆假期 (^~^),但学习动力还是得有!几天后还要忙一阵子准备比赛了,希望自己能有成长吧,大家也要加油成长哦,诸君共勉!
本篇丸~