From c33ce55f137f554965ecf186d5cf499cdc94f139 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Fri, 24 Jul 2026 11:21:35 +0800 Subject: [PATCH] =?UTF-8?q?docs(enemy):=20=E5=AF=BB=E8=B7=AF=E9=80=BC?= =?UTF-8?q?=E8=BF=91+=E6=94=BB=E5=87=BB=E9=80=89=E6=8B=A9=E5=99=A8?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1(=E6=84=9F=E7=9F=A5=E7=AE=A1=E4=BA=A4?= =?UTF-8?q?=E6=88=98/=E6=8B=9B=E5=BC=8F=E8=87=AA=E7=AE=A1=E5=B0=84?= =?UTF-8?q?=E7=A8=8B/=E4=B8=A4=E9=80=89=E6=8B=9B=E6=A8=A1=E5=BC=8F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-07-24-enemy-nav-approach-attack-design.md | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 Docs_Dev/superpowers/specs/2026-07-24-enemy-nav-approach-attack-design.md diff --git a/Docs_Dev/superpowers/specs/2026-07-24-enemy-nav-approach-attack-design.md b/Docs_Dev/superpowers/specs/2026-07-24-enemy-nav-approach-attack-design.md new file mode 100644 index 00000000..528017ff --- /dev/null +++ b/Docs_Dev/superpowers/specs/2026-07-24-enemy-nav-approach-attack-design.md @@ -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 +/// 本招攻击触发范围内是否有玩家(圆形距离判定,招式自管其射程)。 +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` 校验项。