第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,它需要具备以下行为:

  1. 日常巡逻:在指定的路径点之间来回走动
  2. 听觉响应:听到可疑声音(比如玩家的脚步声)时,前往声源位置调查
  3. 视觉响应:看到玩家时,根据玩家状态做出反应
  4. 攻击行为:如果玩家进入攻击范围且处于敌对状态,发起攻击
  5. 血量管理:血量低到一定程度时逃跑
  6. 呼叫支援:发现玩家时,通知附近的守卫

这些行为需要能够互相打断和恢复。比如正在巡逻时听到声音,应该中断巡逻去调查;调查完后如果没发现玩家,应该返回巡逻点继续巡逻。

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,探讨如何通过行为树与群体行为算法的结合,构建出更有生命力的虚拟世界。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐