行为树简介

本文大致介绍了行为树是什么,适合用于复习行为树的概念,或对行为树做一个大致了解。

什么是行为树

行为树是一种用于控制游戏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; }
	
        ……
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值