本文大致介绍了行为树是什么,适合用于复习行为树的概念,或对行为树做一个大致了解。
什么是行为树
行为树是一种用于控制游戏AI或机器人逻辑的树形结构,它按照层级组织决策和行为,从根节点向下遍历执行。
为什么需要行为树?
状态机在处理复杂AI时会出现状态爆炸——10个行为可能需要20+个状态转换。行为树通过节点组合解决这个问题,让你像搭积木一样构建AI逻辑,无需硬编码状态转换。
一般最简单的行为树可能会这样调用:
void Start()
{
behaviorTree = new BehaviorTree();
}
// 每帧调用
void Update()
{
behaviorTree.Execute();
}
行为树的核心组成
行为树由三种核心节点类型构成:
- 条件节点(Condition) 用于检查游戏状态是否满足某个条件,例如“敌人是否可见”或“生命值是否低于30%”。
- 动作节点(Action) 用于执行具体的行为,例如“移动到目标位置”或“播放攻击动画”。
- 组合节点(Composite) 用于组合多个子节点,控制它们的执行顺序和逻辑关系。
节点状态枚举
在开始代码之前,我们先定义节点的三种执行状态。
public enum NodeStatus
{
Running, // 节点正在执行中,需要在下一帧继续运行
Success, // 节点执行成功,返回正值结果
Failure // 节点执行失败,返回负值结果
}
基础节点类
所有节点都继承自抽象的Node类,它定义了行为树的基本框架。每个节点都有一个名称用于调试,每个节点都可以有子节点,并且实现了Tick方法来执行逻辑。
// 行为树节点基类
public abstract class Node
{
// 节点名称,便于调试时识别
protected string name;
// 子节点列表,用于组合节点管理多个子节点
protected List<Node> children = new List<Node>();
// 构造函数,初始化节点名称
public Node(string name)
{
this.name = name;
}
// 添加子节点的方法
public void AddChild(Node child)
{
children.Add(child);
}
// 抽象方法,由子类实现具体的执行逻辑
public abstract NodeStatus Tick();
}
条件节点
所有节点都继承自抽象的Node类,它定义了行为树的基本框架。每个节点都有一个名称用于调试,每个节点都可以有子节点,并且实现了Tick方法来执行逻辑。
// 条件节点:用于检查游戏状态是否满足某个条件
public class ConditionNode : Node
{
// 条件检查委托,返回true表示条件满足
private Func<bool> condition;
public ConditionNode(string name, Func<bool> condition) : base(name)
{
this.condition = condition;
}
// 执行条件检查
public override NodeStatus Tick()
{
// 如果条件满足,返回成功;否则返回失败
return condition() ? NodeStatus.Success : NodeStatus.Failure;
}
}
组合节点详解
选择器
选择器从左到右依次尝试执行子节点,直到某个子节点返回成功为止。用于表示“一系列选项中尝试执行一个”的逻辑,例如尝试攻击敌人,如果攻击不到则尝试逃跑。
// 选择器节点:从左到右尝试执行子节点,找到第一个成功的就停止
public class Selector : Node
{
public Selector(string name) : base(name) { }
public override NodeStatus Tick()
{
// 遍历所有子节点
foreach (var child in children)
{
// 执行子节点并获取结果
NodeStatus status = child.Tick();
// 如果子节点成功,立即返回成功
if (status == NodeStatus.Success)
{
return NodeStatus.Success;
}
// 如果子节点正在运行,也立即返回(保持运行状态)
if (status == NodeStatus.Running)
{
return NodeStatus.Running;
}
// 如果子节点失败,继续尝试下一个子节点
}
// 所有子节点都失败,返回失败
return NodeStatus.Failure;
}
}
序列
序列从左到右依次执行所有子节点,只有当所有子节点都成功时才返回成功。用于表示“必须按顺序完成所有步骤”的逻辑,例如先检查敌人距离,再转向敌人,最后执行攻击。
// 序列节点:从左到右执行所有子节点,所有成功才返回成功
public class Sequence : Node
{
public Sequence(string name) : base(name) { }
public override NodeStatus Tick()
{
// 假设整体成功
bool anyRunning = false;
// 遍历所有子节点
foreach (var child in children)
{
// 执行子节点并获取结果
NodeStatus status = child.Tick();
// 如果子节点失败,立即返回失败(序列中断)
if (status == NodeStatus.Failure)
{
return NodeStatus.Failure;
}
// 如果子节点正在运行,记录状态并继续
if (status == NodeStatus.Running)
{
anyRunning = true;
}
}
// 如果有子节点正在运行,返回Running;否则返回成功
return anyRunning ? NodeStatus.Running : NodeStatus.Success;
}
}
随机选择器
随机选择器在执行前会打乱子节点的顺序,然后像普通选择器一样从打乱后的第一个节点开始尝试。例如,士兵执行“休息”逻辑时,它可以随机选择“散步”或“睡觉”。
// 随机选择器:打乱子节点顺序后执行选择器逻辑
public class RandomSelector : Node
{
public RandomSelector(string name) : base(name) { }
public override NodeStatus Tick()
{
// 打乱顺序
var shuffledChildren = RandomShuffle(children);
// 按打乱后的顺序执行选择器逻辑
foreach (var child in shuffledChildren)
{
NodeStatus status = child.Tick();
if (status == NodeStatus.Success)
{
return NodeStatus.Success;
}
if (status == NodeStatus.Running)
{
return NodeStatus.Running;
}
}
return NodeStatus.Failure;
}
}
你还可以实现一个“加权随机选择器”。
随机序列
同随机选择器。
// 随机序列:打乱子节点顺序后执行序列逻辑
public class RandomSequence : Node
{
private Random random = new Random();
public RandomSequence(string name) : base(name) { }
public override NodeStatus Tick()
{
// 打乱顺序
var shuffledChildren = RandomShuffle(children);
// 按打乱后的顺序执行序列逻辑
bool anyRunning = false;
foreach (var child in shuffledChildren)
{
NodeStatus status = child.Tick();
if (status == NodeStatus.Failure)
{
return NodeStatus.Failure;
}
if (status == NodeStatus.Running)
{
anyRunning = true;
}
}
return anyRunning ? NodeStatus.Running : NodeStatus.Success;
}
}
你还可以实现一个“加权随机序列”。
装饰器节点
它有一个子节点,用于修改或增强其子节点的行为。常见的装饰器包括反转结果、重复执行、条件判断等。
// 装饰器基类:用于修改子节点行为的包装节点
public abstract class Decorator : Node
{
// 装饰器只能有一个子节点
protected Node child;
public Decorator(string name, Node child) : base(name)
{
this.child = child;
}
}
// 反转装饰器:将子节点的结果取反
public class Inverter : Decorator
{
public Inverter(string name, Node child) : base(name, child) { }
public override NodeStatus Tick()
{
// 执行子节点
NodeStatus status = child.Tick();
// 反转结果:成功变失败,失败变成功
if (status == NodeStatus.Success)
{
return NodeStatus.Failure;
}
else if (status == NodeStatus.Failure)
{
return NodeStatus.Success;
}
// Running状态保持不变
return status;
}
}
// 重复装饰器:持续执行子节点直到成功或达到次数上限
public class Repeater : Decorator
{
private int repeatCount; // 需要重复的次数
private int currentCount = 0; // 当前已重复的次数
public Repeater(string name, Node child, int repeatCount) : base(name, child)
{
this.repeatCount = repeatCount;
}
public override NodeStatus Tick()
{
// 持续执行子节点
NodeStatus status = child.Tick();
// 如果子节点失败,重置计数并返回失败
if (status == NodeStatus.Failure)
{
currentCount = 0;
return NodeStatus.Failure;
}
// 如果子节点成功,增加计数
if (status == NodeStatus.Success)
{
currentCount++;
// 达到重复次数,返回成功
if (currentCount >= repeatCount)
{
currentCount = 0;
return NodeStatus.Success;
}
}
// 继续返回Running状态
return NodeStatus.Running;
}
}
黑板系统
黑板是行为树中的共享数据存储区,用于在不同节点之间传递和共享数据。一个通用的黑板如下:
public class Blackboard
{
// 使用字典存储键值对
private Dictionary<string, object> data = new Dictionary<string, object>();
// 设置值
public void SetValue(string key, object value)
{
data[key] = value;
}
// 获取值
public T GetValue<T>(string key)
{
if (data.ContainsKey(key))
{
return (T)data[key];
}
// 如果键不存在,返回默认值
return default(T);
}
}
但显然,这会导致拆箱装箱,有性能问题。所以我一般会为每个游戏写一个黑板类,里面定义你需要的字段。
public class Blackboard
{
/// <summary>
/// 决策的目标单位(攻击目标)
/// </summary>
public GameObject TargetEnemy { get; set; }
/// <summary>
/// 决策的移动方向
/// </summary>
public Vector3 Direction { get; set; }
……
}

9593

被折叠的 条评论
为什么被折叠?



