Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.5 KiB
8.5 KiB
敌人寻路受阻处理设计(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 到达判定改为「到达或受阻结算」,删除无限重发 |