diff --git a/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs b/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs
index daf66248..9c9bb2b1 100644
--- a/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs
+++ b/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs
@@ -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;
+ /// 上次寻路移动是否因不可达/被挡而受阻(走到最近点或原地停,而非到达原目标)。新目标 RequestMoveTo 后复位。
+ public bool LastMoveObstructed => _lastMoveObstructed;
+
+ /// 取上次寻路失败时算出的最近可达世界点。无则返回 false。
+ public bool TryGetClosestReachable(out Vector2 worldPos)
+ {
+ if (_navAgent != null) return _navAgent.TryGetClosestReachablePosition(out worldPos);
+ worldPos = Vector2.zero;
+ return false;
+ }
+
// ── IPathAgent 事件 ────────────────────────────────────────────
public event Action OnLinkStarted;
public event Action OnLinkCompleted;
@@ -69,6 +86,15 @@ namespace BaseGames.Enemies.Navigation
= new Dictionary();
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
{