# 敌人寻路逼近 + 攻击选择器 设计 > 日期: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` 校验项。