diff --git a/Docs_Dev/superpowers/specs/2026-07-28-enemy-navigation-without-pathfinding-design.md b/Docs_Dev/superpowers/specs/2026-07-28-enemy-navigation-without-pathfinding-design.md new file mode 100644 index 00000000..f08eeb64 --- /dev/null +++ b/Docs_Dev/superpowers/specs/2026-07-28-enemy-navigation-without-pathfinding-design.md @@ -0,0 +1,283 @@ +# 敌人导航框架(移除寻路库后)设计 + +> 日期: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 +{ + /// + /// 敌人导航抽象:把"去某个点"的请求翻译为具体移动方式(地面沿地形走 / 飞行直取), + /// 并报告到达与受阻。不含任何路径图概念——可达性由移动层的墙/悬崖夹紧天然决定。 + /// + public interface IEnemyNavigator + { + /// + /// 请求朝世界坐标移动。目标被记录后由实现自行逐帧驱动,**重复传入同一目标是幂等的** + /// (调用方每帧调用或只在目标变化时调用皆可)。传入新目标会清除 HasArrived / IsBlocked。 + /// + void MoveTowards(Vector2 target); + /// 停止移动并清除当前目标。 + void Stop(); + /// 设置移动速度(巡逻走速 / 追击跑速)。 + void SetSpeed(float speed); + + /// 当前是否正朝目标推进(有目标、未到达、未受阻)。 + bool IsMoving { get; } + /// 是否已到达目标(进入停止距离内)。 + bool HasArrived { get; } + /// 是否被地形挡住而无法继续靠近目标(墙 / 悬崖夹停 / 推进停滞)。 + bool IsBlocked { get; } + + /// 取一个可供游走的目标点。取不到时返回 false。 + 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 = dir;MoveSpeed = 当前速度 ← 走敌人自身移动 + 停滞检测(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 +/// 推进停滞检测:滑动窗口内净水平位移不足阈值即判停滞。调用方持有实例并逐帧 Tick。 +public struct StallDetector +{ + public StallDetector(float windowSeconds, float minProgress); + /// 以当前位置重开窗口(开始移动 / 换目标 / 判定后调用)。 + public void Reset(float currentX); + /// 推进一帧;返回 true 表示本窗口内推进不足(停滞)。返回 true 后窗口自动重开。 + 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() ?? 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 执行时已无人引用。