第7章 智能体的灵魂:行为树架构在复杂游戏AI中的落地实践
本文摘要探讨了游戏AI开发从状态机到行为树的演进过程。早期状态机在复杂度提升时面临逻辑混乱、难以维护的问题,而行为树通过树状节点结构实现了逻辑解耦和可视化编辑。文章详细介绍了行为树的三种节点类型(组合节点、装饰节点、条件/动作节点)及其执行机制,重点分析了行为树在商业项目中的核心价值——让策划能够直接参与AI设计。随后展示了轻量级行为树框架的具体实现,包括节点基类设计、组合节点逻辑等代码示例,为开
第7章 智能体的灵魂:行为树架构在复杂游戏AI中的落地实践
在早期的游戏开发中,NPC的行为逻辑大多靠状态机硬编码实现。一个怪物有"巡逻"、“追逐”、"攻击"三个状态,通过条件判断在这些状态间跳转。对于三五状态的简单AI,状态机足够清晰。但当NPC的行为复杂度提升到十几个状态,状态之间的转换关系就变成了蜘蛛网——添加一个新状态,必须检查所有可能跳转到它的前置条件,修改一处往往会引发连锁反应。
更致命的是,状态机的逻辑分散在各个Update函数和条件判断里,策划想要调整行为逻辑,必须由程序员修改代码。每周版本迭代时,AI部分的bug数量总是最高。到了后期,代码里充斥着各种临时加的flag,什么"已经攻击过一次了"、“正在等待返回巡逻点”,整个AI模块就像一堆乱麻,没人敢动。
行为树的出现彻底改变了这种局面。它将AI的决策逻辑从代码中抽离,用树状的节点组合来描述行为。每个节点只负责一件小事,比如"判断是否发现玩家"、“移动到某个位置”、“播放攻击动画”。通过组合这些节点,可以构建出极其复杂的行为模式,而且整个逻辑结构一目了然。更重要的是,行为树的执行机制决定了它天然具备可扩展性——添加新行为就像往树上挂一个新节点,不需要修改任何现有代码。
本章将从零开始手写一个轻量级行为树框架,再用这个框架实现一个具备真实感的NPC行为。然后结合商业项目案例,讨论行为树在大型游戏中的高级应用和优化策略。
7.1 从状态机到行为树:思维模式的转变
7.1.1 状态机的困境:复杂的转移关系
先看一个典型的游戏AI场景:一个守卫NPC,它需要做这些事情:
- 平时在自己的路线上来回巡逻
- 听到可疑声音时,会前往声音来源处调查
- 看到玩家时,先警告,警告无效则攻击
- 攻击时根据距离选择近战或远程
- 血量低到一定程度会逃跑
- 逃跑途中如果脱离战斗,会返回巡逻点
- 如果发现队友被攻击,会前往支援
用状态机实现这个逻辑,状态数量大约在8-10个,状态之间的转移条件更是多达30-40条。每个条件都要考虑当前状态、目标状态、触发时机。更麻烦的是,有些行为需要记忆中间信息,比如"调查声音"状态需要记住声音来源的位置,调查完毕后要返回巡逻中断的位置继续巡逻。这些临时数据必须在各个状态间传递,一不小心就会产生状态残留。
行为树解决这个问题的方式完全不同。它将决策过程看作一棵树,从根节点开始,每一层根据条件选择不同的分支,最终到达某个行为节点。条件判断和行为执行分离,行为节点之间不直接通信,所有共享数据存放在一个叫做"黑板"的地方。
7.1.2 行为树的核心概念
一个行为树由三种类型的节点构成:
组合节点(Composite):控制子节点的执行逻辑,可以有多个子节点。最常见的组合节点包括:
- 顺序节点(Sequence):从左到右依次执行子节点,如果某个子节点返回失败,则整个顺序节点失败;只有所有子节点都成功,顺序节点才成功。
- 选择节点(Selector):从左到右依次执行子节点,如果某个子节点返回成功,则整个选择节点成功;只有所有子节点都失败,选择节点才失败。
- 并行节点(Parallel):同时执行所有子节点,根据预设策略判断成功或失败。
装饰节点(Decorator):只有一个子节点,用于修改子节点的执行行为。比如重复执行一定次数、取反子节点的返回结果、设置超时时间等。
条件节点(Condition):判断某个条件是否成立,不改变游戏状态,只返回成功或失败。
动作节点(Action):执行具体的游戏逻辑,比如移动、攻击、播放动画,返回运行中、成功或失败。
执行过程中,每个节点都会返回一个状态值:Success(成功)、Failure(失败)、Running(运行中)。Running状态是关键所在——如果一个动作需要多帧完成(比如移动到某个位置),它会在未完成时返回Running,下一帧从上次中断的地方继续执行。这让行为树能够自然地处理耗时操作。
7.1.3 为什么行为树更适合商业项目
除了结构清晰、易于扩展这些优点,行为树在商业项目中还有一个无法替代的价值:让策划能够直接参与AI逻辑的设计。
有了行为树,策划可以通过可视化工具拖拽节点、配置参数,而不需要每次改逻辑都找程序员。一个设计良好的行为树框架,可以让策划在一个工具界面里完成"如果玩家等级大于10,并且怪物血量低于30%,那么触发狂暴状态"这样的复杂逻辑组合。程序员只需要实现底层的原子动作和条件,上层的逻辑组合完全交给策划。
这种分工模式的改变,释放了程序员的产能,也让AI行为变得更加丰富多变。下面我们就动手实现这样一个框架。
7.2 手写轻量级行为树框架
7.2.1 节点基类与状态枚举
首先定义节点的执行状态和基类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 行为树节点的执行状态
/// </summary>
public enum BTStatus
{
Invalid, // 无效状态,初始值
Success, // 执行成功
Failure, // 执行失败
Running, // 正在运行
Aborted // 被中断
}
/// <summary>
/// 行为树节点基类
/// </summary>
public abstract class BTNode
{
protected BTStatus status = BTStatus.Invalid;
// 节点名称,用于调试
public string NodeName { get; set; }
// 黑板引用,用于数据共享
protected Blackboard blackboard;
// 当前运行状态
public BTStatus Status => status;
// 是否已终止(成功、失败或中断)
public bool IsTerminated => status == BTStatus.Success ||
status == BTStatus.Failure ||
status == BTStatus.Aborted;
// 是否正在运行
public bool IsRunning => status == BTStatus.Running;
// 是否成功
public bool IsSuccess => status == BTStatus.Success;
// 是否失败
public bool IsFailure => status == BTStatus.Failure;
public BTNode(string nodeName = "")
{
NodeName = string.IsNullOrEmpty(nodeName) ? GetType().Name : nodeName;
}
/// <summary>
/// 设置黑板
/// </summary>
public virtual void SetBlackboard(Blackboard bb)
{
blackboard = bb;
}
/// <summary>
/// 节点进入时调用
/// </summary>
protected virtual void OnEnter()
{
// 子类可重写
}
/// <summary>
/// 节点更新逻辑,子类必须实现
/// </summary>
protected abstract BTStatus OnUpdate();
/// <summary>
/// 节点退出时调用
/// </summary>
protected virtual void OnExit()
{
// 子类可重写
}
/// <summary>
/// 节点中断时调用
/// </summary>
protected virtual void OnAbort()
{
// 子类可重写
}
/// <summary>
/// 执行节点Tick
/// </summary>
public BTStatus Tick()
{
if (status != BTStatus.Running)
{
// 第一次进入或重新进入
OnEnter();
}
// 执行更新逻辑
status = OnUpdate();
// 如果执行完毕(成功或失败),调用退出
if (status != BTStatus.Running)
{
OnExit();
}
return status;
}
/// <summary>
/// 中断节点执行
/// </summary>
public void Abort()
{
if (status == BTStatus.Running)
{
OnAbort();
status = BTStatus.Aborted;
}
}
/// <summary>
/// 重置节点状态
/// </summary>
public virtual void Reset()
{
status = BTStatus.Invalid;
}
/// <summary>
/// 添加子节点(组合节点重写)
/// </summary>
public virtual void AddChild(BTNode child)
{
Debug.LogError($"节点 {NodeName} 不支持添加子节点");
}
/// <summary>
/// 获取子节点列表(组合节点重写)
/// </summary>
public virtual List<BTNode> GetChildren()
{
return null;
}
}
7.2.2 组合节点实现
组合节点是行为树的骨架,负责控制子节点的执行流程。先实现顺序节点和选择节点:
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 顺序节点:依次执行子节点,全部成功才算成功,任一失败即失败
/// </summary>
public class SequenceNode : BTNode
{
protected List<BTNode> children = new List<BTNode>();
protected int currentChildIndex = 0;
public SequenceNode(string nodeName = "Sequence") : base(nodeName)
{
}
public override void AddChild(BTNode child)
{
child.SetBlackboard(blackboard);
children.Add(child);
}
public override List<BTNode> GetChildren()
{
return children;
}
public override void SetBlackboard(Blackboard bb)
{
base.SetBlackboard(bb);
foreach (var child in children)
{
child.SetBlackboard(bb);
}
}
protected override void OnEnter()
{
currentChildIndex = 0;
}
protected override BTStatus OnUpdate()
{
while (currentChildIndex < children.Count)
{
BTNode currentNode = children[currentChildIndex];
BTStatus childStatus = currentNode.Tick();
// 子节点还在运行中,顺序节点也返回运行中
if (childStatus == BTStatus.Running)
{
return BTStatus.Running;
}
// 子节点执行失败,顺序节点立即失败
if (childStatus == BTStatus.Failure)
{
return BTStatus.Failure;
}
// 子节点执行成功,继续下一个
if (childStatus == BTStatus.Success)
{
currentChildIndex++;
}
}
// 所有子节点都执行成功
return BTStatus.Success;
}
protected override void OnAbort()
{
// 中断当前正在运行的子节点
if (currentChildIndex < children.Count)
{
children[currentChildIndex].Abort();
}
}
public override void Reset()
{
base.Reset();
foreach (var child in children)
{
child.Reset();
}
currentChildIndex = 0;
}
}
/// <summary>
/// 选择节点:依次执行子节点,任一成功即成功,全部失败才失败
/// </summary>
public class SelectorNode : BTNode
{
protected List<BTNode> children = new List<BTNode>();
protected int currentChildIndex = 0;
public SelectorNode(string nodeName = "Selector") : base(nodeName)
{
}
public override void AddChild(BTNode child)
{
child.SetBlackboard(blackboard);
children.Add(child);
}
public override List<BTNode> GetChildren()
{
return children;
}
public override void SetBlackboard(Blackboard bb)
{
base.SetBlackboard(bb);
foreach (var child in children)
{
child.SetBlackboard(bb);
}
}
protected override void OnEnter()
{
currentChildIndex = 0;
}
protected override BTStatus OnUpdate()
{
while (currentChildIndex < children.Count)
{
BTNode currentNode = children[currentChildIndex];
BTStatus childStatus = currentNode.Tick();
// 子节点正在运行
if (childStatus == BTStatus.Running)
{
return BTStatus.Running;
}
// 子节点执行成功,选择节点立即成功
if (childStatus == BTStatus.Success)
{
return BTStatus.Success;
}
// 子节点执行失败,尝试下一个
if (childStatus == BTStatus.Failure)
{
currentChildIndex++;
}
}
// 所有子节点都失败
return BTStatus.Failure;
}
protected override void OnAbort()
{
if (currentChildIndex < children.Count)
{
children[currentChildIndex].Abort();
}
}
public override void Reset()
{
base.Reset();
foreach (var child in children)
{
child.Reset();
}
currentChildIndex = 0;
}
}
并行节点稍微复杂一些,它同时执行所有子节点,并根据策略判断整体结果:
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 并行节点:同时执行所有子节点
/// </summary>
public class ParallelNode : BTNode
{
public enum Policy
{
RequireOne, // 任一子节点成功即成功
RequireAll // 所有子节点成功才成功
}
private List<BTNode> children = new List<BTNode>();
private Policy successPolicy;
private Policy failurePolicy;
public ParallelNode(Policy successPolicy = Policy.RequireAll,
Policy failurePolicy = Policy.RequireOne,
string nodeName = "Parallel") : base(nodeName)
{
this.successPolicy = successPolicy;
this.failurePolicy = failurePolicy;
}
public override void AddChild(BTNode child)
{
child.SetBlackboard(blackboard);
children.Add(child);
}
public override List<BTNode> GetChildren()
{
return children;
}
public override void SetBlackboard(Blackboard bb)
{
base.SetBlackboard(bb);
foreach (var child in children)
{
child.SetBlackboard(bb);
}
}
protected override BTStatus OnUpdate()
{
int successCount = 0;
int failureCount = 0;
bool anyRunning = false;
foreach (var child in children)
{
// 跳过已经终止的子节点(后续不再执行)
if (child.IsTerminated)
{
if (child.IsSuccess) successCount++;
if (child.IsFailure) failureCount++;
continue;
}
BTStatus childStatus = child.Tick();
if (childStatus == BTStatus.Running)
{
anyRunning = true;
}
else if (childStatus == BTStatus.Success)
{
successCount++;
}
else if (childStatus == BTStatus.Failure)
{
failureCount++;
}
}
// 检查成功条件
if (successPolicy == Policy.RequireAll)
{
if (successCount == children.Count)
{
return BTStatus.Success;
}
}
else // RequireOne
{
if (successCount > 0)
{
return BTStatus.Success;
}
}
// 检查失败条件
if (failurePolicy == Policy.RequireAll)
{
if (failureCount == children.Count)
{
return BTStatus.Failure;
}
}
else // RequireOne
{
if (failureCount > 0)
{
return BTStatus.Failure;
}
}
// 还有子节点在运行,继续等待
if (anyRunning)
{
return BTStatus.Running;
}
// 没有运行中的节点,也没有达到成功/失败条件
// 这种情况不应该发生,但以防万一
return BTStatus.Failure;
}
protected override void OnAbort()
{
foreach (var child in children)
{
if (child.IsRunning)
{
child.Abort();
}
}
}
public override void Reset()
{
base.Reset();
foreach (var child in children)
{
child.Reset();
}
}
}
7.2.3 装饰节点实现
装饰节点用于修饰单个子节点的行为,比如取反结果、重复执行等:
using UnityEngine;
/// <summary>
/// 装饰节点基类:只有一个子节点
/// </summary>
public abstract class DecoratorNode : BTNode
{
protected BTNode child;
public DecoratorNode(string nodeName = "Decorator") : base(nodeName)
{
}
public override void AddChild(BTNode child)
{
if (this.child != null)
{
Debug.LogError($"装饰节点 {NodeName} 只能有一个子节点");
return;
}
child.SetBlackboard(blackboard);
this.child = child;
}
public override List<BTNode> GetChildren()
{
return child != null ? new List<BTNode> { child } : new List<BTNode>();
}
public override void SetBlackboard(Blackboard bb)
{
base.SetBlackboard(bb);
child?.SetBlackboard(bb);
}
public override void Reset()
{
base.Reset();
child?.Reset();
}
}
/// <summary>
/// 取反节点:反转子节点的结果
/// </summary>
public class InverterNode : DecoratorNode
{
public InverterNode(string nodeName = "Inverter") : base(nodeName)
{
}
protected override BTStatus OnUpdate()
{
if (child == null)
{
return BTStatus.Failure;
}
BTStatus childStatus = child.Tick();
switch (childStatus)
{
case BTStatus.Success:
return BTStatus.Failure;
case BTStatus.Failure:
return BTStatus.Success;
case BTStatus.Running:
return BTStatus.Running;
default:
return childStatus;
}
}
}
/// <summary>
/// 重复节点:重复执行子节点指定次数,-1表示无限循环
/// </summary>
public class RepeatNode : DecoratorNode
{
private int repeatCount;
private int currentCount;
public RepeatNode(int count, string nodeName = "Repeat") : base(nodeName)
{
this.repeatCount = count;
this.currentCount = 0;
}
protected override void OnEnter()
{
currentCount = 0;
}
protected override BTStatus OnUpdate()
{
if (child == null)
{
return BTStatus.Failure;
}
while (repeatCount == -1 || currentCount < repeatCount)
{
BTStatus childStatus = child.Tick();
if (childStatus == BTStatus.Running)
{
return BTStatus.Running;
}
if (childStatus == BTStatus.Success || childStatus == BTStatus.Failure)
{
currentCount++;
// 如果达到次数,返回成功;否则重置子节点继续下一轮
if (repeatCount != -1 && currentCount >= repeatCount)
{
return BTStatus.Success;
}
child.Reset();
}
}
return BTStatus.Success;
}
}
7.2.4 动作节点与条件节点基类
动作节点执行具体的行为,条件节点判断条件是否成立。这两类节点通常由业务逻辑继承实现:
using UnityEngine;
/// <summary>
/// 动作节点基类
/// </summary>
public abstract class ActionNode : BTNode
{
public ActionNode(string nodeName = "Action") : base(nodeName)
{
}
}
/// <summary>
/// 条件节点基类
/// </summary>
public abstract class ConditionNode : BTNode
{
public ConditionNode(string nodeName = "Condition") : base(nodeName)
{
}
protected sealed override BTStatus OnUpdate()
{
return CheckCondition() ? BTStatus.Success : BTStatus.Failure;
}
/// <summary>
/// 检查条件是否成立
/// </summary>
protected abstract bool CheckCondition();
}
7.2.5 黑板系统:数据共享中心
行为树各节点之间需要共享数据,比如玩家的位置、NPC的目标点、当前状态等。黑板系统就是用来解决这个问题的:
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 黑板:行为树的数据共享中心
/// </summary>
public class Blackboard
{
private Dictionary<string, object> data = new Dictionary<string, object>();
/// <summary>
/// 设置值
/// </summary>
public void Set<T>(string key, T value)
{
data[key] = value;
}
/// <summary>
/// 获取值,若不存在返回默认值
/// </summary>
public T Get<T>(string key, T defaultValue = default(T))
{
if (data.TryGetValue(key, out object value))
{
return (T)value;
}
return defaultValue;
}
/// <summary>
/// 尝试获取值
/// </summary>
public bool TryGet<T>(string key, out T value)
{
if (data.TryGetValue(key, out object objValue))
{
value = (T)objValue;
return true;
}
value = default(T);
return false;
}
/// <summary>
/// 是否包含键
/// </summary>
public bool HasKey(string key)
{
return data.ContainsKey(key);
}
/// <summary>
/// 移除键
/// </summary>
public bool Remove(string key)
{
return data.Remove(key);
}
/// <summary>
/// 清空黑板
/// </summary>
public void Clear()
{
data.Clear();
}
}
在商业项目中,黑板通常还会支持数据分层的概念:
- 节点本地数据:只对当前节点可见,生命周期随节点
- 树级数据:整棵行为树所有节点可见,生命周期随行为树
- 全局数据:所有行为树实例可见,全局共享
这里我们实现一个简化版的分层黑板:
/// <summary>
/// 分层黑板系统
/// </summary>
public class HierarchicalBlackboard
{
// 全局数据
private static Dictionary<string, object> globalData = new Dictionary<string, object>();
// 树级数据
private Dictionary<string, object> treeData = new Dictionary<string, object>();
// 节点级数据
private Stack<Dictionary<string, object>> nodeDataStack = new Stack<Dictionary<string, object>>();
/// <summary>
/// 设置全局数据
/// </summary>
public static void SetGlobal<T>(string key, T value)
{
globalData[key] = value;
}
/// <summary>
/// 获取全局数据
/// </summary>
public static T GetGlobal<T>(string key, T defaultValue = default(T))
{
if (globalData.TryGetValue(key, out object value))
{
return (T)value;
}
return defaultValue;
}
/// <summary>
/// 进入新节点(创建节点数据层)
/// </summary>
public void EnterNode()
{
nodeDataStack.Push(new Dictionary<string, object>());
}
/// <summary>
/// 退出节点(销毁节点数据层)
/// </summary>
public void ExitNode()
{
if (nodeDataStack.Count > 0)
{
nodeDataStack.Pop();
}
}
/// <summary>
/// 设置树级数据
/// </summary>
public void SetTreeData<T>(string key, T value)
{
treeData[key] = value;
}
/// <summary>
/// 设置节点数据(当前节点)
/// </summary>
public void SetNodeData<T>(string key, T value)
{
if (nodeDataStack.Count > 0)
{
nodeDataStack.Peek()[key] = value;
}
else
{
Debug.LogError("没有活动的节点层,无法设置节点数据");
}
}
/// <summary>
/// 获取数据(按优先级:节点 > 树 > 全局)
/// </summary>
public T Get<T>(string key, T defaultValue = default(T))
{
// 查找节点数据
if (nodeDataStack.Count > 0)
{
var nodeData = nodeDataStack.Peek();
if (nodeData.TryGetValue(key, out object nodeValue))
{
return (T)nodeValue;
}
}
// 查找树级数据
if (treeData.TryGetValue(key, out object treeValue))
{
return (T)treeValue;
}
// 查找全局数据
if (globalData.TryGetValue(key, out object globalValue))
{
return (T)globalValue;
}
return defaultValue;
}
}
7.2.6 行为树容器
最后需要一个容器来管理整棵树的执行:
using UnityEngine;
/// <summary>
/// 行为树容器
/// </summary>
public class BehaviorTree
{
private BTNode rootNode;
private Blackboard blackboard;
private bool isRunning;
public BTNode RootNode => rootNode;
public Blackboard Blackboard => blackboard;
public bool IsRunning => isRunning;
public BehaviorTree(BTNode root, Blackboard bb = null)
{
rootNode = root;
blackboard = bb ?? new Blackboard();
// 将黑板传递给根节点
rootNode.SetBlackboard(blackboard);
}
/// <summary>
/// 执行行为树
/// </summary>
public BTStatus Tick()
{
if (rootNode == null)
{
return BTStatus.Failure;
}
isRunning = true;
BTStatus result = rootNode.Tick();
if (result != BTStatus.Running)
{
isRunning = false;
}
return result;
}
/// <summary>
/// 重置行为树
/// </summary>
public void Reset()
{
rootNode?.Reset();
isRunning = false;
}
/// <summary>
/// 中断行为树
/// </summary>
public void Abort()
{
rootNode?.Abort();
isRunning = false;
}
}
至此,一个完整的行为树框架就搭建完成了。接下来我们用这个框架实现一个真实的NPC AI。
7.3 案例实战:打造一个有真实感的守卫NPC
7.3.1 需求分析
假设我们需要实现一个城堡守卫NPC,它需要具备以下行为:
- 日常巡逻:在指定的路径点之间来回走动
- 听觉响应:听到可疑声音(比如玩家的脚步声)时,前往声源位置调查
- 视觉响应:看到玩家时,根据玩家状态做出反应
- 攻击行为:如果玩家进入攻击范围且处于敌对状态,发起攻击
- 血量管理:血量低到一定程度时逃跑
- 呼叫支援:发现玩家时,通知附近的守卫
这些行为需要能够互相打断和恢复。比如正在巡逻时听到声音,应该中断巡逻去调查;调查完后如果没发现玩家,应该返回巡逻点继续巡逻。
7.3.2 定义动作节点和条件节点
首先实现具体的动作节点:
using UnityEngine;
using UnityEngine.AI;
/// <summary>
/// 移动到指定位置
/// </summary>
public class MoveToPositionAction : ActionNode
{
private NavMeshAgent agent;
private float stoppingDistance;
private float moveSpeed;
public MoveToPositionAction(NavMeshAgent navAgent, float stopDist = 1.0f, float speed = 3.5f, string nodeName = "MoveToPosition") : base(nodeName)
{
agent = navAgent;
stoppingDistance = stopDist;
moveSpeed = speed;
}
protected override void OnEnter()
{
// 从黑板获取目标位置
Vector3 targetPos = blackboard.Get<Vector3>("TargetPosition", Vector3.zero);
if (targetPos != Vector3.zero)
{
agent.speed = moveSpeed;
agent.stoppingDistance = stoppingDistance;
agent.SetDestination(targetPos);
}
}
protected override BTStatus OnUpdate()
{
if (agent == null || !agent.isActiveAndEnabled)
{
return BTStatus.Failure;
}
// 获取目标位置
Vector3 targetPos = blackboard.Get<Vector3>("TargetPosition", Vector3.zero);
if (targetPos == Vector3.zero)
{
return BTStatus.Failure;
}
// 如果路径无效
if (!agent.hasPath)
{
return BTStatus.Failure;
}
// 检查是否到达目的地
if (agent.remainingDistance <= agent.stoppingDistance)
{
// 到达目的地,记录到达点并返回成功
blackboard.Set<Vector3>("LastReachedPosition", agent.transform.position);
return BTStatus.Success;
}
// 还在移动中
return BTStatus.Running;
}
protected override void OnExit()
{
// 如果执行失败或中断,停止移动
if (status != BTStatus.Success)
{
agent.ResetPath();
}
}
}
/// <summary>
/// 巡逻动作:沿着路径点移动
/// </summary>
public class PatrolAction : ActionNode
{
private NavMeshAgent agent;
private Transform[] waypoints;
private int currentWaypointIndex = 0;
private float waitTimeAtPoint = 2.0f;
private float waitTimer = 0f;
private PatrolState state = PatrolState.Moving;
private enum PatrolState
{
Moving,
Waiting
}
public PatrolAction(NavMeshAgent navAgent, Transform[] points, float waitTime = 2.0f, string nodeName = "Patrol") : base(nodeName)
{
agent = navAgent;
waypoints = points;
waitTimeAtPoint = waitTime;
}
protected override void OnEnter()
{
currentWaypointIndex = 0;
state = PatrolState.Moving;
if (waypoints != null && waypoints.Length > 0)
{
MoveToCurrentWaypoint();
}
}
private void MoveToCurrentWaypoint()
{
if (waypoints != null && waypoints.Length > 0)
{
agent.SetDestination(waypoints[currentWaypointIndex].position);
agent.stoppingDistance = 0.5f;
}
}
protected override BTStatus OnUpdate()
{
if (waypoints == null || waypoints.Length == 0 || agent == null)
{
return BTStatus.Failure;
}
if (state == PatrolState.Moving)
{
// 检查是否到达当前路径点
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
{
// 到达,切换到等待状态
state = PatrolState.Waiting;
waitTimer = 0f;
return BTStatus.Running;
}
// 还在移动中
return BTStatus.Running;
}
else // Waiting
{
waitTimer += Time.deltaTime;
if (waitTimer >= waitTimeAtPoint)
{
// 等待结束,移动到下一个路径点
currentWaypointIndex = (currentWaypointIndex + 1) % waypoints.Length;
MoveToCurrentWaypoint();
state = PatrolState.Moving;
}
return BTStatus.Running;
}
}
protected override void OnAbort()
{
agent.ResetPath();
}
}
/// <summary>
/// 攻击玩家动作
/// </summary>
public class AttackPlayerAction : ActionNode
{
private Animator animator;
private GameObject owner;
private float attackRange = 2.0f;
private float attackCooldown = 1.5f;
private float lastAttackTime = -10f;
public AttackPlayerAction(GameObject npc, Animator anim, float range = 2.0f, float cooldown = 1.5f, string nodeName = "Attack") : base(nodeName)
{
owner = npc;
animator = anim;
attackRange = range;
attackCooldown = cooldown;
}
protected override BTStatus OnUpdate()
{
// 获取玩家位置
GameObject player = blackboard.Get<GameObject>("PlayerObject");
if (player == null)
{
return BTStatus.Failure;
}
float distance = Vector3.Distance(owner.transform.position, player.transform.position);
// 检查是否在攻击范围内
if (distance > attackRange)
{
return BTStatus.Failure;
}
// 检查攻击冷却
if (Time.time - lastAttackTime < attackCooldown)
{
return BTStatus.Running; // 等待冷却
}
// 执行攻击
animator.SetTrigger("Attack");
lastAttackTime = Time.time;
// 伤害逻辑可以在这里触发,或者通过动画事件触发
Debug.Log($"{owner.name} 攻击玩家!");
// 攻击后回到运行中,等待下一次攻击
return BTStatus.Running;
}
}
条件节点实现:
using UnityEngine;
/// <summary>
/// 检查是否发现玩家(视觉检测)
/// </summary>
public class CanSeePlayerCondition : ConditionNode
{
private Transform owner;
private float viewDistance;
private float viewAngle;
private LayerMask obstacleMask;
public CanSeePlayerCondition(Transform npcTransform, float distance, float angle, LayerMask obstacles, string nodeName = "CanSeePlayer") : base(nodeName)
{
owner = npcTransform;
viewDistance = distance;
viewAngle = angle;
obstacleMask = obstacles;
}
protected override bool CheckCondition()
{
GameObject player = blackboard.Get<GameObject>("PlayerObject");
if (player == null)
{
return false;
}
Vector3 directionToPlayer = player.transform.position - owner.position;
float distance = directionToPlayer.magnitude;
// 距离检查
if (distance > viewDistance)
{
return false;
}
// 角度检查
float angle = Vector3.Angle(owner.forward, directionToPlayer.normalized);
if (angle > viewAngle * 0.5f)
{
return false;
}
// 视线遮挡检查
if (Physics.Raycast(owner.position, directionToPlayer.normalized, distance, obstacleMask))
{
return false; // 有障碍物遮挡
}
// 发现玩家,记录玩家位置到黑板
blackboard.Set<Vector3>("PlayerLastSeenPosition", player.transform.position);
return true;
}
}
/// <summary>
/// 检查是否听到声音
/// </summary>
public class CanHearNoiseCondition : ConditionNode
{
private Transform owner;
private float hearingRange;
public CanHearNoiseCondition(Transform npcTransform, float range, string nodeName = "CanHearNoise") : base(nodeName)
{
owner = npcTransform;
hearingRange = range;
}
protected override bool CheckCondition()
{
// 从黑板获取最近的声音位置
if (blackboard.TryGet<Vector3>("NoisePosition", out Vector3 noisePos))
{
float distance = Vector3.Distance(owner.position, noisePos);
if (distance <= hearingRange)
{
// 记录声音位置,供后续移动使用
blackboard.Set<Vector3>("InvestigatePosition", noisePos);
// 清除声音,避免重复调查
blackboard.Remove("NoisePosition");
return true;
}
}
return false;
}
}
/// <summary>
/// 检查血量是否过低
/// </summary>
public class IsHealthLowCondition : ConditionNode
{
private NPCHealth health;
private float thresholdPercent;
public IsHealthLowCondition(NPCHealth npcHealth, float threshold, string nodeName = "HealthLow") : base(nodeName)
{
health = npcHealth;
thresholdPercent = threshold;
}
protected override bool CheckCondition()
{
if (health == null)
{
return false;
}
float percent = (float)health.CurrentHealth / health.MaxHealth;
return percent <= thresholdPercent;
}
}
7.3.3 组合行为树
有了这些基础节点,我们就可以像搭积木一样组合出完整的守卫AI:
using UnityEngine;
using UnityEngine.AI;
/// <summary>
/// 守卫NPC控制器
/// </summary>
public class GuardController : MonoBehaviour
{
[Header("巡逻设置")]
[SerializeField] private Transform[] patrolWaypoints;
[SerializeField] private float patrolWaitTime = 2.0f;
[Header("感知设置")]
[SerializeField] private float viewDistance = 15.0f;
[SerializeField] private float viewAngle = 90.0f;
[SerializeField] private float hearingRange = 10.0f;
[SerializeField] private LayerMask obstacleMask;
[Header("战斗设置")]
[SerializeField] private float attackRange = 2.5f;
[SerializeField] private float attackCooldown = 1.2f;
[SerializeField] private float healthLowThreshold = 0.3f;
private NavMeshAgent agent;
private Animator animator;
private NPCHealth health;
private BehaviorTree behaviorTree;
private Blackboard blackboard;
// 模拟声音源,供其他脚本调用
public void ReportNoise(Vector3 noisePosition)
{
blackboard.Set<Vector3>("NoisePosition", noisePosition);
}
private void Awake()
{
agent = GetComponent<NavMeshAgent>();
animator = GetComponent<Animator>();
health = GetComponent<NPCHealth>();
// 初始化黑板
blackboard = new Blackboard();
blackboard.Set<GameObject>("PlayerObject", GameObject.FindGameObjectWithTag("Player"));
blackboard.Set<Vector3>("SpawnPosition", transform.position);
// 构建行为树
BuildBehaviorTree();
}
private void BuildBehaviorTree()
{
// 创建根节点 - 一个选择节点,用于处理优先级
SelectorNode root = new SelectorNode("GuardRoot");
// 1. 逃跑行为(最高优先级)
SequenceNode fleeSequence = new SequenceNode("FleeSequence");
fleeSequence.AddChild(new IsHealthLowCondition(health, healthLowThreshold, "CheckHealthLow"));
// 寻找安全位置(简化:跑回出生点)
fleeSequence.AddChild(new MoveToPositionAction(agent, 0.5f, 5.0f, "FleeToSpawn"));
root.AddChild(fleeSequence);
// 2. 攻击行为(次高优先级)
SequenceNode attackSequence = new SequenceNode("AttackSequence");
attackSequence.AddChild(new CanSeePlayerCondition(transform, viewDistance, viewAngle, obstacleMask, "CheckSeePlayer"));
// 如果看到玩家,先移动到攻击范围
SelectorNode attackSelector = new SelectorNode("AttackSelector");
SequenceNode moveToAttack = new SequenceNode("MoveToAttack");
moveToAttack.AddChild(new InverterNode("NotInRange")); // 条件:不在攻击范围内
// 设置目标位置为玩家位置
ConditionNode playerNotInRange = new ConditionNode("PlayerNotInRange")
{
// 自定义条件:检查距离
protected override bool CheckCondition()
{
GameObject player = blackboard.Get<GameObject>("PlayerObject");
if (player == null) return false;
float dist = Vector3.Distance(transform.position, player.transform.position);
return dist > attackRange;
}
};
moveToAttack.AddChild(playerNotInRange);
moveToAttack.AddChild(new MoveToPositionAction(agent, attackRange * 0.8f, 4.0f, "ApproachPlayer"));
attackSelector.AddChild(moveToAttack);
// 攻击动作
attackSelector.AddChild(new AttackPlayerAction(gameObject, animator, attackRange, attackCooldown, "AttackPlayer"));
attackSequence.AddChild(attackSelector);
root.AddChild(attackSequence);
// 3. 调查声音行为
SequenceNode investigateSequence = new SequenceNode("InvestigateSequence");
investigateSequence.AddChild(new CanHearNoiseCondition(transform, hearingRange, "CheckNoise"));
investigateSequence.AddChild(new MoveToPositionAction(agent, 1.0f, 3.5f, "MoveToNoise"));
// 调查完后,在位置等待几秒
WaitNode waitAfterInvestigate = new WaitNode(3.0f, "WaitAfterInvestigate");
investigateSequence.AddChild(waitAfterInvestigate);
root.AddChild(investigateSequence);
// 4. 默认巡逻行为
root.AddChild(new PatrolAction(agent, patrolWaypoints, patrolWaitTime, "DefaultPatrol"));
// 创建行为树
behaviorTree = new BehaviorTree(root, blackboard);
}
private void Update()
{
if (behaviorTree != null)
{
behaviorTree.Tick();
}
}
/// <summary>
/// 等待节点(简单实现)
/// </summary>
private class WaitNode : ActionNode
{
private float waitTime;
private float timer;
public WaitNode(float time, string nodeName = "Wait") : base(nodeName)
{
waitTime = time;
}
protected override void OnEnter()
{
timer = 0f;
}
protected override BTStatus OnUpdate()
{
timer += Time.deltaTime;
return timer >= waitTime ? BTStatus.Success : BTStatus.Running;
}
}
}
7.3.4 与其他系统的交互
这个守卫NPC可以通过事件系统与其他模块交互。比如玩家踩到陷阱发出声音:
public class TrapTrigger : MonoBehaviour
{
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
// 播放陷阱音效
AudioSource.PlayClipAtPoint(trapSound, transform.position);
// 通知所有守卫(通过事件中心)
GameEvents.ReportNoise(transform.position, 15f);
}
}
}
// 事件中心
public static class GameEvents
{
public static System.Action<Vector3, float> OnNoiseReported;
public static void ReportNoise(Vector3 position, float radius)
{
OnNoiseReported?.Invoke(position, radius);
}
}
// 守卫控制器中订阅事件
private void OnEnable()
{
GameEvents.OnNoiseReported += OnNoiseReported;
}
private void OnDisable()
{
GameEvents.OnNoiseReported -= OnNoiseReported;
}
private void OnNoiseReported(Vector3 position, float radius)
{
// 计算距离
float distance = Vector3.Distance(transform.position, position);
if (distance <= radius)
{
blackboard.Set<Vector3>("NoisePosition", position);
}
}
7.4 商业项目中的行为树优化
7.4.1 性能优化:LOD分级调度
在开放世界游戏中,同屏可能存在上百个NPC,如果每个NPC每帧都执行完整的行为树Tick,CPU会不堪重负。《原神》等商业项目采用了LOD(Level of Detail)分级调度策略:
public class NPCManager : MonoBehaviour
{
[SerializeField] private float highDetailDistance = 30f;
[SerializeField] private float mediumDetailDistance = 60f;
[SerializeField] private float lowDetailDistance = 100f;
private List<GuardController> allGuards = new List<GuardController>();
private void Update()
{
Transform player = Camera.main.transform;
foreach (var guard in allGuards)
{
float distance = Vector3.Distance(player.position, guard.transform.position);
if (distance <= highDetailDistance)
{
guard.SetTickRate(1); // 每帧更新
guard.SetBehaviorDetail(BehaviorDetail.Full);
}
else if (distance <= mediumDetailDistance)
{
guard.SetTickRate(3); // 每3帧更新一次
guard.SetBehaviorDetail(BehaviorDetail.Simplified);
}
else if (distance <= lowDetailDistance)
{
guard.SetTickRate(10); // 每10帧更新一次
guard.SetBehaviorDetail(BehaviorDetail.Minimal);
}
else
{
guard.SetTickRate(0); // 不更新
guard.EnableVisual(false); // 隐藏或冻结
}
}
}
}
简化版行为树可以省略一些细节判断,比如只做核心的状态切换,不做精细的移动控制。
7.4.2 内存优化:对象池与节点复用
行为树的每个节点都是一个对象,如果每个NPC都实例化一整棵树,内存占用会很大。实际项目中通常采用原型模式或节点复用:
public class BehaviorTreeFactory
{
// 节点原型缓存
private static Dictionary<string, BTNode> nodePrototypes = new Dictionary<string, BTNode>();
// 注册节点原型
public static void RegisterPrototype(string name, BTNode prototype)
{
nodePrototypes[name] = prototype;
}
// 克隆树结构
public static BTNode CloneTree(BTNode prototypeNode)
{
// 深度克隆节点及其子节点
BTNode clone = prototypeNode.Clone(); // 需要实现Clone方法
foreach (var child in prototypeNode.GetChildren())
{
clone.AddChild(CloneTree(child));
}
return clone;
}
}
7.4.3 可视化调试
没有可视化工具的行为树就像蒙着眼睛编程。商业项目都会配套编辑器,这里给一个简单的运行时调试器:
using UnityEngine;
public class BehaviorTreeDebugger : MonoBehaviour
{
private GuardController guard;
private GUIStyle style;
private void Start()
{
guard = GetComponent<GuardController>();
style = new GUIStyle();
style.normal.textColor = Color.white;
style.fontSize = 12;
}
private void OnGUI()
{
if (guard == null || guard.BehaviorTree == null) return;
Vector3 screenPos = Camera.main.WorldToScreenPoint(transform.position + Vector3.up * 2);
screenPos.y = Screen.height - screenPos.y;
GUI.Label(new Rect(screenPos.x - 50, screenPos.y - 15, 100, 30),
$"BT: {guard.CurrentBehavior}", style);
// 显示黑板关键数据
var bb = guard.Blackboard;
GUI.Label(new Rect(screenPos.x - 50, screenPos.y, 100, 30),
$"Health: {bb.Get<int>("Health")}", style);
}
}
7.4.4 与动画系统的融合
行为树的决策结果最终要落实到动画表现上。常见的做法是通过动画参数驱动状态机:
public class AIAnimationController : MonoBehaviour
{
private Animator animator;
private NavMeshAgent agent;
private void Update()
{
// 同步移动速度
animator.SetFloat("Speed", agent.velocity.magnitude);
// 同步攻击状态
animator.SetBool("IsAttacking", isAttacking);
// 通过动画事件触发实际伤害
}
// 由动画事件调用
public void OnAttackHit()
{
// 造成伤害
}
}
7.5 行为树的进阶模式
7.5.1 事件驱动型行为树
标准行为树每帧从根节点遍历,存在一定的性能浪费。事件驱动型行为树在节点状态变化时触发父节点重新评估,可以减少遍历次数。实现思路是为每个节点添加观察者列表,当节点状态变化时通知父节点。
7.5.2 动态子树替换
在MMO中,NPC的行为可能随场景变化,比如白天和夜晚不同、节日期间特殊行为。动态子树替换允许在运行时切换整个行为分支:
public class DynamicBehaviorNode : DecoratorNode
{
private BTNode dayBehavior;
private BTNode nightBehavior;
public DynamicBehaviorNode(BTNode day, BTNode night)
{
dayBehavior = day;
nightBehavior = night;
}
protected override BTStatus OnUpdate()
{
bool isDay = CheckIsDay();
BTNode currentBehavior = isDay ? dayBehavior : nightBehavior;
// 如果当前执行的节点不是目标节点,切换
if (child != currentBehavior)
{
if (child != null && child.IsRunning)
{
child.Abort();
}
child = currentBehavior;
child.Reset();
}
return child.Tick();
}
}
7.5.3 行为树与规划系统的结合
单纯的决策树只能根据预设规则响应,缺乏目标规划能力。高级AI会将行为树与规划算法结合,比如GOAP(Goal-Oriented Action Planning)。行为树负责常规行为,GOAP负责解决特定问题。
7.6 行为树与有限状态机的对比
根据IEEE的最新研究,行为树在模块化、可读性和可维护性上显著优于有限状态机,尤其是在任务复杂度增加时。
| 维度 | 有限状态机 | 行为树 |
|---|---|---|
| 状态转移复杂度 | 随状态数平方级增长 | 树状结构,线性增长 |
| 模块化复用 | 困难,状态间耦合 | 容易,节点独立 |
| 并行行为支持 | 需要额外状态组合 | 原生支持Parallel节点 |
| 调试可读性 | 转移图复杂 | 树结构直观 |
| 扩展性 | 修改影响大 | 增加节点不影响现有逻辑 |
| 运行时性能 | 高 | 稍低(需遍历) |
| 学习曲线 | 平缓 | 中等 |
商业项目中,两者常常混合使用:上层用行为树做决策,下层用状态机做动画控制。比如行为树决定"攻击玩家",具体是近战攻击还是远程攻击、攻击的动画细节,由动画状态机负责。
通过本章的学习,我们从零实现了一个完整的轻量级行为树框架,并用它构建了一个具备复杂行为的守卫NPC。这个框架虽然简单,但包含了行为树的核心思想:组合节点控制流程、条件动作分离、黑板共享数据、Running状态支持耗时操作。
在实际商业项目中,行为树的价值不仅在于技术层面,更在于它改变了团队协作模式。程序员负责开发原子节点,策划通过可视化工具组合行为,AI逻辑的迭代速度大大加快。正如IEEE论文所指出的,随着任务复杂度的增加,行为树在模块化、可读性上的优势越来越明显。
在下一章中,我们将把视角从单个NPC扩展到群体AI,探讨如何通过行为树与群体行为算法的结合,构建出更有生命力的虚拟世界。
更多推荐


所有评论(0)