From c929b5efb7a80209715e6729bfdcd77ef39dcaa9 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 28 Jul 2026 11:04:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor(enemy):=20=E4=B8=89=E5=A4=84=E5=8D=A1?= =?UTF-8?q?=E6=AD=BB=E6=A3=80=E6=B5=8B=E6=94=B6=E6=95=9B=E4=B8=BA=20StallD?= =?UTF-8?q?etector;=E4=BF=AE=E5=A4=8D=20Pace=20=E6=8C=81=E7=BB=AD=E5=8F=97?= =?UTF-8?q?=E9=98=BB=E6=97=B6=E6=97=A0=E6=B3=95=E6=8E=89=E5=A4=B4=E7=9A=84?= =?UTF-8?q?=E6=AD=BB=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scripts/Enemies/Abilities/RushAbility.cs | 20 +++--------- .../Enemies/Navigation/EnemyLocomotion.cs | 27 +++++++--------- .../Enemies/Navigation/EnemyNavAgent.cs | 32 ++++++------------- 3 files changed, 25 insertions(+), 54 deletions(-) diff --git a/Assets/_Game/Scripts/Enemies/Abilities/RushAbility.cs b/Assets/_Game/Scripts/Enemies/Abilities/RushAbility.cs index 839f7c8b..693e5226 100644 --- a/Assets/_Game/Scripts/Enemies/Abilities/RushAbility.cs +++ b/Assets/_Game/Scripts/Enemies/Abilities/RushAbility.cs @@ -31,9 +31,8 @@ namespace BaseGames.Enemies.Abilities { private RushAbilitySO _cfg; - // 卡死检测:窗口时长与窗口内最小净位移(撞上未纳入墙层的碰撞体时兜底结束冲锋) - private const float StuckWindowSeconds = 0.15f; - private const float StuckMinProgress = 0.05f; + // 推进停滞检测(撞上未纳入墙层的碰撞体时兜底结束冲锋) + private StallDetector _stall = new StallDetector(0.15f, 0.05f); // 落位收敛的最长额外推进时长(秒):用于脱离"中心悬在崖外"的非法收尾姿态 private const float SettleMaxSeconds = 0.5f; @@ -101,8 +100,7 @@ namespace BaseGames.Enemies.Abilities { var body = _enemy.Body; float elapsed = 0f; - float winX = _enemy.transform.position.x; - float winT = 0f; + _stall.Reset(_enemy.transform.position.x); while (true) { @@ -120,16 +118,8 @@ namespace BaseGames.Enemies.Abilities float dt = Time.deltaTime; elapsed += dt; - winT += dt; - - // 兜底:撞上未纳入墙层的碰撞体(玩家/边界墙)导致原地不动 → 结束本次冲锋。 - // 用窗口净位移判定,避免"贴住微抖"每帧重置。 - if (winT >= StuckWindowSeconds) - { - if (Mathf.Abs(_enemy.transform.position.x - winX) < StuckMinProgress) break; - winX = _enemy.transform.position.x; - winT = 0f; - } + // 兜底:撞上未纳入墙层的碰撞体(玩家/边界墙)导致原地不动 → 结束本次冲锋 + if (_stall.Tick(_enemy.transform.position.x, dt)) break; } } diff --git a/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs b/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs index 1a59ff27..f40c49a9 100644 --- a/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs +++ b/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs @@ -33,8 +33,8 @@ namespace BaseGames.Enemies private bool _gaitInit; private float _paceDir = 1f; private bool _paceBlockedPrev; - private float _paceWindowStartX; - private int _paceWindowFrames; + // Pace 推进停滞检测(原为 12 帧计数,@60fps ≈ 0.2s,改用统一原语并保持观感) + private StallDetector _paceStall = new StallDetector(0.2f, 0.05f); private int _wpIndex = -1; private int _wpDir = 1; private bool _warnedNoWaypoints; @@ -179,22 +179,17 @@ namespace BaseGames.Enemies // 用与移动层夹紧同一判定(WouldBlockAhead,即时按请求方向+碰撞体前缘), // 保证"夹停在地形边缘"与"掉头"一致,不会夹停却不翻向。 bool blocked = mv != null && mv.WouldBlockAhead(_paceDir); - // 兜底卡死检测(窗口净位移):命令前进但一段时间净位移过小(撞上未挂检测层的墙/ - // 卡角,含贴墙微抖)也视为受阻,使巡逻对场景配置鲁棒。用净位移而非逐帧差, - // 避免"贴墙推进-被弹回"的微抖每帧重置计数。 - float px = _enemy.transform.position.x; - if (++_paceWindowFrames >= 12) - { - if (Mathf.Abs(px - _paceWindowStartX) < 0.05f) blocked = true; - _paceWindowStartX = px; - _paceWindowFrames = 0; - } - // 边沿触发:仅"障碍首次出现"翻一次;翻后按新方向即时判定,天然消抖, - // 且两侧都堵(窄台)时翻一次即停住、不来回抖。 - if (blocked && !_paceBlockedPrev) + // 兜底卡死检测:命令前进但窗口内净位移过小(撞上未挂检测层的墙/卡角,含贴墙微抖)也视为受阻。 + if (_paceStall.Tick(_enemy.transform.position.x, Time.deltaTime)) blocked = true; + + // 翻向:首次受阻翻一次(边沿触发,天然消抖);或"持续受阻但反方向畅通"也翻—— + // 后者修复"顶着堵侧却因边沿触发失效而永远转不回来"的死锁。 + // 两侧都堵(窄台)时只有边沿那一次翻向,之后保持,不来回抖。 + bool oppositeFree = mv != null && !mv.WouldBlockAhead(-_paceDir); + if (blocked && (!_paceBlockedPrev || oppositeFree)) { _paceDir = -_paceDir; - _paceWindowStartX = px; _paceWindowFrames = 0; // 翻向后重开窗口 + _paceStall.Reset(_enemy.transform.position.x); // 翻向后重开窗口 } _paceBlockedPrev = blocked; _enemy.MoveInDirection(_paceDir); diff --git a/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs b/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs index 64870527..f9ee5561 100644 --- a/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs +++ b/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs @@ -91,9 +91,7 @@ namespace BaseGames.Enemies.Navigation private bool _redirectedToClosest; // 本轮已改道到最近点,避免每帧重复下发 private bool _obstructionSignal; // 受阻信号(寻路失败/卡死),延后到 FixedUpdate 统一处理 // 卡死检测窗口 - private float _stuckWinTimer; - private float _stuckWinStartDist; - private bool _stuckWinInit; + private StallDetector _segStall; // 连接段状态缓存 private NavLinkType _currentLinkType = NavLinkType.None; @@ -121,6 +119,8 @@ namespace BaseGames.Enemies.Navigation _navAgent.OnSegmentTraversal += HandleSegmentTraversal; _navAgent.OnFailedToFindPath += HandlePathFailed; _navAgent.OnReachedGoal += HandleGoalReached; + + _segStall = new StallDetector(_stuckWindowSeconds, _stuckMinProgress); } private void OnDestroy() @@ -327,30 +327,16 @@ namespace BaseGames.Enemies.Navigation _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; - } + if (!onSegment) { _segStall.Reset(transform.position.x); return; } + if (_segStall.Tick(transform.position.x, Time.fixedDeltaTime)) + _obstructionSignal = true; // 窗口内几乎没推进 → 卡死 } - private void ResetStuckWindow() => _stuckWinInit = false; + private void ResetStuckWindow() => _segStall.Reset(transform.position.x); // 统一受阻处理:尽力改道到最近可达点(走到障碍前),拿不到更近点则停下,交由意图层结算。 private void ProcessObstruction()