feat(enemy): EnemyNavAgent 卡死检测+统一受阻处理+best-effort 改道到最近可达点

This commit is contained in:
2026-07-23 16:56:56 +08:00
parent 4079392a79
commit 675e1950af
@@ -41,6 +41,12 @@ namespace BaseGames.Enemies.Navigation
"烘焙段都超过此值(真正脱离导航面)才判失败。")]
[SerializeField] private float _maxSnapDistance = 10f;
[Header("受阻检测(寻路移动无进展即判被挡)")]
[Tooltip("卡死检测窗口时长(s):段上移动时,此时长内朝子目标推进小于阈值即判受阻")]
[SerializeField] private float _stuckWindowSeconds = 0.5f;
[Tooltip("卡死推进阈值(m):窗口内朝子目标的距离缩小不足此值即判卡死")]
[SerializeField] private float _stuckMinProgress = 0.05f;
// 重寻路防抖状态
private Vector2 _lastPathGoal;
private bool _hasPathGoal;
@@ -53,6 +59,17 @@ namespace BaseGames.Enemies.Navigation
public Vector2 CurrentLinkStart => _currentLinkStart;
public Vector2 CurrentLinkEnd => _currentLinkEnd;
/// <summary>上次寻路移动是否因不可达/被挡而受阻(走到最近点或原地停,而非到达原目标)。新目标 RequestMoveTo 后复位。</summary>
public bool LastMoveObstructed => _lastMoveObstructed;
/// <summary>取上次寻路失败时算出的最近可达世界点。无则返回 false。</summary>
public bool TryGetClosestReachable(out Vector2 worldPos)
{
if (_navAgent != null) return _navAgent.TryGetClosestReachablePosition(out worldPos);
worldPos = Vector2.zero;
return false;
}
// ── IPathAgent 事件 ────────────────────────────────────────────
public event Action<NavLinkType> OnLinkStarted;
public event Action<NavLinkType> OnLinkCompleted;
@@ -69,6 +86,15 @@ namespace BaseGames.Enemies.Navigation
= new Dictionary<NavLinkType, INavLinkHandler>();
private INavLinkHandler _activeHandler;
// 受阻状态(供意图层结算)
private bool _lastMoveObstructed;
private bool _redirectedToClosest; // 本轮已改道到最近点,避免每帧重复下发
private bool _obstructionSignal; // 受阻信号(寻路失败/卡死),延后到 FixedUpdate 统一处理
// 卡死检测窗口
private float _stuckWinTimer;
private float _stuckWinStartDist;
private bool _stuckWinInit;
// 连接段状态缓存
private NavLinkType _currentLinkType = NavLinkType.None;
private bool _wasNavOnSegment;
@@ -155,6 +181,9 @@ namespace BaseGames.Enemies.Navigation
_lastPathGoal = target;
_hasPathGoal = true;
_lastPathTime = Time.time;
_lastMoveObstructed = false; // 新目标 → 复位受阻态
_redirectedToClosest = false;
ResetStuckWindow();
_navAgent.UpdatePath(target);
}
@@ -248,7 +277,12 @@ namespace BaseGames.Enemies.Navigation
}
}
private void HandlePathFailed(NavAgent _) => OnNavPathFailed?.Invoke();
private void HandlePathFailed(NavAgent _)
{
_obstructionSignal = true; // 延后到 FixedUpdate 统一处理,避免在 NavAgent 失败回调内重入 UpdatePath
OnNavPathFailed?.Invoke();
}
private void HandleGoalReached(NavAgent _) => OnGoalReached?.Invoke();
// ── 物理原生移动桥 ─────────────────────────────────────────────
@@ -264,6 +298,8 @@ namespace BaseGames.Enemies.Navigation
bool onSegment = _navAgent.IsMovingOnSegment;
_enemyMovement.NavDriving = onSegment; // 状态标记(供调试/上层判断"是否导航中"
TickStuckDetection(onSegment);
if (onSegment)
{
// 物理原生:朝子目标水平移动,由 EnemyMovement 施加 velocity、刚体重力保持贴地
@@ -271,7 +307,7 @@ namespace BaseGames.Enemies.Navigation
_enemyMovement.PendingInput.MoveDir = Mathf.Abs(dx) > 0.05f ? Mathf.Sign(dx) : 0f;
_enemyMovement.PendingInput.MoveSpeed = _movement != null ? _movement.movementSpeed : 0f;
// 段推进:到达当前子目标 → 推进到下一段(原由 TBM 负责,禁用后由本组件负责)
// 段推进:到达当前子目标 → 推进到下一段
if (_navAgent.HasReachedCurrentSubGoal(0.25f))
_navAgent.CompleteSegmentTraversal();
}
@@ -281,9 +317,61 @@ namespace BaseGames.Enemies.Navigation
_enemyMovement.PendingInput.WantStop = true;
}
// 受阻信号统一处理(寻路失败/卡死):此处不在 NavAgent 回调内,UpdatePath 安全
if (_obstructionSignal)
{
_obstructionSignal = false;
ProcessObstruction();
}
_wasNavOnSegment = onSegment;
}
// 卡死检测:段上移动时,滑动窗口内朝子目标推进不足阈值 → 判受阻(覆盖"障碍未进图"的物理空推)。
private void TickStuckDetection(bool onSegment)
{
if (!onSegment) { _stuckWinInit = false; return; }
float dist = Mathf.Abs(_navAgent.PathSubGoal.x - transform.position.x);
if (!_stuckWinInit)
{
_stuckWinInit = true;
_stuckWinTimer = 0f;
_stuckWinStartDist = dist;
return;
}
_stuckWinTimer += Time.fixedDeltaTime;
if (_stuckWinTimer >= _stuckWindowSeconds)
{
if (_stuckWinStartDist - dist < _stuckMinProgress)
_obstructionSignal = true; // 窗口内几乎没靠近子目标 → 卡死
_stuckWinTimer = 0f;
_stuckWinStartDist = dist;
}
}
private void ResetStuckWindow() => _stuckWinInit = false;
// 统一受阻处理:尽力改道到最近可达点(走到障碍前),拿不到更近点则停下,交由意图层结算。
private void ProcessObstruction()
{
_lastMoveObstructed = true;
if (!_redirectedToClosest
&& _navAgent.TryGetClosestReachablePosition(out var closest)
&& Vector2.Distance(transform.position, closest) > _repathTargetThreshold)
{
_redirectedToClosest = true;
ResetStuckWindow();
_navAgent.UpdatePath(closest); // 走向最近可达点
return;
}
// 无更近点 / 已在最近点附近(含"障碍未进图"无 closest 数据)→ 停下
_navAgent.Stop();
_enemyMovement.PendingInput.WantStop = true;
}
// ── 工具 ───────────────────────────────────────────────────────
private static NavLinkType ParseLinkType(string name) => name switch
{