Files
zeling_v2/Docs_Dev/superpowers/specs/2026-07-28-enemy-navigation-without-pathfinding-design.md
T

284 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 敌人导航框架(移除寻路库后)设计
> 日期:2026-07-28  状态:设计(待实现)
> 取代:`2026-07-27-nav-offgraph-recovery-design.md`(脱图脱困)——见 §11,该 spec 随本方案作废。
> 关联:`2026-07-27-enemy-body-authority-design.md`(身体几何权威)。
## 1. 背景与决策依据
项目引入了第三方 2D 寻路库(烘焙导航面 + 段 + 连接段 + A*)。评估后决定**移除**,依据:
**本类型横版动作游戏的敌人移动范式**(对照同类作品与本项目敌人名册)几乎只有五类:
固定不动 / 单平台来回 / 飞行直取 / 直线冲锋 / 跳扑。
**地面小怪基本不会为追击而自行规划路径爬上另一个平台**——跨越地形是玩家的玩法,不是敌人的能力;
敌人"够不着"时的表现就是停在边缘。Boss 更是在固定竞技场跑脚本化连段,不需要导航图。
本项目敌人名册与之吻合:固定(植物/晶簇)、飞行(已不用寻路库)、单平台巡逻+攻击、地下潜行(不走导航图)、
天花板倒挂→下落。项目还已有 `EnemyPatrolZone`("地图固定巡逻/追击区域"),本身就是"局部活动"模型。
**已付出的代价(实测)**:21,069 行第三方代码、**4 处对第三方源码的直接修改**(升级即冲突)、
405 行适配层、"不可每帧重算路径"等隐式约束、以及一整类脆弱失败——
**agent 中心一旦离开烘焙段,所有寻路 API 静默返回 false,三种巡逻同时失效**
## 2. 核心洞察:不要重建一个"轻量导航图"
最容易走错的一步是把"导航段"换成自研的"平台可行走区间"——那只是换了个更差的轮子。
**可达性在 `EnemyMovement` 里已经天然存在**
```csharp
MoveHorizontal(dir): if (IsGrounded && WouldBlockAhead(dir)) dir = 0; // 墙 + 悬崖夹紧
```
敌人走不过去的地方,物理上就走不过去。**不需要预先计算平台范围——走过去,被夹停了就是边界。**
这把导航层从"图/段/路径"降维为"朝目标走 + 观察是否被夹停",并且**"脱图"这一状态不复存在**,
连同它引发的整类 bug 一并消失。
## 3. 目标 / 非目标
**目标**
- 用约 200 行的直接移动导航替换 405 行寻路适配层,删除第三方包与对其源码的修改。
- 接口按真实需求瘦身并改名,去掉全部"导航图"概念。
- 三种巡逻策略与追击在新框架下行为合理(走到平台边缘转身=该品类的标准表现)。
- 顺带收敛"卡死检测"的 3 处重复实现。
**非目标**
- 不做跨平台自动寻路(已决策放弃;跨越地形改由**能力**表达,见 §7)。
- 不改 AI 决策层(BrainGraph)与能力层的对外契约。
- 不改 `EnemyMovement` 的物理与探测实现(只增共享原语)。
## 4. 分层与职责
```
BrainGraph AI 决策:进哪个状态 / 触发哪个能力
↓ 声明意图
IEnemyLocomotion 策略:巡逻走法(Wander/Pace/Waypoints)、追击、朝向、步态动画
↓ 请求"去某点"
IEnemyNavigator 机制:地面/飞行两种"怎么去",报告 到达 / 受阻
↓ 写移动意图(PendingInput)
EnemyMovement 物理:velocity、朝向、转身、击退 + 地面/墙/悬崖探测(可达性的真正来源)
↓ 读身体几何
EnemyBase.Body 权威:碰撞体宽高/边缘/脚点
```
每层只依赖下一层。三种巡逻策略**对地面与飞行通用**,差异全部封装在 navigator 实现里。
## 5. 接口设计:`IPathAgent`(20+ 成员) → `IEnemyNavigator`(7 成员)
```csharp
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人导航抽象:把"去某个点"的请求翻译为具体移动方式(地面沿地形走 / 飞行直取),
/// 并报告到达与受阻。不含任何路径图概念——可达性由移动层的墙/悬崖夹紧天然决定。
/// </summary>
public interface IEnemyNavigator
{
/// <summary>
/// 请求朝世界坐标移动。目标被记录后由实现自行逐帧驱动,**重复传入同一目标是幂等的**
/// (调用方每帧调用或只在目标变化时调用皆可)。传入新目标会清除 HasArrived / IsBlocked。
/// </summary>
void MoveTowards(Vector2 target);
/// <summary>停止移动并清除当前目标。</summary>
void Stop();
/// <summary>设置移动速度(巡逻走速 / 追击跑速)。</summary>
void SetSpeed(float speed);
/// <summary>当前是否正朝目标推进(有目标、未到达、未受阻)。</summary>
bool IsMoving { get; }
/// <summary>是否已到达目标(进入停止距离内)。</summary>
bool HasArrived { get; }
/// <summary>是否被地形挡住而无法继续靠近目标(墙 / 悬崖夹停 / 推进停滞)。</summary>
bool IsBlocked { get; }
/// <summary>取一个可供游走的目标点。取不到时返回 false。</summary>
bool TryPickWanderPoint(out Vector2 point);
}
}
```
### 5.1 成员对照(旧 → 新)
| 旧 `IPathAgent` | 新 | 说明 |
|---|---|---|
| `RequestMoveTo` | `MoveTowards` | 语义不变 |
| `StopNavigation` | `Stop` | |
| `IsAtDestination()` | `HasArrived` | 改为属性 |
| `SetSpeed` / `IsMoving` | 同名保留 | |
| `LastMoveObstructed` | `IsBlocked` | 语义不变,`EnemyLocomotion` 的受阻结算逻辑原样可用 |
| `WalkToRandom` / `WalkToRandomOnSegment` | `TryPickWanderPoint` | 合并;不再有"段"概念 |
| `ResolveStandablePoint` | **删除** | 路点摆在崖外时走到边缘被夹停 → `IsBlocked` → 结算推进下一个路点(现有逻辑已覆盖) |
| `TryGetClosestReachable` | **删除** | 被夹停处即最近可达点 |
| `CanReach` | **删除** | 仅调试面板使用,改显示 `IsBlocked` |
| `IsNearEdge` | **删除** | 移动层的 `WouldFallAhead` 才是权威 |
| `IsOnLink` / `CurrentLinkType` / `CurrentLinkStart` / `CurrentLinkEnd` | **删除** | 导航图概念 |
| `OnLinkStarted` / `OnLinkCompleted` / `OnNavPathFailed` / `OnGoalReached` | **删除** | 同上;状态改为轮询属性 |
## 6. 两个实现
### 6.1 `GroundNavigator`(新增,约 120 行,取代 405 行适配层)
`Assets/_Game/Scripts/Enemies/Navigation/`(与飞行实现同处)。
```
MoveTowards(t): 记录目标 t,清 HasArrived / IsBlocked
FixedUpdate:
dx = t.x - Body.Center.x
若 |dx| <= _stoppingDistance → HasArrived = true,停
否则 dir = sign(dx)
若 Movement.WouldBlockAhead(dir) → IsBlocked = true,停
否则 写 PendingInput.MoveDir = dirMoveSpeed = 当前速度 ← 走敌人自身移动
停滞检测(StallDetector)命中 → IsBlocked = true,停
TryPickWanderPoint(out p):
p = 当前 X ± Random(_wanderMinRange, _wanderMaxRange)Y 取当前
返回 true ← 不做地形扫描;走不到自然会被夹停
```
- 只管水平方向;垂直交给重力(与现状一致)。
- 速度经 `PendingInput.MoveSpeed``SetSpeed(0)` 时退化为移动层配置的 `WalkSpeed`
- **不做地形扫描**:Wander 走到平台边缘被夹停即视为本次游走结束,重新挑点——
"走到边缘、停下、转身"正是该品类的标准表现(与 Pace 观感一致)。
### 6.2 `FlyingNavigator`(由现有飞行导航瘦身而来)
现有飞行实现本就不依赖第三方库,只需按新接口裁剪:直接朝目标点飞;
`IsBlocked` 恒 false`TryPickWanderPoint` 在 home 附近的圆内取点。
### 6.3 `NullEnemyNavigator`
测试与"无导航敌人"用的空实现(由现有 `NullPathAgent` 改名瘦身):
`IsMoving/HasArrived/IsBlocked` 均 false`TryPickWanderPoint` 返回 false。
## 7. 巡逻与追击在新框架下
| 策略 | 变化 |
|---|---|
| **Pace** | 逻辑不变(本就不用导航);改用共享 `StallDetector`(§8);**并修复边沿翻向缺陷**(见 §7.1) |
| **Wander** | `TryPickWanderPoint()``MoveTowards()` → **到达或受阻**即进入停顿,随后重挑点 |
| **Waypoints** | `MoveTowards(路点)` → **到达或受阻**即推进下一个;删除吸附步骤(`ResolveStandablePoint` 已移除) |
| **Approach(追击)** | `MoveTowards(玩家位置)`;玩家在够不着处 → 走到边缘 `IsBlocked` → 交由 AI 决策(见 §7.2) |
**同时修掉的既有缺陷**Wander 在"挑点失败"时会退化为永久停顿并播 Idle 动画。
新实现中挑点不依赖导航图、不会失败,该缺陷从根上消失。
### 7.1 Pace 边沿翻向缺陷(独立于本次移除,一并修复)
现状 `if (blocked && !_paceBlockedPrev)` 为边沿触发:`blocked` 持续为真时永不再翻,
敌人会永久顶着障碍/崖沿,即便反方向畅通。实测可复现(`_paceDir=-1`、当前受阻、反方向不受阻)。
```csharp
bool oppositeFree = mv != null && !mv.WouldBlockAhead(-_paceDir);
if (blocked && (!_paceBlockedPrev || oppositeFree)) { _paceDir = -_paceDir; ... }
```
保留"两侧都堵时只翻一次即停住、不来回抖"的原意图,同时让"顶着堵侧而另一侧通畅"能掉头。
### 7.2 跨平台移动 = 能力,不是导航
**"够不着"由 navigator 报告,"要不要过去"由 AI 决策,"怎么过去"由能力实现。**
```
Approach 态 → IsBlocked 且玩家不在同一可行走范围
→ AI 可选:① 停在边缘警戒/放弃(品类默认) ② 触发跳扑类能力
```
`INavLinkHandler``EnemyMovement.JumpToTarget` **保留**(本就是自有抽象,不含第三方类型),
供将来实现跳扑能力时复用。
## 8. 共享原语:`StallDetector`(收敛 3 处重复)
"窗口内净位移不足即判停滞"目前在 3 处各写一遍,连时间单位都不一致:
| 位置 | 现参数 |
|---|---|
| `EnemyLocomotion` Pace | 12 **帧** / 0.05m |
| 寻路适配层 | 0.5 **秒** / 0.05m |
| `RushAbility` | 0.15 **秒** / 0.05m |
收敛为移动层旁的纯逻辑结构(`BaseGames.Enemies` 命名空间,三处消费者均可见):
```csharp
/// <summary>推进停滞检测:滑动窗口内净水平位移不足阈值即判停滞。调用方持有实例并逐帧 Tick。</summary>
public struct StallDetector
{
public StallDetector(float windowSeconds, float minProgress);
/// <summary>以当前位置重开窗口(开始移动 / 换目标 / 判定后调用)。</summary>
public void Reset(float currentX);
/// <summary>推进一帧;返回 true 表示本窗口内推进不足(停滞)。返回 true 后窗口自动重开。</summary>
public bool Tick(float currentX, float deltaTime);
}
```
三处改为使用它。**Pace 由帧计数改为时间**:12 帧 @60fps ≈ 0.2s,故 Pace 取 `windowSeconds = 0.2f` 以保持观感。
## 9. 消费点与改动清单
### 9.1 需要改动的消费者
| 文件 | 改动 |
|---|---|
| `EnemyBase.cs` | `_nav` / `Nav` 类型改为 `IEnemyNavigator``GetComponent<IEnemyNavigator>() ?? new NullEnemyNavigator()` |
| `Navigation/EnemyLocomotion.cs` | Wander/Waypoints 改用新接口;Pace 缺陷修复 + `StallDetector`;删除吸附调用 |
| `Editor/Enemies/EnemyLocomotionEditor.cs` | 调试面板改显示 `IsMoving/HasArrived/IsBlocked`;删除连接段与"吸附距离"显示、`CanReach` 诊断 |
| `Abilities/RushAbility.cs` | 内联停滞检测改用 `StallDetector` |
| `FlyingEnemy.cs` / `IPathAgent.cs` 等注释 | 更新措辞 |
> ⚠ **同名陷阱**`UI/MainMenu/DataDrivenMainMenuController.cs` 里的 `Nav` 是 **UI 导航栈**,与敌人导航无关,**不得改动**。
### 9.2 删除清单
| 删除 | 备注 |
|---|---|
| 第三方寻路包整目录(21,069 行 / 112 文件) | 含我们对其源码的 4 处修改 |
| `Navigation/EnemyNavAgent.cs`(405 行) | 唯一运行时耦合点 |
| `Editor/Enemies/Navigation/NavSurfaceBakeShortcut.cs` | 烘焙快捷工具 |
| `Editor/Scene/SceneObjectPlacerTool.cs` / `SceneScaffoldTools.cs` 中的相关用法 | 放置导航组件/导航面的代码 |
| 2 处 asmdef 对第三方包的引用 | `BaseGames.Editor``BaseGames.Enemies.Navigation` |
| 场景/预制体上的导航组件与导航面物体 | `TestRoomA` 场景、`ENM_ChaoFeng` 预制体 |
| `IPathAgent.cs` 中 13 个导航图成员 | 见 §5.1 |
**净变化**:删除约 21,500 行,新增约 200 行。
## 10. 边界与错误处理
| 情形 | 行为 |
|---|---|
| 目标在另一平台/崖对面 | 走到边缘 → `IsBlocked` → 巡逻结算推进 / 追击交 AI 决策。**不再有"脱图"失效** |
| 目标在正上方够不着 | 同上(水平已到达则 `HasArrived`;AI 可据玩家高度决定是否跳扑) |
| 敌人被击退/冲锋到崖沿悬空 | 无导航图 → 无失效;移动层夹紧仍生效,敌人可正常走回。`RushAbility` 的落位收敛保持不变 |
| 敌人无 navigator 组件 | `NullEnemyNavigator` 兜底(不移动),与现状一致 |
| 飞行敌人 | 走 `FlyingNavigator`,不受地形夹紧影响 |
| Boss(竞技场脚本化战斗) | **已核实:Boss 代码完全不引用导航**(无 `.Nav`/`IPathAgent`/`MoveTo`/`Locomotion`),其预制体上的导航组件为脚手架顺带添加的冗余件,直接移除即可 |
## 11. 作废的先前设计
`2026-07-27-nav-offgraph-recovery-design.md`(导航脱图统一脱困)**随本方案作废**:
其解决的"agent 脱离烘焙段导致寻路全面失效"问题,在移除导航图后不再存在。
该 spec 中**唯一仍然有效的部分是 Pace 边沿翻向修复**,已并入本 spec §7.1。
## 12. 测试
**EditMode 单测**(纯逻辑,无场景依赖,沿用既有范式):
- `StallDetector`:窗口内推进足够→false;推进不足→true 且窗口重开;`Reset` 行为。
- `GroundNavigator` 的判定纯函数:到达判定 `Arrived(currentX, targetX, stopDist)`
游走点取值落在 `[minRange, maxRange]` 区间内且方向随机。
- Pace 翻向:`ShouldFlipPace(blocked, blockedPrev, oppositeFree)` 四类组合。
**回归**:现有 EditMode 全套(当前 191 条)必须全绿。
**PlayMode 验收**`TestRoomA`):
1. 三种巡逻各自正常:Wander 走到边缘停下重挑、Pace 正常来回、Waypoints 依次推进;
2. 追击(ApproachAttack):玩家同平台可追到并攻击;玩家跳到够不着处 → 敌人停在边缘(不再卡死/原地踏步);
3. 冲锋(RushAbility):越过锁定点即停、可冲下悬崖、收尾不悬空;
4. 对照修复前记录的缺陷:Wander 不再播 Idle 卡死;Pace 顶着崖沿能掉头。
## 13. 迁移阶段(每阶段独立可回退,均过编译门 + 191 回归)
| 阶段 | 内容 | 风险 |
|---|---|---|
| **P0** | 单独修复 Pace 边沿翻向缺陷 + 引入 `StallDetector` 并收敛 3 处(此时寻路库仍在) | 低 |
| **P1** | 新增 `GroundNavigator` + `IEnemyNavigator` 接口与单测,**不动现有接线** | 极低 |
| **P2** | 消费者切换到新接口(`EnemyBase`/`EnemyLocomotion`/编辑器面板),场景敌人换成 `GroundNavigator`PlayMode 验收 | **中**(行为对比重点) |
| **P3** | Boss 预制体移除冗余导航组件(已核实其代码不用导航,纯删除) | 低 |
| **P4** | 删除 `EnemyNavAgent`、编辑器中的相关用法、asmdef 引用、场景导航面物体 | 低 |
| **P5** | 删除第三方包整目录 | 低 |
P2 是唯一需要重点比对行为的阶段;P4/P5 执行时已无人引用。