From a4b9729de70b0f21a455101bc0493d1683125f1f Mon Sep 17 00:00:00 2001 From: Joywayer Date: Fri, 10 Jul 2026 15:12:28 +0800 Subject: [PATCH] =?UTF-8?q?@=20fix(enemy):=20=E5=B7=A1=E9=80=BB=E8=BE=B9?= =?UTF-8?q?=E7=BC=98=E4=B8=8D=E8=B6=8A=E7=95=8C=20+=20=E5=8D=A1=E6=AD=BB?= =?UTF-8?q?=E5=85=9C=E5=BA=95=20+=20Locomotion=20=E6=83=B0=E6=80=A7?= =?UTF-8?q?=E5=8F=91=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WouldBlockAhead(EnemyMovement):按请求方向从碰撞体前缘(bounds.max/min.x) 即时射线判墙/悬崖,天然考虑碰撞体宽度;MoveHorizontal 据此夹紧前进,保证 "碰撞体边缘停在地形边缘、不越界"。移动层夹紧与 Pace 翻向共用同一判定, 避免"夹停却不掉头"。nav 走 MoveWithSpeed 不受影响。 - Pace 加窗口净位移卡死兜底:撞上未挂检测层的墙/卡角(含贴墙微抖)也能翻向, 用净位移而非逐帧差以免微抖重置计数。 - EnemyBase.Locomotion 改惰性发现:关域重载下 Awake 设的实例字段可能残留 为 null,首次访问按需补发现,编辑器/构建都不为空(此前致入口态偶发 NRE)。 PlayMode(跨调用取样,避免 Thread.Sleep 冻结主线程)验证:Pace 来回踱步、 遇边缘/墙掉头、碰撞体不越界。 Co-Authored-By: Claude Opus 4.8 (1M context) @ --- Assets/_Game/Scripts/Enemies/EnemyBase.cs | 4 ++- Assets/_Game/Scripts/Enemies/EnemyMovement.cs | 33 ++++++++++++++++++- .../Enemies/Navigation/EnemyLocomotion.cs | 27 ++++++++++++--- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/Assets/_Game/Scripts/Enemies/EnemyBase.cs b/Assets/_Game/Scripts/Enemies/EnemyBase.cs index f0f84fcb..8bfae856 100644 --- a/Assets/_Game/Scripts/Enemies/EnemyBase.cs +++ b/Assets/_Game/Scripts/Enemies/EnemyBase.cs @@ -209,7 +209,9 @@ namespace BaseGames.Enemies // BD 任务访问接口(公共只读属性)──────────────────────────────── public IPathAgent Nav => _nav; - public IEnemyLocomotion Locomotion => _locomotion; + // 惰性发现兜底:关域重载后 Awake 设的实例字段可能残留为 null(编辑器快速迭代), + // 首次访问时按需补发现,保证编辑器与构建下都不为空。 + public IEnemyLocomotion Locomotion => _locomotion ?? (_locomotion = GetComponentInChildren(true)); public EnemyMovement Movement => _movement; public EnemyStats Stats => _stats; /// 敌人配置 SO,供 BD Task / 状态对象读取配置数据(如 knockUpDuration)。 diff --git a/Assets/_Game/Scripts/Enemies/EnemyMovement.cs b/Assets/_Game/Scripts/Enemies/EnemyMovement.cs index af4cca99..622a4f41 100644 --- a/Assets/_Game/Scripts/Enemies/EnemyMovement.cs +++ b/Assets/_Game/Scripts/Enemies/EnemyMovement.cs @@ -226,6 +226,35 @@ namespace BaseGames.Enemies return new Vector2(x, b.min.y); } + /// + /// 朝 前进是否会撞墙或让碰撞体前缘越过地形边缘。 + /// 用碰撞体前缘(bounds.max.x / min.x)判定,天然考虑碰撞体宽度—— + /// 保证"碰撞体边缘不越过地形边缘"。用当前请求方向即时射线,避免缓存朝向过期。 + /// 供移动层夹紧与巡逻 Pace 翻向共用同一判定,避免"夹停了却不掉头"。 + /// + public bool WouldBlockAhead(float dir) + { + var col = _groundCheckCollider != null ? _groundCheckCollider : GetComponent(); + if (col == null) return false; + Bounds b = col.bounds; + float sign = Mathf.Sign(dir); + float edgeX = sign >= 0f ? b.max.x : b.min.x; // 朝向侧碰撞体前缘 + LayerMask wallLayer = (_wallMask.value != 0) ? _wallMask : _groundMask; + + // 墙:从前缘水平探;命中则前方有墙 + if (_wallCheckDist > 0f && + Physics2D.Raycast(new Vector2(edgeX, b.center.y), new Vector2(sign, 0f), _wallCheckDist, wallLayer)) + return true; + + // 悬崖:从"前缘 + 前探偏移"处向下探地;无地面则前缘外是悬崖 + if (_ledgeCheckDownDist > 0f && + !Physics2D.Raycast(new Vector2(edgeX + sign * _ledgeCheckFwdOffset, b.min.y), + Vector2.down, _ledgeCheckDownDist, _groundMask)) + return true; + + return false; + } + private void WallAndLedgeCheck() { LayerMask wallLayer = (_wallMask.value != 0) ? _wallMask : _groundMask; @@ -375,7 +404,9 @@ namespace BaseGames.Enemies public void MoveHorizontal(float dir) { if (_isTurning) return; - UpdateFacing(dir); + if (dir != 0f) UpdateFacing(dir); // 先朝请求方向(视觉转身),即使随后停步 + // 地面怪:前缘将越过地形边缘/撞墙则不前进(碰撞体边缘停在地形边缘,不越界) + if (dir != 0f && IsGrounded && WouldBlockAhead(dir)) dir = 0f; var vel = _rb.velocity; vel.x = dir * _config.WalkSpeed; _rb.velocity = vel; diff --git a/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs b/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs index 479450ba..5a464aa7 100644 --- a/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs +++ b/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs @@ -27,6 +27,8 @@ namespace BaseGames.Enemies private bool _gaitInit; private float _paceDir = 1f; private bool _paceBlockedPrev; + private float _paceWindowStartX; + private int _paceWindowFrames; private int _wpIndex = -1; private int _wpDir = 1; private bool _warnedNoWaypoints; @@ -114,11 +116,26 @@ namespace BaseGames.Enemies case PatrolStrategy.Pace: // 撞墙/悬崖翻向的来回踱步(速度驱动,不依赖 nav) var mv = _enemy.Movement; - bool blocked = mv != null && (mv.IsWallAhead || mv.IsLedgeAhead); - // 边沿触发:仅在"障碍首次出现"时翻向一次。TickPatrol 跑在 Update(可变帧率), - // 而 IsWallAhead/IsLedgeAhead 在 FixedUpdate 刷新——若用电平触发(每帧 blocked 就翻), - // 检测刷新前的多个 Update 帧会把同一障碍连翻多次→边缘左右抖动。 - if (blocked && !_paceBlockedPrev) _paceDir = -_paceDir; + // 用与移动层夹紧同一判定(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) + { + _paceDir = -_paceDir; + _paceWindowStartX = px; _paceWindowFrames = 0; // 翻向后重开窗口 + } _paceBlockedPrev = blocked; _enemy.MoveInDirection(_paceDir); break;