feat(enemy): EnemyNavAgent 卡死检测+统一受阻处理+best-effort 改道到最近可达点
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user