From 66d100f353229c9595cbeaccf71c6dce7458cd76 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Thu, 23 Jul 2026 10:35:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(enemy):=20=E7=9B=AE=E6=A0=87=E7=82=B9?= =?UTF-8?q?=E5=90=B8=E9=99=84=E6=94=B9=E7=94=A8=E5=9B=BA=E5=AE=9A=E5=A4=A7?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E8=B7=9D=E7=A6=BB,=E4=B8=8E=20PB2d=20?= =?UTF-8?q?=E6=98=A0=E5=B0=84=E4=BB=BB=E6=84=8F=E7=82=B9=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上版把搜索距离绑到 agent→目标距离(仿 UpdatePath),仍脆弱:吸附距离取决于 agent 离得多远 而非目标离导航段多远——agent 贴在高处路点正下方时距离小、reach 小仍会漏。 查明 PB2d 映射机制(NavGraph.TryMapPointSurface):queryAABB 边长=传入值、可靠搜索半径仅其一半; PB2d 映射"鼠标点击等任意点"用固定的 closestToSegmentMaxDistance(默认 20,"想多大就多大")。 据此改为:EnemyNavAgent._maxSnapDistance(默认 10m,真实"点离最近段"上限),底层传 2× 补偿 AABB 半宽。 只要目标在 _maxSnapDistance 内一定吸附到最近烘焙段,返回 false 仅表示目标真正脱离导航面。 Play 实测:两路点 + 压力用例(agent 正上方 3m/5m)ResolveStandablePoint 均 ok=True 且映射到地面段。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../PathBerserker2d/NavAgent/NavAgent.cs | 33 ++++++++++--------- .../Enemies/Navigation/EnemyNavAgent.cs | 10 ++++-- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs b/Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs index 9c3e6b09..aa90f606 100644 --- a/Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs +++ b/Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs @@ -620,24 +620,27 @@ namespace PathBerserker2d return PathTo(goal); } - // [项目新增] 目标点吸附:把任意世界点映射到最近的可行走段,再按 endMargin 从段两端内缩, - // 得到"身体真正站得住"的世界点(避免落在角色宽度站不住的崖沿被移动层夹停)。 - // 与 SetRandomDestinationOnCurrentSegment 同源:endMargin 传 agent 半宽 + 落崖检测余量。段短于 - // 2*endMargin → 退化到中点。映射方式与 PB2d 自身 UpdatePath 映射目标一致(见 UpdatePath): - // 搜索距离 = agent 到目标的距离(+余量)、用不带 agent 过滤的 TryMapPoint(就近段几何映射,可达性交给 - // allowCloseEnoughPath 的寻路阶段),故只要图上有段就一定能吸附——不会像固定小半径那样漏掉稍离地的点。 + // [项目新增] 目标点吸附:把任意世界点映射到最近的可行走段(与 PB2d 自身"把鼠标点击等任意点映射到 + // 最近导航位置"同一机制),再按 endMargin 从段两端内缩,得到"身体真正站得住"的世界点(避免落在角色宽度 + // 站不住的崖沿被移动层夹停)。与 SetRandomDestinationOnCurrentSegment 同源:endMargin 传 agent 半宽+ + // 落崖检测余量。段短于 2*endMargin → 退化到中点。 + // + // maxSnapDistance = 允许"点离最近段"的最大真实距离(m),语义同 PathBerserker2dSettings.closestToSegmentMaxDistance + // ("想多大就多大")。注意 TryMapPoint 传入的是 AABB 边长、可靠搜索半径只有其一半(见 TryMapPointSurface 的 + // queryAABB=pointMapDistance/2),故这里传 2*maxSnapDistance 以保证"点在 maxSnapDistance 内一定被吸附"。 + // 只要目标离某条烘焙段不超过 maxSnapDistance,就一定映射得到——不会因固定小半径漏掉离地稍远的点。 /// - /// Maps to the NEAREST walkable segment (same robustness as - /// PB2d's own goal mapping in UpdatePath — search distance scales with agent→target distance), - /// then insets by from BOTH segment ends so the returned point is - /// one a bodied agent can actually stand on (not clamped at a ledge). A segment shorter than - /// 2*endMargin degenerates to its midpoint. Returns false only if the graph has no segment at all. + /// Maps to the NEAREST walkable segment within + /// (same mechanism PB2d uses to map an arbitrary point such as a + /// mouse click to the graph — cf. closestToSegmentMaxDistance), then insets by + /// from BOTH segment ends so the returned point is one a bodied agent can + /// actually stand on (not clamped at a ledge). A segment shorter than 2*endMargin degenerates to its + /// midpoint. Returns false only if no baked segment lies within maxSnapDistance of the target. /// - public bool TryGetStandablePointNear(Vector2 worldTarget, float endMargin, out Vector2 standable) + public bool TryGetStandablePointNear(Vector2 worldTarget, float maxSnapDistance, float endMargin, out Vector2 standable) { - // 与 UpdatePath 映射目标同款:半径随距离放大,保证图上有段就映射得到最近点。 - float searchDist = Vector2.Distance(Position, worldTarget) + 0.1f; - if (!PBWorld.TryMapPoint(worldTarget, searchDist, out var ptr)) + // TryMapPoint 的可靠搜索半径 ≈ 传入值/2(broadphase AABB 半边=pointMapDistance/2),故传 2 倍补偿。 + if (!PBWorld.TryMapPoint(worldTarget, maxSnapDistance * 2f, out var ptr)) { standable = worldTarget; return false; diff --git a/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs b/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs index d8ea7eff..daf66248 100644 --- a/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs +++ b/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs @@ -35,6 +35,12 @@ namespace BaseGames.Enemies.Navigation [Tooltip("两次重算最小间隔(s):留出跨帧路径计算完成的时间")] [SerializeField] private float _repathMinInterval = 0.25f; + [Header("目标点吸附")] + [Tooltip("把巡逻/移动目标吸附到最近可行走段时,允许目标离最近段的最大真实距离(m)。语义同 PB2d 的 " + + "closestToSegmentMaxDistance——想多大就多大;只要目标在此距离内一定吸附得到。仅当目标离所有" + + "烘焙段都超过此值(真正脱离导航面)才判失败。")] + [SerializeField] private float _maxSnapDistance = 10f; + // 重寻路防抖状态 private Vector2 _lastPathGoal; private bool _hasPathGoal; @@ -178,12 +184,12 @@ namespace BaseGames.Enemies.Navigation // 目标点吸附:把任意世界目标映射到最近可行走段、并按身体宽度(EdgeSafeMargin)从段两端内缩, // 得到"身体真正站得住"的点。身体宽度是本执行层独占的知识——意图层(AI/Locomotion)只递原始目标。 - // 底层用与 PB2d 自身目标映射同款的距离自适应搜索,只要图上有段就一定吸附得到;返回 false 仅表示图为空。 + // 用 PB2d 映射任意点到导航面的机制(_maxSnapDistance 内一定命中最近段);返回 false 仅表示目标真正脱离导航面。 public bool ResolveStandablePoint(Vector2 target, out Vector2 standable) { if (_navAgent == null) { standable = target; return false; } float margin = _enemyMovement != null ? _enemyMovement.EdgeSafeMargin : 0f; - return _navAgent.TryGetStandablePointNear(target, margin, out standable); + return _navAgent.TryGetStandablePointNear(target, _maxSnapDistance, margin, out standable); } public bool IsNearEdge()