# 敌人寻路受阻处理设计(Waypoints 巡逻 + MoveTo 一次性目标) > 日期:2026-07-23 > 状态:设计已批准,待写实现计划 > 范围:**仅**走 PathBerserker2d 寻路的移动——Waypoints 巡逻、一次性 `MoveTo(point)`。 > 不在本次:Approach 追击(当前未接线且真实追击 `ContactChaseAbility` 走速度冲刺、不经寻路)。 ## 1. 问题 在巡逻 waypoint 之间放一个障碍物时,敌人会「对着障碍物一直空推」,不会智能寻路到最近可到达的点。根因是受阻有**两种成因**,当前都没被正确处理: - **成因 A:障碍已进 NavGraph**(已烘焙/未来软障碍)。寻路失败(`NoPathFromStartToGoal`),`NavAgent.OnFailedToFindPath` → `EnemyNavAgent.OnNavPathFailed` 触发,但**全项目无人订阅**(已 grep 确认)。`EnemyLocomotion` 的 Waypoints 分支在 nav 空闲且未到达时**无限重发同一不可达点**(`EnemyLocomotion.cs:207-213`)→ 死循环。 - **成因 B:障碍未进 NavGraph**(当前裸摆的 Collider)。寻路**成功**返回一条穿墙直线,`Nav.IsMoving=true`,敌人被物理碰撞体夹停、永远到不了目标、既不「到达」也不「失败」→ 纯物理空推。 > 说明:成因 A 的「让障碍进图」是另一项工作(Tier-1 软障碍 / 重烘焙),本设计只负责「受阻后敌人如何正确反应」。为使本次独立可测(对当前裸障碍即成因 B 立即见效),**必须同时覆盖 A 与 B**。 ## 2. 目标行为 当一次寻路移动无法到达目标时,敌人应**寻路到最近的可到达点**,而非对着障碍空推: - **Waypoints 巡逻**:走到最近可达点 → 短暂停顿 → 推进到下一个 waypoint 继续巡逻。(拿不到更近点则直接视为「到达受阻点」→ 停顿 → 推进。) - **MoveTo 一次性目标**:尽力走到最近可达点并停住(best-effort reach),不空推。 ## 3. 架构与职责划分 三层,职责清晰、可独立理解与测试: ### 3.1 PB2D `NavAgent.cs`(vendored 源码)— 暴露既有的「最近可达点」 寻路失败时 `Pathfinder` 已把最近可达点写进 `PathRequest.closestReachablePosition`(`Pathfinder.cs:79`、`UpdateClosestPositionToGoal:325`),但该字段在 PB2D 程序集内部,外部拿不到。 - 新增字段 `private NavSegmentPositionPointer _lastClosestReachable = NavSegmentPositionPointer.Invalid;` - 在**两处** `OnFailedToFindPath` 触发前赋值该字段: - `HandlePathRequest()` 的 Failed 分支(`currentPathRequest` 失败,`:947-969`) - `Repath()` 的 Failed 分支(`repathPathRequest` 失败,`:922-931`) - 仅当 `FailReason == NoPathFromStartToGoal` 且 `closestReachablePosition` 有效时赋值,否则置 Invalid。 - 新增 `public bool TryGetClosestReachablePosition(out Vector2 worldPos)`:读 `_lastClosestReachable`,无效返回 false。 - **不改动任何现有逻辑**,纯暴露既有数据。约 12 行。 ### 3.2 `EnemyNavAgent.cs`(寻路执行层)— 通用「受阻检测 + best-effort 改道」 这是**可复用**的核心,将来「基于寻路的追击」直接复用。 - **卡死检测(覆盖成因 B)**:当 `Nav.IsMovingOnSegment` 时,用滑动窗口测「到 `PathSubGoal` 的距离是否在 ~0.5s 内几乎未减小」(复用 Pace 已验证的净位移窗口思路,`EnemyLocomotion.cs:169-175`)。无进展即判「卡死」。 - **寻路失败(覆盖成因 A)**:`OnNavPathFailed` 已存在,接入统一「受阻」判定。 - **统一受阻处理**:受阻 = 卡死 ∪ 寻路失败。触发后: - `TryGetClosestReachable(out p)` 拿到有效、且明显比当前位置更靠近目标的点、且本轮尚未改道 → 自动 `UpdatePath(p)` 一次(走向最近点),置 `LastMoveObstructed=true`; - 拿不到更近点(已在最近点附近 / 无最近点 / 成因 B 无 closest 数据)→ `Stop()`,置 `LastMoveObstructed=true`。 - **对外暴露**: - `bool LastMoveObstructed { get; }`(下次对**新目标** `RequestMoveTo` 时复位)。 - `IPathAgent` 新增 `bool TryGetClosestReachable(out Vector2 worldPos)`(转发 `RawNavAgent.TryGetClosestReachablePosition`)。 - **效果**:所有走寻路的 `MoveTo` 天然变为「尽力到最近点、绝不空推」;一次性 `MoveTo` 无需额外策略即正确。 ### 3.3 `EnemyLocomotion.cs`(意图层)— 仅 Waypoints 推进策略 - Waypoints 到达判定改为:**到达 `_wpEffectiveGoal`** 或 **(`Nav.LastMoveObstructed` 且 `Nav` 空闲)**。 - 后者即:执行层已把敌人带到最近可达点并停住 → 视为「本路点结算」。 - 结算 → 停顿(复用 Wander 的 `_wanderPauseMin/Max`)→ `AdvanceWaypoint()` 推进下一个点。**删除对同一不可达点的无限重发**(`:207-213`)。 - 保留「刚出生尚未映射到导航图」的瞬态重发(防抖),但只在 `Nav` 空闲、未到达、**且未受阻**时。 ### 3.4 `IPathAgent.cs` 及其实现 - 接口新增 `bool TryGetClosestReachable(out Vector2 worldPos)` 与 `bool LastMoveObstructed { get; }`。 - `FlyingDirectNavigator` 与空实现:`TryGetClosestReachable` 返回 false、`LastMoveObstructed` 返回 false(飞行敌走直线,无此概念)。 ## 4. 数据流 ``` EnemyLocomotion.TickPatrol (Waypoints) └─ SetWaypointGoal(): ResolveStandablePoint → _wpEffectiveGoal → MoveTo └─ EnemyNavAgent.RequestMoveTo(target) [复位 LastMoveObstructed] └─ NavAgent.UpdatePath(target) → 异步 A* ├─ 成功: 跟随路径(FixedUpdate 朝 PathSubGoal 走) │ └─ 卡死检测(窗口无进展) ─┐ └─ 失败(NoPath): OnFailedToFindPath ─┤ ▼ EnemyNavAgent 统一受阻处理 ├─ 有更近点 → UpdatePath(closest), LastMoveObstructed=true └─ 无更近点 → Stop(), LastMoveObstructed=true └─ 到达 _wpEffectiveGoal 或 (LastMoveObstructed && Nav 空闲) └─ 停顿 → AdvanceWaypoint() ``` ## 5. 边界与错误处理 - **最近点 ≈ 当前位置**(障碍就在脚下):执行层 `Stop()` → Nav 立即空闲 → Waypoints 结算推进。不空推。 - **无导航图 / 映射失败**:`TryGetClosestReachable` 返回 false → Waypoints 结算推进;MoveTo 停。 - **不误判瞬态**:Pending 期间只累计卡死窗口、`OnNavPathFailed` 不误触发;卡死窗口仅在 `IsMovingOnSegment` 时累计。 - **防抖动**:改道后 `LastMoveObstructed=true` 抑制本轮重复改道,等 Nav 结算;对**新目标**的 `RequestMoveTo` 才复位。 - **根因合规(CLAUDE.md §6)**:卡死检测是「AI 对无法推进的正确反应」,是真实行为而非兜底掩盖;`TryGetClosestReachablePosition` 是暴露既有数据而非改逻辑。让障碍进图仍是另一项独立工作。 ## 6. 扩展点(本次不实现,但预留) 将来「基于寻路的追击」接线 `IEnemyLocomotion.Approach(Transform)` → 每帧 `MoveTo(玩家位置)` 时,因受阻处理在 `EnemyNavAgent` 执行层,追击**自动获得** best-effort 贴近最近可达点、绝不空推的行为,无需改动本次代码。 ## 7. 验证方式(Play 模式,经 Unity MCP) - **成因 B(当前裸障碍)**:两 waypoint 间摆 Collider → 敌人走到障碍前、停顿、推进下一个点(不再空推)。 - **成因 A**(待软障碍/烘焙障碍就绪后复测):寻路失败 → 走最近点分支同样结算推进。 - **MoveTo 一次性**:目标被挡 → 敌人停在最近可达点、不空推。 - **回归**:无障碍时 Waypoints/Wander/Pace 巡逻与既有行为一致。 ## 8. 受影响文件清单 | 文件 | 改动 | |---|---| | `Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs` | 加 `_lastClosestReachable` 字段 + 两处赋值 + `TryGetClosestReachablePosition` | | `Assets/_Game/Scripts/Enemies/IPathAgent.cs` | 接口加 `TryGetClosestReachable` / `LastMoveObstructed`;空实现返回 false | | `Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs` | 卡死检测 + 统一受阻处理 + best-effort 改道 + 暴露状态 | | `Assets/_Game/Scripts/Enemies/Navigation/FlyingDirectNavigator.cs` | 实现新接口成员(返回 false) | | `Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs` | Waypoints 到达判定改为「到达或受阻结算」,删除无限重发 |