284 lines
15 KiB
Markdown
284 lines
15 KiB
Markdown
# 敌人导航框架(移除寻路库后)设计
|
||
|
||
> 日期: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 = 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
|
||
/// <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 执行时已无人引用。
|