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
相关产品推荐
相关产品推荐

