You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity行为树实现收割机AI命令下达:序列不执行及命令覆盖问题

Unity RTS单位AI行为树问题排查与命令覆盖实现

问题概述

开发小型RTS项目时,为收割机等单位搭建的行为树基础功能正常,但存在两个核心问题:

  • 右键下达移动命令后,行为树中处理命令的首个序列从未触发,尽管已将_hasOrder设为true并赋值_targetPosition。
  • 需要实现新命令覆盖旧命令的逻辑(如多次移动命令仅执行最后一次),当前尝试的Condition节点检查方式未达预期。

当前状态:

  • 收割机会自动执行采集-运送资源的循环逻辑
  • 单位选择功能正常,右键地形可调用Move()方法传递目标位置
  • 行为树根节点为Selector,首个Sequence用于处理玩家命令,第二个Sequence处理自动采集逻辑

相关代码片段

HarvestingUnit 类

public class HarvestingUnit : BaseUnit
{
        [SerializeField] private HarvestingBuilding _associatedBuilding = default;
        private int _inventorySlots = 1;
        private int _collectedResources = 0;
        private float _harvestingTime = 3.0f;

        protected override Node SetupTree()
        {
            Node root = new Selector(new List<Node>
            {
                // 处理玩家命令的序列(未触发)
                new Sequence(new List<Node>
                {
                    new Condition(() => _hasOrder == true),
                    new GiveTarget(() => _targetPosition),
                    new TaskGoTo(transform, _agent),
                    new ActionNode(() => _hasOrder = false),
                }),

                // 自动采集资源的序列
                new Sequence(new List<Node>
                {                        
                    new CheckForAvailableResource(_associatedBuilding),
                    new Selector(new List<Node>
                    {
                        // 背包满时运送资源
                        new Sequence(new List<Node>
                        {
                            new Condition(() => _collectedResources == _inventorySlots),
                            new GiveTarget(() => _associatedBuilding.GetNearestWarehoursePosition()),
                            new TaskTransportResource(transform, _agent, _associatedBuilding.GetNearestWarehoursePosition()),
                            new ActionNode(() => _collectedResources = 0)
                        }),

                        // 背包未满时采集资源
                        new Sequence(new List<Node>
                        {
                            new Repeater(new List<Node>
                            {                     
                                new Condition(() => _collectedResources < _inventorySlots),
                                new TaskGoTo(transform, _agent),
                                new TaskHarvest(transform),
                                new Wait(_harvestingTime),
                                new ActionNode(() => _collectedResources++),
                            })
                        }),
                    })
                })
            });
            root.Reset();
            return root;
        }

    public void Move(Vector3 targetPosition)
    {
        _hasOrder = true;
        Debug.Log(_hasOrder);
        _targetPosition = targetPosition;
    }
}

Tree 基类

public abstract class Tree : MonoBehaviour
{
    private Node _root = null;

    protected virtual void Start()
    {
        _root = SetupTree();
    }

    private void Update()
    {
        if(_root != null)
        {
            _root.Evaluate();
        }
    }

    protected abstract Node SetupTree();
}

问题排查与解决方案

1. 命令序列未触发的原因及修复

核心问题分析

  • Selector节点逻辑特性:Selector会优先执行第一个返回Success的子节点。第二个Sequence中的CheckForAvailableResource节点如果始终返回Success,会导致Selector一直执行第二个Sequence,完全跳过第一个命令序列。
  • Repeater节点阻塞:第二个Sequence内的Repeater节点如果持续返回Running,会让整个父Sequence保持Running状态,Selector会一直停留在这个分支,不会再去检查第一个命令序列的Condition。

修复步骤

  • 调整Selector分支优先级:确保玩家命令分支拥有最高优先级,且能打断自动采集逻辑。修改行为树结构,在自动采集的Sequence前添加一个Condition,检查_hasOrder == false,这样当有玩家命令时,自动采集分支会直接返回Failure,Selector会切换到命令分支:
// 修改后的自动采集序列
new Sequence(new List<Node>
{                        
    new Condition(() => _hasOrder == false), // 新增:仅当无玩家命令时执行
    new CheckForAvailableResource(_associatedBuilding),
    // ... 后续原有逻辑
})
  • 检查Node节点的返回值:确认Condition(() => _hasOrder == true)节点在_hasOrder为true时确实返回Success,TaskGoTo等节点在执行完成后返回Success而非持续Running(如果是异步移动,需要节点正确处理Running/Success状态)。

2. 新命令覆盖旧命令的实现

实现逻辑

在Move()方法中直接覆盖_targetPosition,同时确保行为树能立即响应新的目标:

  • 如果单位正在执行旧命令,直接更新目标位置即可(前提是TaskGoTo节点会持续读取_targetPosition的值,而非仅在初始化时读取)。
  • 若GiveTarget节点仅在首次执行时传递目标,需要修改GiveTarget或TaskGoTo,使其每帧检查_targetPosition是否更新。

代码修改示例

public void Move(Vector3 targetPosition)
{
    _hasOrder = true;
    _targetPosition = targetPosition;
    // 可选:重置当前正在执行的移动任务,强制重新读取目标
    if (_agent != null && _agent.isActiveAndEnabled)
    {
        _agent.SetDestination(_targetPosition);
    }
}

同时,修改TaskGoTo节点,使其每帧检查目标是否变化:

public class TaskGoTo : Node
{
    private Transform _transform;
    private NavMeshAgent _agent;
    private Func<Vector3> _getTarget; // 改为通过委托动态获取目标

    public TaskGoTo(Transform transform, NavMeshAgent agent, Func<Vector3> getTarget)
    {
        _transform = transform;
        _agent = agent;
        _getTarget = getTarget;
    }

    public override NodeState Evaluate()
    {
        Vector3 target = _getTarget();
        if (Vector3.Distance(_transform.position, target) < 0.1f)
        {
            _agent.isStopped = true;
            return NodeState.Success;
        }
        // 每帧更新目标
        if (_agent.destination != target)
        {
            _agent.SetDestination(target);
        }
        _agent.isStopped = false;
        return NodeState.Running;
    }
}

然后在SetupTree中修改TaskGoTo的初始化:

new TaskGoTo(transform, _agent, () => _targetPosition),

额外注意事项

  • 行为树节点的状态管理:确保所有节点正确返回NodeState.Success/NodeState.Failure/NodeState.Running,尤其是Repeater节点,需要在条件不满足时返回Failure,让父节点能切换分支。
  • 变量同步问题:_hasOrder和_targetPosition需保证在Unity主线程中修改和读取,避免异步访问导致的逻辑异常。

内容的提问来源于stack exchange,提问作者BLecoffre

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 14:22:45