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 {