docs(enemy): 新增敌人 AI 作者指南(三条路径 + 模块清单 + 六个范例 + 陷阱)
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
# 敌人 AI 作者指南
|
||||
|
||||
> 文件位置:`Docs/Guides/08_EnemyAi_Authoring_Guide.md`
|
||||
> 版本:1.0 · 适用项目:zeling_v2
|
||||
|
||||
本文档面向**给项目新增敌人类型的人**(策划配合 / 程序独立完成均可)。目标体量 100–200 种敌人,
|
||||
其中约 95% 只需建一个配方资产、两个下拉选一选,零代码零编译。读完本文档应该能在 5 分钟内判断
|
||||
「我这个敌人该走哪条路径」,并在动手前看到已知的坑。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [三条作者路径与选择判据](#1-三条作者路径与选择判据)
|
||||
2. [架构:骨架固定,两层可插拔](#2-架构骨架固定两层可插拔)
|
||||
3. [现有模块清单](#3-现有模块清单)
|
||||
4. [怎么新增一个模块](#4-怎么新增一个模块)
|
||||
5. [六个敌人的组装范例](#5-六个敌人的组装范例)
|
||||
6. [常见陷阱](#6-常见陷阱)
|
||||
7. [校验与工具](#7-校验与工具)
|
||||
|
||||
---
|
||||
|
||||
## 1. 三条作者路径与选择判据
|
||||
|
||||
| 路径 | 占比 | 怎么做 | 何时选 |
|
||||
|---|---|---|---|
|
||||
| **配方资产** | ~80% | 菜单 `BaseGames/AI/Enemy AI Recipe Wizard` 建 `PerceptionRecipeSO` 资产,Inspector 里两个 `[SerializeReference]` 下拉分别选一个未发现层模块、一个交战层模块,填能力引用(`AbilityRef`)。零代码零编译。 | 敌人行为完全落在「未发现 → 警觉 → 交战」范式内,且现有模块(见 [§3](#3-现有模块清单))已经覆盖需求或只需调参。 |
|
||||
| **配方 + 自定义前置态** | ~15% | 写一个 `AiScript` 子类(打 `[AiDefinition("id")]`),`Build(BrainBuilder b)` 里先 `PerceptionSkeleton.Add(b, unaware, engagement)` 把骨架建好,再用 `b.DeclareState(...)` 追加自己的前置态,最后 `b.Entry("自己的态")` 覆盖入口(`PerceptionSkeleton.Add` 已经把 `Entry` 设成了 `unaware.Entry`,覆盖必须在 `Add` **之后**调用)。挂在 `EnemyAiBrain._definitionId` 上。 | 需要在骨架前面插一段「出生就不受骨架管」的态——典型是掉落链(天花板吸附 → 落地检测 → 进入未发现层)、出场链(战斗触发 → 出场演出 → 进入未发现层)。 |
|
||||
| **定制脚本** | ~5% | 同样是 `AiScript` 子类,但完全不调用 `PerceptionSkeleton.Add`,自己用 `b.DeclareState(...)`/`b.State(...).To(...)` 写整张图。 | 敌人的行为形状与「未发现 → 警觉 → 交战」范式无关——典型是陷阱型:固定位置、无移动、靠触发区直接进技能循环,没有巡逻/警觉/追击这些语义。 |
|
||||
|
||||
后两条路径共用的入口机制:`EnemyAiBrain` 上 `_recipe`(配方资产)与 `_definitionId`(`AiScript` id 字符串)二选一,`Awake` 里做互斥校验,两者都配或都不配会 `Debug.LogError` 并禁用组件(见 `Assets/_Game/Scripts/Enemies/AIBrain/EnemyAiBrain.cs`)。`_definitionId` 通过 `AiDefinitionRegistry`(反射收集所有 `[AiDefinition]` 的 `AiScript` 子类)解析,同一 id 重复会在编辑器进入 Play 时直接抛异常。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构:骨架固定,两层可插拔
|
||||
|
||||
`PerceptionSkeleton.Add(BrainBuilder b, IUnawareModule unaware, IEngagementModule engagement)`
|
||||
(`Assets/_Game/Scripts/Enemies/AIBrain/Modules/PerceptionSkeleton.cs`)做且只做一件事:把「未发现 → 警觉 → 交战」的升降级规则,挂到两个模块声明的态上。它按固定顺序执行:
|
||||
|
||||
1. `unaware.Declare(b)` — 未发现层**只声明**自己的态(`OnEnter`/`Tick`/`OnExit`),不挂任何转换。
|
||||
2. `engagement.Build(b, unaware.Rest)` — 交战层声明自己的态并挂好脱战边(脱战目标由骨架传入的 `rest` 决定)。
|
||||
3. `AiStateFragments.Terminal(b, PerceptionSkeleton.Death)` — 骨架自己声明 `"Death"` 终态。
|
||||
4. 校验:`unaware.States` 非空,且 `unaware.Rest` 必须属于 `unaware.States`(否则 `InvalidOperationException`)。
|
||||
5. `b.Entry(unaware.Entry)`;`b.Global().To(Death).OnEvent(AiSignal.Died)`(全局边,最高优先级,见 [§6](#6-常见陷阱) 死亡相关条目)。
|
||||
6. **升级边**:对 `unaware.States` 里每一个态,挂 `To(engagement.EntryState).When(engagement.CanEngage, engagement.EngageLabel)`,再挂 `To("Alert").When(有警觉态且在视野)`。`Alert` 态本身(`AiStateFragments.Locomotion(b, "Alert", LocomotionMode.Face)`)挂 `To(engagement.EntryState).When(CanEngage)` 与 `To(unaware.Rest).When(脱离视野)`。
|
||||
7. `unaware.Link(b)` — 未发现层**最后**挂自己的内部边(如巡逻计时)。
|
||||
|
||||
**铁律:每个构件只声明自己拥有的态、只挂自己负责的边。**
|
||||
|
||||
- 骨架不知道未发现层内部有几个态、叫什么名字(`SinglePost` 只有一个 `Post`,`AlternatingIdlePatrol` 有 `Idle`/`Patrol`)——骨架只认 `unaware.Entry`/`unaware.Rest`/`unaware.States` 这三个契约成员,边全部**指向**它们,从不假设内部结构。
|
||||
- 未发现模块不知道警觉层、交战层长什么样——只暴露 `Rest` 这一个供骨架接线的锚点。
|
||||
- 交战模块不知道骨架怎么把自己接进来——只暴露 `EntryState`/`CanEngage`/`EngageLabel`,`Build(b, rest)` 里的 `rest` 是骨架传入的脱战目标,模块自己决定脱战边挂在哪、什么时候触发。
|
||||
|
||||
这条铁律的直接后果:新增一种「未发现层行为」或「交战打法」时,只需新增一个实现类,不需要改 `PerceptionSkeleton`、不需要改其他任何已有模块。
|
||||
|
||||
死亡态 (`Death`) 由骨架自己声明为无行为终态,**不做成第三层可插拔层**——原因见 [§6 死亡链不能放条件边](#6-常见陷阱)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 现有模块清单
|
||||
|
||||
### 未发现层(`IUnawareModule`,`Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/`)
|
||||
|
||||
| 类名 | 语义 | 参数(默认值) | 适用敌人 |
|
||||
|---|---|---|---|
|
||||
| `SinglePost` | 单一未发现态:站桩或单一巡逻,出生态与脱战态是同一个态(`Post`)。 | `_mode`(`LocomotionMode`,默认 `Patrol`) | 没有伪装/交替行为、发现即扑上来的普通巡逻怪。 |
|
||||
| `DisguiseThenPatrol` | 单向降级:出生时伪装静止,一旦交战过就**只回巡逻**——伪装态出生后不可逆。两个态 `Disguise`/`Patrol`,刻意不挂内部边。 | `_disguiseMode`(默认 `Idle`)、`_patrolMode`(默认 `Patrol`) | 伪装型小怪,如 E001(见 [§5](#5-六个敌人的组装范例))。 |
|
||||
| `AlternatingIdlePatrol` | 站立 ⇄ 巡逻定时交替,脱战回站立(`Idle`)。内部计时边经 `Link` 挂载。 | `_idleDwell`(秒,默认 `2f`,`Min(0.1f)`)、`_patrolDwell`(秒,默认 `4f`,`Min(0.1f)`) | 有"游走-驻留"节奏感的普通巡逻怪。 |
|
||||
|
||||
### 交战层(`IEngagementModule`,`Assets/_Game/Scripts/Enemies/AIBrain/Modules/Engagement/`)
|
||||
|
||||
| 类名 | 语义 | 参数(默认值) | 适用敌人 |
|
||||
|---|---|---|---|
|
||||
| `RushEngagement` | 接触冲锋:单一态 `Rush`,一个能力包办追击、方向、速度、动画与命中判定;AI 只负责进/出这个态。 | `_ability`(`AbilityRef`,冲锋能力)、`_exit`(`RushExit`,默认 `OnLostTarget`) | 无独立攻击判定、靠身体接触伤害的贴身怪。 |
|
||||
| `ApproachAttackEngagement` | 寻路逼近 ↔ 到射程选招攻击:`Approach` 态用 `Locomotion.Pursue` 逼近,`Combat.HasEligibleAttack()` 为真时转 `Attack`,`Combat.UseBestAttack()` 出招,招式打完(`!IsAbilityRunning()`)回 `Approach`。招式的射程/冷却/权重由 `EnemyAttackSelector` 负责,本模块只决定"什么时候该逼近、什么时候该出手"。 | 无可调参数(射程/冷却配在能力与 `EnemyAttackSelector` 上,不在本模块) | 有独立攻击招式(近战一击/多招轮转)、需要先欺身再出手的敌人。 |
|
||||
|
||||
`RushExit` 有两个取值(`Assets/_Game/Scripts/Enemies/AIBrain/Modules/Engagement/RushExit.cs`):
|
||||
|
||||
- `OnLostTarget`(默认):追不到就放弃,脱离全部感知区(`AiStateFragments.LostAllZones`)即停。
|
||||
- `Committed`:起手即锁定,整段冲锋跑完才脱战,中途丢失感知不打断;模块**自动**给进入条件附加冷却门(`Combat.CanUseAbility(id)`),否则打完回未发现态后下一帧仍在追逐区会立刻再冲,出现"冲锋态 ↔ 未发现态"每帧抖动。前提是所引用冲锋能力的 `cooldown`(`EnemyAbilitySO.cooldown`,默认 `1.5f`)配置为 `> 0`;配成 `0` 的话抖动照样发生,那属于能力侧配置错误,见 [§7](#7-校验与工具) 待补的校验器。
|
||||
|
||||
`ApproachAttackEngagement` 的攻击态刻意不挂"脱离全部感知即打断"的边——玩家在前摇中跑出感知区时招式仍完整打完,之后经 `Approach` 自然脱战,观感上比"起手到一半凭空收招"更合理。它还假设攻击射程恒小于感知区(`InAttackRange` 门早于 `LostAllZones` 生效);若将来出现射程大于追击区的招式,属于射程/感知区配置错误,不要靠调边序绕开。
|
||||
|
||||
---
|
||||
|
||||
## 4. 怎么新增一个模块
|
||||
|
||||
1. 实现 `IUnawareModule` 或 `IEngagementModule`,打 `[Serializable]`,并**提供一个公开的无参构造函数**(可以再加带参构造给 EditMode 测试用,但无参构造必须存在)。`SubclassSelectorDrawer.GetImplementations`(`Assets/_Game/Scripts/Editor/AI/SubclassSelectorDrawer.cs`)反射收集下拉选项时,条件正是「非抽象 + 打了 `[Serializable]` + 有 `GetConstructor(Type.EmptyTypes)`」——少一条都不会出现在 Inspector 下拉里,且没有任何报错提示。
|
||||
2. **不要在构造函数里做校验或预计算。** 模块经 `[SerializeReference]` 反序列化,反序列化路径不保证调用构造函数。缓存用惰性属性(参考 `RushEngagement.CanEngage`:`_canEngage` 是 `[NonSerialized] Func<...>` 字段,首次访问时才构建并缓存);校验放 `Build()`/`Link()` 里的调用路径上(参考 `RushEngagement.RequireAbilityId()`,在 `CanEngage` getter 与 `Build()` 两处都会调用,漏配能力时在建图期而非运行时热路径抛错)。
|
||||
3. `IUnawareModule.States` 用 `static readonly` 数组(如 `SinglePost.StatesArray`),不要用实例字段初始化器——静态初始化器一定会执行,而实例字段初始化器在走 `[SerializeReference]` 反序列化路径时不保证执行,会得到一个空数组,触发 `PerceptionSkeleton.Add` 里的 `InvalidOperationException`。
|
||||
4. 未发现层的内部边必须放 `Link(BrainBuilder b)`,不能放 `Declare(BrainBuilder b)`——原因见 [§6](#6-常见陷阱) 第一条。
|
||||
5. 若模块类将来要改名或挪命名空间,务必看 [§6](#6-常见陷阱)「重命名模块类」一条。
|
||||
|
||||
---
|
||||
|
||||
## 5. 六个敌人的组装范例
|
||||
|
||||
以下按当前设计文档(`Docs/Game/敌人/小怪/E00*.md`)推演组装方式;E001 是唯一已迁移的真实配方资产,其余为按文档设计应选用的模块组合(尚未建资产)。
|
||||
|
||||
### E001 草蛭 —— 配方路径(真实资产:`Assets/_Game/Data/Enemies/E001/ENM_E001_Ai.asset`)
|
||||
|
||||
伪装待机 → 感知玩家后持续追击啃咬 → 丢失目标回巡逻,且伪装态出生后不可逆。实际配置:
|
||||
|
||||
- 未发现层:`DisguiseThenPatrol`(`_disguiseMode = Idle`,`_patrolMode = Patrol`)
|
||||
- 交战层:`RushEngagement`(`_ability` 引用 `Assets/_Game/Data/Enemies/E001/Abilities/ABL_E001_Chase.asset`,`_exit = Committed`)
|
||||
|
||||
没有独立攻击判定、靠身体接触伤害,且是持续追击而非单次出招,所以选 `RushEngagement` 而非 `ApproachAttackEngagement`。
|
||||
|
||||
### E002 簧蛭 —— 定制脚本路径(陷阱型,完全不用骨架)
|
||||
|
||||
固定位置、无法移动,靠正下方矩形触发区直接钻出啃咬,悬挂一段时间后(此间可被反打)自动缩回冷却,整个行为与"未发现 → 警觉 → 交战"的巡逻/追击语义无关。写一个 `AiScript` 子类,不调用 `PerceptionSkeleton.Add`,自己声明:
|
||||
|
||||
```
|
||||
DeclareState("Hidden") — 待机,Tick 检测正下方触发区是否有玩家
|
||||
.To("Emerge").When(玩家进入触发区)
|
||||
DeclareState("Emerge") — 钻出啃咬,OnEnter 触发攻击能力
|
||||
.To("Hang").When(!IsAbilityRunning())
|
||||
DeclareState("Hang") — 悬挂可受击窗口,计时到点或玩家离开区域回 Hidden
|
||||
.To("Hidden").After(hangDuration)
|
||||
Global().To("Death").OnEvent(AiSignal.Died)
|
||||
Entry("Hidden")
|
||||
```
|
||||
|
||||
冷却(缩回后到下次可再触发的间隔)用 `AiRuntime.TimeInState`/自定义计时条件在 `Hidden` 态内部处理,不复用骨架的任何契约(本来就不该复用——这里没有"未发现层"或"交战层"的概念)。
|
||||
|
||||
### E003 幼蛭 —— 配方 + 自定义前置态路径
|
||||
|
||||
两种出场方式(场景预置于天花板 / 由 E005 死亡时生成),共同点是出场后先有一段"下落"过程,不受骨架管(重力控制、不响应玩家输入),落地后才进入正常的"未发现(爬行)→ 交战(追击)"循环。写 `AiScript` 子类:
|
||||
|
||||
```
|
||||
Build(b):
|
||||
PerceptionSkeleton.Add(b, new SinglePost(LocomotionMode.Patrol), new RushEngagement(fallAbility, RushExit.OnLostTarget));
|
||||
b.DeclareState("Fall")
|
||||
.OnEnter(x => 播放下落 / 交给重力)
|
||||
.To(SinglePost.Post).When(落地检测)
|
||||
b.Entry("Fall"); // 覆盖骨架设的 Entry(骨架已把 Entry 设为 unaware.Entry="Post")
|
||||
```
|
||||
|
||||
一击即死(`maxHP = 1`)不是 AI 图的职责,归 `EnemyStatsSO`/受伤流程处理,不体现在状态机里。
|
||||
|
||||
### E004 蛭母 —— 定制脚本路径(Boss,多技能轮转 + 出场演出)
|
||||
|
||||
出场剧情 → 三技能轮转(撕咬/头槌连段/酸液远程,按距离和 CD/权重选择)→ 死亡分两阶段(挣扎前摇 → 爆体)。技能选择逻辑(按距离分层、CD 轮转)比 `ApproachAttackEngagement` 复杂得多,且需要「玩家绕后触发 Flip、但技能执行中不检测」这类与感知骨架无关的定制转换,属于"完全定制"的判据(与范式无关的独门机制)。Boss 是否要接入 `PerceptionSkeleton` 视具体实现取舍:若拆出"未发现(几乎不存在,出场即战斗)→ 交战(技能轮转视为一个大号 `Attack` 态)"两层意义不大,直接写定制脚本更直接。死亡链遵循 [§6](#6-常见陷阱) 的死亡规则,`Death_Pre`/`Death` 完全交给物理层(`EnemyDeathSequence` 演出 + `EnemyBase.PerformDeath`),AI 图里只需要一个 `Death` 终态兜住全局边。
|
||||
|
||||
### E005 肥蛭 —— 配方路径(精英怪,攻击选择器覆盖远近两招)
|
||||
|
||||
近战撕咬 + 远程酸液两招轮转,靠距离和权重/CD 选招,行为形状与 `ApproachAttackEngagement`(逼近 ↔ 到射程选招攻击)完全吻合:
|
||||
|
||||
- 未发现层:`SinglePost`(`_mode = Patrol`)或 `AlternatingIdlePatrol`(若需要站立-巡逻节奏)
|
||||
- 交战层:`ApproachAttackEngagement`,两招(撕咬近战、酸液远程)配在 `EnemyAttackSelector` 里,各自的射程/冷却/权重由选择器负责,不进 AI 配方。
|
||||
|
||||
死亡生成幼蛭(Spawn E003)由 `Death` 动画的 `AnimationEvent` 触发,不进 AI 图(同样是"死亡完全归物理层")。
|
||||
|
||||
### E006 讙 —— 配方路径
|
||||
|
||||
巡逻 + 发现玩家后跳跃突进爪击,动画只做原地滞空攻击,位移由程序在起跳帧施加冲量——这个"位移不在动画里"的细节不影响 AI 图的形状,本质仍是"未发现(巡逻/待机)→ 交战(单一跳跃攻击态)":
|
||||
|
||||
- 未发现层:`AlternatingIdlePatrol`(站立 ⇄ 巡逻)或 `SinglePost`,按最终巡逻节奏需求二选一
|
||||
- 交战层:`RushEngagement`(若跳跃攻击视为"一个能力包办位移+伤害+动画")——落地后到下次起跳的间隔(`attackInterval`)由冲锋能力自身的冷却承担,选 `_exit = Committed` 语义最接近(起手即锁定,打完才能再次进入)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见陷阱
|
||||
|
||||
以下每条都给出"为什么"——不解释原因的规则会被绕过。
|
||||
|
||||
1. **未发现层内部边必须放 `Link()`,不能放 `Declare()`。**
|
||||
`AiRuntime.TryTransition` 按**声明顺序**遍历当前态的转换列表,条件都满足时先声明的先命中。`PerceptionSkeleton.Add` 的调用顺序固定为 `Declare → 挂升级边 → Link`:升级边(进警觉/进交战)必须先于未发现层的内部边(如巡逻计时)声明。如果把内部边挂进 `Declare()`,它会排在升级边前面——玩家进入追逐区那一帧,若恰好计时器也到点,敌人会切去另一个待机态而不是扑上来。
|
||||
|
||||
2. **死亡态用 `AiStateFragments.AbilityOnce`,不用 `Ability`。**
|
||||
`Ability()` 建的态每帧 `Tick` 都会 `EnsureAbility`(若能力已结束则重新触发),用于"只要还在这个态就该一直在做"的追击/冲锋类持续态。死亡演出只该播一次——用 `Ability()` 会在演出播完后被下一帧 `Tick` 重新触发,死亡动画无限重播。`AbilityOnce()` 只在 `OnEnter` 触发一次,没有 `Tick`。
|
||||
|
||||
3. **死亡链里不能放条件边,死亡完全归物理层。**
|
||||
`EnemyBase.PerformDeath` 先 `ForceState(EnemyStateType.Dead)`,再 `_brain?.Send(AiSignal.Died)`。`ForceState` 之后 `EnemyBrainContext.IsControllable`(`_enemy.CurrentState == EnemyStateType.Controlled`)永久为假;`AiRuntime.Tick` 里事件转换(`Died` 信号)先于 `IsControllable` 让位门处理,能正常切到 `Death` 终态,但**此后**任何条件边都不会再被求值(`Tick` 在让位门处直接 `return`)。所以骨架把 `Death` 声明成 `AiStateFragments.Terminal`(无行为终态),不做成可插拔层——死亡演出/清理/对象池归还全部交给 `EnemyBase.PerformDeath`、`EnemyDeathSequence`(前摇演出)、`EnemySpawnerOnEvent`(死亡生成小怪)等物理层组件。
|
||||
|
||||
4. **状态名只是 trace 标签,不驱动动画。**
|
||||
动画由能力(`EnemyAbilityBase` 及其子类,如 `RushAbility`)与 `EnemyLocomotion` 驱动,与 AI 状态名无耦合关系。`AiRuntime.CurrentStateName` 的消费者只有 `EnemyAiBrain.CurrentStateName`(调试面板)和测试断言——不要指望改一个状态的名字会影响任何演出表现。
|
||||
|
||||
5. **受击 / 硬直不进 AI 图。**
|
||||
`AiSignal` 只保留 `Died` 一个值(`Assets/_Game/Scripts/AI/AiSignal.cs`),受击/硬直/击飞/弹反由敌人物理状态机 `EnemyStateType` 处理,`AiRuntime.Tick` 经 `IActorVitals.IsControllable` 让位门自动挂起决策(`IsSuspended = true`,不推进转换也不跑 `OnTick`)。不要为受击反应在 AI 图里加信号或态,那是在和物理层的受击逻辑打架。
|
||||
|
||||
6. **声明态用 `DeclareState`(重名报错),挂边到已声明的态用 `State`(宽松)。**
|
||||
`BrainBuilder.DeclareState(name)` 首次声明一个态的行为回调,若名字已存在直接抛 `InvalidOperationException`——防止两个独立模块取了同名状态,后一个的 `OnEnter`/`Tick`/`OnExit` 静默覆盖前一个(敌人跑错行为且零报错)。`BrainBuilder.State(name)` 不存在则新建、存在则直接返回,只用于"给别人已声明的态挂转换",比如 `PerceptionSkeleton` 给未发现层的态挂升级边时,用的是 `b.State(states[i])` 而非 `DeclareState`——因为这些态的行为已经在 `unaware.Declare(b)` 里声明过了。
|
||||
|
||||
7. **⚠️ 重命名或移动模块类时必须加 `[MovedFrom]`(`UnityEngine.Scripting.APIUpdating`),这是硬性规则,不是建议。**
|
||||
配方资产(`PerceptionRecipeSO._unaware`/`_engagement`)用 `[SerializeReference]` 按**类型全名**(含命名空间与程序集)存模块实例。重命名类、移动命名空间、或把类从一个 asmdef 挪到另一个,都会让引用该模块的所有配方资产在反序列化时找不到类型——**静默**丢失该字段(反序列化成 `null`),而不是报错提示"类型找不到"。随后 `PerceptionSkeleton.Add` 里 `unaware`/`engagement` 为 `null` 会在 `ArgumentNullException` 或 `Build()` 里更下游的 NRE 处爆掉,错误信息完全指不到"根因是某个配方资产的某个模块字段丢了引用"这件事。`[MovedFrom]` 让 Unity 序列化系统按旧类型名找到新类型,是唯一的根因修复手段——事后一个个打开配方资产手动重选模块,属于治标不治本的下游补救。
|
||||
|
||||
---
|
||||
|
||||
## 7. 校验与工具
|
||||
|
||||
建配方必须走向导(见 CLAUDE.md 第 2 条):
|
||||
|
||||
```
|
||||
菜单栏 → BaseGames → AI → Enemy AI Recipe Wizard
|
||||
```
|
||||
|
||||
输入敌人 ID(如 `E007`),按规范产出 `Assets/_Game/Data/Enemies/{EnemyID}/ENM_{EnemyID}_Ai.asset`(`EnemyAiRecipeWizard.Create`,已存在则选中不覆盖)。创建后在 Inspector 里选两层模块、填能力引用,再把资产拖到敌人预制体的 `EnemyAiBrain._recipe` 上。
|
||||
|
||||
建完(无论走哪条路径)跑三项自检(CLAUDE.md 第 3 条):
|
||||
|
||||
```
|
||||
菜单栏 → BaseGames → Tools → Validation → Validate All ScriptableObjects
|
||||
菜单栏 → BaseGames → Addressables → Validate Address Keys
|
||||
菜单栏 → BaseGames → Tools → Maintenance → Physics2D Layer Matrix → Check
|
||||
```
|
||||
|
||||
**尚未实现、是紧接的下一步**:专门的 `AiDefinitionValidator`——校验配方引用的能力(`AbilityRef`)是否存在、`RushEngagement` 在 `RushExit.Committed` 下所引用能力的 `cooldown` 是否 `> 0`(否则出现 [§3](#3-现有模块清单) 提到的抖动,当前只在注释里提醒,没有工具报错)等。目前这类漏配只能在 Play Mode 里靠肉眼观察或看 `EnemyAiBrain.CurrentStateName`/`AiRuntime.Trace` 排查。
|
||||
Reference in New Issue
Block a user