13 KiB
13 KiB
敌人寻路逼近 + 攻击选择器 设计
日期: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)+ 到攻击范围内选招攻击:
- 敌人沿寻路路径(含跳/落/爬 NavLink)绕过地形持续逼近玩家,而非直线冲。
- 进入某招的攻击范围后,按攻击策略从"够得着且未冷却"的候选里选一个攻击。
- 攻击时暂停逼近;攻击结束回到逼近,重判距离/重选招,形成 逼近 → 攻击 → 重定位 → 攻击 的闭环。
冲锋范式(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 新增字段(选招/射程元数据,基类通用)
[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 增加:
/// <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:
public interface IAttackCandidate { bool CanUse { get; } // 未冷却+未运行+存活 bool InAttackRange(); // 招式自管射程 bool RequiresGrounded { get; } float Weight { get; } int Priority { get; } bool Execute(); }EnemyAbilityBase(攻击类)实现之。 - API:
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 新增
public AttackSelectionMode attackSelectionMode = AttackSelectionMode.WeightedRandom;
AttackSelectionMode { WeightedRandom, Priority }(BaseGames.Enemies 或 .Abilities)。
4.5 ICombatant 扩展(EnemyBrainContext 实现)
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 扩展
void Pursue(Vector2 target); // 寻路逼近原语:设 Approach 模式 + RunSpeed + 每帧 Nav.RequestMoveTo(target)
- Vector2 驱动(不耦合 Transform),底层已防抖/受阻/NavLink。
- 现有
Approach(Transform)可委托Pursue(target.position)。
4.7 PerceptionStateMachine:新增 ApproachAttack 交战风格(opt-in)
Config 新增:
public EngagementStyle Engagement = EngagementStyle.ChaseAbility; // ChaseAbility(现状) | ApproachAttack(新)
public string ApproachState = "Approach";
public string AttackState = "Attack";
EngagementStyle == ApproachAttack 时,把原单一 Chase 能力节点换成两态子机(ChaseAbility 风格拓扑不变):
- Idle/Patrol/Alert →
ApproachWhencanChase(沿用现有InChaseZone/ 冷却门条件) - Approach(Locomotion 驱动)
OnEnter/Tick:Locomotion.Pursue(Sensor.LastKnown)OnExit:Locomotion.Stop()- →
AttackWhenCombat.HasEligibleAttack() - →
RestWhen!InChaseZone && !InVisionZone
- Attack(能力驱动,无固定 id)
OnEnter:Locomotion.Stop()+Combat.UseBestAttack()(攻击时暂停逼近)- →
ApproachWhen!Combat.IsAbilityRunning()(打完回逼近重判/重选招) - →
RestWhen!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校验项。