docs(enemy): 寻路逼近+攻击选择器设计(感知管交战/招式自管射程/两选招模式)

This commit is contained in:
2026-07-24 11:21:35 +08:00
parent 4a80a11ab5
commit c33ce55f13
@@ -0,0 +1,215 @@
# 敌人寻路逼近 + 攻击选择器 设计
> 日期:2026-07-24  状态:设计(待实现)
> 关联:`2026-07-23-enemy-pathfind-obstruction-handling-design.md`(受阻处理)、
> `2026-07-21-enemy-ability-config-to-so-design.md`(能力参数入 SO)、
> `2026-07-03-enemy-ai-framework-design.md`BrainGraph HFSM)。
## 1. 背景与目标
### 1.1 现状
- **导航层**`EnemyNavAgent` / PathBerserker2d)已完整:`RequestMoveTo` 自带重寻路防抖、受阻检测、
改道到最近可达点、NavLink(跳/落/爬)委托能力执行。
- **执行层**`EnemyLocomotion`)有 `LocomotionMode.Approach``MoveTo(Vector2)`,但目前没有任何
AI 状态真正用寻路做持续逼近。
- **决策层**`PerceptionStateMachine`)的 `Chase` 现在只能挂**单个** `ChaseAbilityId`。E001 用的
`ContactChaseAbility` 是"朝玩家一侧速度直冲、撞墙即停"的 committed 冲锋,**完全不走寻路**
被中间隔着的平台/沟拦住就停。
- **感知层**`PhysicsPerceptionSystem`)槽位驱动,`EnemyBase` 把槽位翻译成 `ISensor` 语义供 AI 读。
### 1.2 目标(本设计范围)
新增一种与"冲锋(Chase)"并列的交战范式 —— **寻路逼近(Approach)+ 到攻击范围内选招攻击**
1. 敌人沿寻路路径(含跳/落/爬 NavLink)**绕过地形持续逼近玩家**,而非直线冲。
2. 进入某招的攻击范围后,**按攻击策略从"够得着且未冷却"的候选里选一个攻击**。
3. **攻击时暂停逼近**;攻击结束回到逼近,重判距离/重选招,形成 逼近 → 攻击 → 重定位 → 攻击 的闭环。
冲锋范式(`ContactChaseAbility`)保持不变,两种范式共存、按敌人选用。
### 1.3 非目标
- 不改冲锋(Chase)范式的行为。
- 不做矩形/扇形攻击范围(本期仅圆形距离判定,`PerceptionProbe` 化留作后续扩展)。
- 不做多目标/仇恨列表(沿用现有单一玩家目标)。
- 不新建敌人预制体(预制体已延后,见 memory `enemy_prefabs_deferred`)。
## 2. 职责边界(核心原则)
| 层级 | 归属组件 | 负责 |
|---|---|---|
| **敌人级交战状态感知** | `PhysicsPerceptionSystem` | 玩家是否脱战 / 进警戒 / 进追逐等**敌人级**状态(`aggro`/`alert`/`los`/`sight` 槽) |
| **招式级攻击触发范围** | **各攻击能力自身** | 本招"够不够得着"——射程内聚在招自己,与动画/HitBox/冷却/权重同处 |
| **交战决策**(逼近↔攻击、选招) | AI 状态机(`PerceptionStateMachine` + `AiScript`) | 只决定进哪个态/转换条件/何时选招;不做位移、不做选招计算 |
| **逼近位移** | `EnemyLocomotion` | 只执行寻路逼近(新增 `Pursue` |
| **选招计算** | `EnemyAttackSelector` | 过滤候选(射程+冷却+LOS+着地)→ 按 mode 选一个;纯逻辑、可单测 |
| **攻击执行** | 各攻击能力 | 自己的动画/HitBox/朝向/停移 |
> 关键:**交战入口仍由感知系统决定**(`aggro` 槽 → `InChaseZone()` → 进 Approach);
> **"某招够不够得着"下沉到招自身**(编辑内聚:改一个招只看一个组件,不再跨到中心感知配槽、不再对名字)。
## 3. 数据流
```
感知系统(aggro/los/sight 槽) ──> ISensor ──> PerceptionStateMachine
│(是否脱战/警戒/追逐) │决策
▼ ▼
InChaseZone()=true ──> Approach态 Attack态
│Tick │OnEnter
Locomotion.Pursue(LastKnown) Combat.UseBestAttack()
│(寻路,RunSpeed) │
▼ ▼
EnemyBase.MoveTo ─> Nav(防抖/受阻/NavLink) EnemyAttackSelector
(各候选自答 InAttackRange)
选中招.Execute()(自驱动画/HitBox/停移)
```
- **Approach → Attack**`Combat.HasEligibleAttack()`(有招够得着且未冷却)。
- **Attack → Approach**`!Combat.IsAbilityRunning()`(打完回逼近重判)。
- **任意态 → Rest**:脱离全部感知区(`!InChaseZone && !InVisionZone`)。
## 4. 组件设计
### 4.1 `EnemyAbilitySO` 新增字段(选招/射程元数据,基类通用)
```csharp
[Header("调度分类与选招")]
public AbilityCategory category = AbilityCategory.None; // None/Attack/Movement/Utility
[Min(0f)] public float weight = 1f; // 权重随机用
[Header("攻击触发范围(招式自管,圆形)")]
[Min(0f)] public float rangeRadius = 0f; // 0 = 未配置(category==Attack 时 Awake 报错)
public Vector2 rangeOffset = Vector2.zero; // 相对敌人,X 随朝向翻转
// 复用现有:requiresLineOfSight / requiresGrounded / priority / cooldown
```
- `AbilityCategory` 新增枚举(`BaseGames.Enemies.Abilities`)。选择器只收 `category == Attack`
- 旧的 `preferredMinRange/preferredMaxRange` 降级为纯编辑器提示(不再参与选招),后续可删。
### 4.2 攻击能力:自答"够不够得着"
`EnemyAbilityBase` 增加:
```csharp
/// <summary>本招攻击触发范围内是否有玩家(圆形距离判定,招式自管其射程)。</summary>
public virtual bool InAttackRange()
{
if (_config == null || _config.rangeRadius <= 0f || _enemy?.PlayerTransform == null) return false;
float sign = _transform.localScale.x < 0f ? -1f : 1f;
Vector2 origin = (Vector2)_transform.position
+ new Vector2(_config.rangeOffset.x * sign, _config.rangeOffset.y);
float r = _config.rangeRadius;
return ((Vector2)_enemy.PlayerTransform.position - origin).sqrMagnitude <= r * r;
}
```
- LOS 复用敌人级 `EnemyBase.IsPlayerVisible()`(不在招上单独打射线)。
- 无物理 Overlap(有玩家 Transform,距离判定即可),零 GC、零额外物理查询。
### 4.3 `EnemyAttackSelector``EnemyBase` 持有,类比 `EnemyAbilityRegistry`
- **收集**`EnemyBase.Awake``Abilities.CollectFrom` 后,从 `Abilities.All``Config.category == Attack`
的能力为候选。
- **可单测抽象**:选择器作用于候选接口,不直连 MonoBehaviour
```csharp
public interface IAttackCandidate {
bool CanUse { get; } // 未冷却+未运行+存活
bool InAttackRange(); // 招式自管射程
bool RequiresGrounded { get; }
float Weight { get; }
int Priority { get; }
bool Execute();
}
```
`EnemyAbilityBase`(攻击类)实现之。
- **API**
```csharp
bool HasEligible(bool hasLOS, bool grounded);
IAttackCandidate Select(bool hasLOS, bool grounded, AttackSelectionMode mode); // 无合格→null
```
- **单招合格判定**`c.CanUse && c.InAttackRange() && (!requiresLOS(此招) || hasLOS)
&& (!c.RequiresGrounded || grounded)`。
- `requiresLineOfSight` 读各招 `Config``hasLOS = enemy.IsPlayerVisible()``grounded` 由 `EnemyBase` 提供。
- **选招(AttackSelectionMode**
- `WeightedRandom`:合格集内按 `Weight` 加权随机(`UnityEngine.Random`)。
- `Priority`:取 `Priority` 最高;并列取候选列表首个(确定性)。
- **根因校验**:收集后若某 `category==Attack` 候选 `rangeRadius<=0` → `LogError`(永远够不着的静默 bug 暴露);
若敌人配了 ApproachAttack 但候选集为空 → `LogError`。
### 4.4 `EnemyStatsSO` 新增
```csharp
public AttackSelectionMode attackSelectionMode = AttackSelectionMode.WeightedRandom;
```
`AttackSelectionMode { WeightedRandom, Priority }``BaseGames.Enemies` 或 `.Abilities`)。
### 4.5 `ICombatant` 扩展(`EnemyBrainContext` 实现)
```csharp
bool HasEligibleAttack(); // = selector.HasEligible(enemy.IsPlayerVisible(), enemy.Movement.IsGrounded)
bool UseBestAttack(); // sel=selector.Select(...); return sel != null && sel.Execute();
```
> 着地读 `EnemyBase.Movement.IsGrounded``EnemyMovement` 已暴露);如需可在 `EnemyBase` 加一层便捷只读属性。
- 动态量(LOS/着地/选招模式)在 `EnemyBrainContext` 侧采集后委托选择器,选择器保持无敌人耦合。
### 4.6 `IEnemyLocomotion` 扩展
```csharp
void Pursue(Vector2 target); // 寻路逼近原语:设 Approach 模式 + RunSpeed + 每帧 Nav.RequestMoveTo(target)
```
- Vector2 驱动(不耦合 Transform),底层已防抖/受阻/NavLink。
- 现有 `Approach(Transform)` 可委托 `Pursue(target.position)`。
### 4.7 `PerceptionStateMachine`:新增 ApproachAttack 交战风格(opt-in
`Config` 新增:
```csharp
public EngagementStyle Engagement = EngagementStyle.ChaseAbility; // ChaseAbility(现状) | ApproachAttack(新)
public string ApproachState = "Approach";
public string AttackState = "Attack";
```
`EngagementStyle == ApproachAttack` 时,把原单一 Chase 能力节点换成两态子机(`ChaseAbility` 风格拓扑不变):
- **Idle/Patrol/Alert** → `Approach` When `canChase`(沿用现有 `InChaseZone` / 冷却门条件)
- **Approach**Locomotion 驱动)
- `OnEnter/Tick``Locomotion.Pursue(Sensor.LastKnown)`
- `OnExit``Locomotion.Stop()`
- → `Attack` When `Combat.HasEligibleAttack()`
- → `Rest` When `!InChaseZone && !InVisionZone`
- **Attack**(能力驱动,无固定 id
- `OnEnter``Locomotion.Stop()` + `Combat.UseBestAttack()`(攻击时暂停逼近)
- → `Approach` When `!Combat.IsAbilityRunning()`(打完回逼近重判/重选招)
- → `Rest` When `!InChaseZone && !InVisionZone`(打完且脱离感知区)
- **Rest** → `Patrol`(不变)
- 全局 `Died` → `Death`(不变)
## 5. 边界与错误处理
| 情形 | 行为 |
|---|---|
| 无 NavLink 隔沟够不到玩家 | 走现有受阻逻辑 → 停在最近可达点等待(仍在 chase 区,Approach 态保持) |
| 配了 ApproachAttack 但无攻击候选 / 招 `rangeRadius<=0` | `Awake` 显式 `LogError`(根因暴露,不静默兜底,CLAUDE.md §6 |
| 玩家走出所有招射程 | 无合格攻击 → 持续逼近 |
| 攻击被受击打断 | 能力中断 → Attack.Tick 见 `!IsAbilityRunning` → 回 Approach |
| 玩家脱离感知区 | `!InChaseZone && !InVisionZone` → Rest → Patrol |
| 关闭 Domain Reload | 选择器无静态态;`UnityEngine.Random` 运行时安全(见 memory `domain_reload_disabled` |
## 6. 测试
### 6.1 EditMode 单测(`EnemyAttackSelector`
用假候选(实现 `IAttackCandidate`+ 复用 `Assets/Tests/EditMode/AI/Fakes` 范式:
- 合格过滤:冷却中 / 射程外 / 无 LOS(招 requiresLOS 且 hasLOS=false/ 需着地但腾空 → 各自被排除。
- `Priority` 模式:多招合格取最高 `Priority`,并列取首个(确定性断言)。
- `WeightedRandom` 模式:仅从合格集选(多次采样断言从不选到不合格招);单一合格招 → 必选它(确定性)。
- 空候选 / 全不合格 → `Select` 返回 null、`HasEligible` 为 false。
### 6.2 手动 / PlayMode
- 一个 `EngagementStyle=ApproachAttack` 的测试敌人(TestRoomA 内临时搭,或改 ChaoFeng):
跨平台缺口(跳 NavLink)逼近玩家 → 到某招射程停 → 选招攻击 → 打完重定位 → 玩家逃出感知区回巡逻。
- 验证攻击射程 Gizmo 与实际触发一致。
## 7. 脚手架 / 规范合规(CLAUDE.md §2/§6
- 无新资产类型(仅 `EnemyAbilitySO`/`EnemyStatsSO` 加字段)。
- 能力/角色向导需按能力类型设 `category`(攻击类→`Attack`)与默认 `weight`/`rangeRadius`——列入实现计划。
- 攻击能力在编辑器画自身射程 Gizmo(`rangeRadius`+`rangeOffset`,随朝向翻转),设计师摆招即见。
- 自检工具(`SOValidationRunner`)加一条:`category==Attack` 必须 `rangeRadius>0`。
## 8. 涉及文件(预估)
- 改:`EnemyAbilitySO.cs`+category/weight/rangeRadius/rangeOffset)、`EnemyStatsSO.cs`+attackSelectionMode)、
`EnemyAbilityBase.cs`+InAttackRange/IAttackCandidate 实现)、`EnemyBase.cs`+选择器持有/IsGrounded 暴露)、
`EnemyBrainContext.cs`+HasEligibleAttack/UseBestAttack)、`ICombatant.cs`、`IEnemyLocomotion.cs`+Pursue)、
`EnemyLocomotion.cs`+Pursue 实现)、`PerceptionStateMachine.cs`+ApproachAttack 拓扑)。
- 新:`AbilityCategory.cs`、`AttackSelectionMode.cs`、`IAttackCandidate.cs`、`EnemyAttackSelector.cs`、
`EnemyAttackSelectorTests.cs`EditMode)。
- 编辑器:攻击能力射程 Gizmo;能力向导 category/range 设置;`SOValidationRunner` 校验项。