# 小怪与 Boss 实现计划 — 01(修订版) > 依据《小怪设计-程序开发文档-01》《Boss设计-程序开发文档-01》及 `Assets/_Game/Scripts` 框架深度阅读整理。 > **本版本已对照源码纠正所有架构偏差,可直接用于落地实现。** --- ## 架构关键约束 > ⚠️ **在编写任何代码前,必须理解以下约束,否则将产生运行时错误。** ### 1. 动画所有权原则(最重要) **能力脚本对动画负完整责任**:所有技能/触发型动画(包括前摇、循环、收招、反馈等)必须在对应 `EnemyAbilityBase` 子类的 `ExecuteCoroutine()` 内通过 `_animancer.Play(clip)` 播放。 **以下系统严禁写动画逻辑**: - BD 行为树节点(禁止使用 `BD_PlayAnimation` 触发技能或触发型动画) - 寻路系统(PathBerserker2d、NavAgent 等) - 其他非 Ability、非 EnemyBase/BossBase 的脚本 **框架自动处理的标准状态动画(无需在 BT 中操作)**: | 动画 | 谁负责 | |------|--------| | Idle | `EnemyBase.Start()` + `SetAiPhase(Idle)` 自动播放 `AnimConfig.Idle` | | Walk | `SetAiPhase(Patrol)` 自动播放 `AnimConfig.Walk` | | Run | `SetAiPhase(Chase)` 自动播放 `AnimConfig.Run` | | Dead | `EnemyBase.Die()` 自动播放 `AnimConfig.Dead` + 归还对象池 | | Phase 过渡 | `BossBase.EnterPhase()` 子类重写内部播放 | **BT 的正确职责**:条件判断(BD_IsXxx)+ 调用 `BD_UseAbility` / `BD_SetAiPhase` / `BD_SetState` — **不播放动画**。 `EnemyAnimationConfigSO` 支持的字段(`Idle / Walk / Run / Attack / Hurt / Stagger / KnockUp / Dead / Alert / Investigate / Turn`)是给框架状态机内部使用的,不是给 BT 调用的接口。 ### 2. 能力注册机制 `EnemyAbilityRegistry` 在 `EnemyBase.Awake()` 时调用 `GetComponentsInChildren(true)` 自动扫描所有子 GameObject,按 `EnemyAbilitySO.abilityId` 注册。 - **能力组件必须挂在 Prefab 的子对象上**(或根对象的子层级) - `abilityId` 必须全局唯一,不可重复 - BD Task `BD_UseAbility`、`BD_CanUseAbility`、`BD_IsAbilityRunning` 都通过 abilityId 或 SO 引用查找能力 ### 3. 导航系统约束 - 框架使用 **PathBerserker2d**(`NavAgent` + `TransformBasedMovement` + `EnemyNavAgent`) - `EnemyBase.MoveTo(Vector2)` → `_nav.RequestMoveTo(target)` → `NavAgent.UpdatePath(target)` - `EnemyMovement.MoveHorizontal(dir)` 使用 `EnemyStatsSO.WalkSpeed`;`MoveWithSpeed(dir, speed)` 使用显式速度 - **固定位置敌人(E002 簧蛭)不挂 EnemyNavAgent / NavAgent,不调用 MoveTo** - 朝向翻转:`EnemyMovement.UpdateFacing()` 优先用 `SpriteRenderer.flipX`,无 SR 时用 `localScale.x` ### 4. 感知系统约束(SensorToolkit) `EnemySensorHub` 标准槽位名称约定: | 槽名 | Sensor 类型 | 用途 | |------|------------|------| | `aggro` | RangeSensor2D | 主要警戒/感知范围 | | `attack_melee` | RangeSensor2D | 近战攻击距离触发 | | `attack_range` | RangeSensor2D | 远程攻击距离触发 | | `los` | LOSSensor2D | 视线检测 | | `wall_ahead` | RaySensor2D | 前方障碍物检测 | | `ledge` | RaySensor2D | 前方悬崖检测 | `BD_IsSensorDetecting` 的 m_SlotName 必须与 Inspector 中 `EnemySensorHub._slots` 对应的 slotName 完全一致。 `BD_Patrol` 优先读取 `wall_ahead` 和 `ledge` 槽的 SensorToolkit 结果;若未配置这两个槽,自动回退 Raycast 兜底。 ### 5. AI 阶段(AiPhase)枚举值 只能使用以下枚举值:`Idle / Patrol / Alert / Chase / Combat / Investigate / ReturnHome` ### 6. Boss 技能系统约束 - 技能通过 `BossSkillExecutor` 执行,通过 `BossSkillSO.availablePhaseIndices` 控制阶段可用性 - `BD_UseBossSkill` 调用 `BossBase.UseBossSkill(skillId)`,等待 `IsBossSkillExecuting == false` - `BD_BossPhaseTransition(targetPhase, duration)` 调用 `BossBase.BeginPhaseTransition()`,等待 `IsPhaseTransitioning == false` ### 7. AnimationEvent 集成约束 `EnemyBase` 暴露以下虚方法供动画 AnimationEvent 调用: - `SpawnProjectile(string payload)` — 生成弹体(需子类重写实现具体逻辑) - `OnAnimationComplete(string payload)` — 动画完成回调 - `EnemyCombat.EnableHitBox(int index)` / `DisableHitBox(int index)` — 按 AttackType 枚举索引控制 HitBox ### 8. 对象池约束 - 弹体和小怪生成:`IObjectPoolService.Spawn(string poolKey, Vector3 pos, Quaternion rot)`,返回 `GameObject` - `BD_SummonMinions` 已封装召唤逻辑,可在**行为树触发的召唤行为**中直接使用。注意:E005 死亡生成 E003 发生在 `EnemyBase.Die()` 覆写中(通过 AnimationEvent `SpawnProjectile` 回调触发),不经过 BT,不复用此 Task。 - 所有可池化对象在 Prefab 根节点挂 `PooledObject` 组件;`Die()` 自动归还而非 Destroy ### 9. 伤害事件约束(重要) - **`HurtBox` 无公开 C# 事件**:`HurtBox` 内部通过 `DamageInfoEventChannelSO` 事件频道广播,**不暴露** `OnDamageTaken` 等 C# 事件。 - **订阅伤害通知的正确做法**:重写 `EnemyBase.OnDamageTaken(DamageInfo info)` 虚方法(`protected virtual`)。 示例(嘲风击落计数器):在 `ChaoFengBoss.OnDamageTaken()` 中调用计数器组件的公开方法,而非订阅 HurtBox 事件。 - `EnemyBase` 提供的公开 C# 事件:`OnDied`(`event System.Action`)、`OnAiPhaseChanged`(`event System.Action`)。 ### 10. Ability 命名规范(通用复用原则) 能力脚本命名以**行为描述**为准,不包含具体敌人名称,使同一 Ability 可被任意小怪/Boss 配置复用。 | 脚本文件 | 描述 | 当前使用者 | |---------|------|-----------| | `PlayClipAbility.cs` | 播放单个 Clip 等待完成(无副作用) | E001 激活动画 | | `ContactChaseAbility.cs` | 循环追击 + 体接触伤害,失去感知后收招退出 | E001 追击行为 | | `CeilingHangStrikeAbility.cs` | 天花板三段攻击(出击→脆弱悬挂→收回) | E002 吊钩攻击 | | `AnimatedCeilingDropAbility.cs` | 带下落动画的天花板跌落(Kinematic→Dynamic) | E003 幼蛭落下 | | `AppearAbility.cs` | 出场动画播放 + 进入战斗阶段 | E004 蛭母出场 | | `RepeatSlamAbility.cs` | 可配置次数的砸地多连击 | E004 头槌 | | `FacePlayerAbility.cs` | 战斗中主动朝向玩家(含动画,CanUse 含背后检测);**不同于**巡逻转向 | E004 Flip | | `MeleeVulnerabilityAbility.cs` | 近战攻击 + 后摇脆弱窗口 | E005 撕咬 | | `ReturnProjectile.cs` | 飞出到最大距离后自动返回弹体 | 嘲风 回旋扇 | **字段命名规范**:能力内部字段同样使用通用名,不含角色名前缀。 例:`_attackClip`(不用 `_biteClip`)、`_hitBox`(不用 `_biteHitBox`)、`_faceClip`(不用 `_flipClip`)。 > **巡逻转身 vs 战斗 Flip 的区别**: > - **巡逻转身**(E001/E006):由 `EnemyMovement._enableTurnAnimation=true` + `AnimConfig.Turn` 驱动,`BD_Patrol` 触发 `MoveHorizontal()` 自动播转身动画,**无需任何 Ability**。 > - **战斗 Flip**(E004):玩家绕到背后时主动触发,是一个有 CanUse 条件门控的**战斗技能**,需要 `FacePlayerAbility` 作为独立 Ability 注册到 BT。 --- ## 通用前置任务 ### P0-A:动画 Clip 制作规范 - 动画 Clip 放在 `Assets/_Game/Art/Characters/Enemies/{EnemyID}/Animations/` 下 - Clip 名与需求表完全一致(区分大小写) - 每个能力引用的 Clip 通过 `EnemyAttackSO.clip`(ClipTransition 类型)绑定,不通过 EnemyAnimationConfigSO ### P0-B:Layer 配置确认 - Enemy HitBox Layer:`EnemyHitBox` - Enemy HurtBox Layer:`EnemyHurtBox` - 弹体 Layer:`EnemyProjectile` - `HitBox._rivalHitBoxMask`:勾选 PlayerHitBox + EnemyHitBox ### P0-C:对象池 PoolKey 注册 | PoolKey(= Addressable Address) | Prefab | |----------------------------------|--------| | `ENM_YouZhi` | ENM_YouZhi.prefab(E005死亡生成) | | `PROJ_ZhiMu_Acid` | PROJ_ZhiMu_Acid.prefab(ArcProjectile) | | `PROJ_FeiZhi_Acid` | PROJ_FeiZhi_Acid.prefab(ArcProjectile) | | `PROJ_Boomerang` | PROJ_Boomerang.prefab(ReturnProjectile) | | `PROJ_TornadoSmall` | PROJ_TornadoSmall.prefab | | `PROJ_TornadoLarge` | PROJ_TornadoLarge.prefab | | `PROJ_WindStone` | PROJ_WindStone.prefab(ArcProjectile) | > ⚠️ **Pool Key 必须与 Addressable Address 完全一致**(大小写、格式均匹配)。Address 规则:小怪 Prefab → `ENM_*`,抛射物 Prefab → `PROJ_*`。 --- ## 小怪实现计划 --- ### E001 草蛭 **核心难点**:伪装默认状态,感知后 Skill_Start→Skill_Loop 追击,自定义 Clip 通过 Ability 播放 #### SO 配置 **`ENM_E001_Stats.asset`**(EnemyStatsSO):MaxHP、WalkSpeed(巡逻)、RunSpeed(追击)、DetectRange。 **`ABL_E001_Activate.asset`**:abilityId=`"e001_activate"`,cooldown=0 **`ABL_E001_Chase.asset`**:abilityId=`"e001_chase"`,cooldown=0,preferredMaxRange=DetectRange #### 新建 Ability **`PlayClipAbility : EnemyAbilityBase`**(激活序列,一次性): `csharp [SerializeField] private Animancer.ClipTransition _clip; protected override IEnumerator ExecuteCoroutine() { Phase = AbilityRunState.Windup; _animancer.Play(_clip); yield return EnemyAbilityWaits.Get(_clip.Clip.length); // 完成后 BT 检测 IsRunning==false 自动进入下一分支 } ` **`ContactChaseAbility : EnemyAbilityBase`**(追击循环): `csharp [SerializeField] private Animancer.ClipTransition _loopClip; [SerializeField] private Animancer.ClipTransition _endClip; [SerializeField] private BodyContactDamage _contactDamage; [SerializeField] private EnemySensorHub _sensorHub; protected override IEnumerator ExecuteCoroutine() { Phase = AbilityRunState.Active; _animancer.Play(_loopClip); _contactDamage.enabled = true; while (true) { if (_enemy.PlayerTransform == null) break; if (!_sensorHub.IsDetecting("aggro", _enemy.PlayerTransform.gameObject)) break; _enemy.MoveTo(_enemy.PlayerTransform.position); yield return null; } _contactDamage.enabled = false; _enemy.StopMovement(); _animancer.Play(_endClip); yield return EnemyAbilityWaits.Get(_endClip.Clip.length); _enemy.SetAiPhase(AiPhase.Patrol); // 恢复巡逻 } ` #### Prefab 结构 ` E001_CaoZhi ├── [EnemyBase] [EnemyMovement] [EnemyNavAgent] [NavAgent] [TransformBasedMovement] ├── [EnemySensorHub] → slots: [{aggro, RangeSensor2D}, {wall_ahead, RaySensor2D}, {ledge, RaySensor2D}] ├── [Rigidbody2D] Dynamic [Collider2D] ├── HurtBox/ [HurtBox] [Collider2D trigger=true Layer=EnemyHurtBox] ├── ContactDamageZone/ [BodyContactDamage] [HitBox] [Collider2D trigger=true Layer=EnemyHitBox] └── Abilities/ ├── [PlayClipAbility] (_config=ABL_E001_Activate) └── [ContactChaseAbility] (_config=ABL_E001_Chase) ` #### 行为树 ` Selector ├── BD_IsStateMatch(Dead) → BD_StopMovement ← Die() 已自动播放 AnimConfig.Dead,BT 只需停止移动 ├── Sequence [激活] │ ├── Selector [可激活阶段] ← 设计:Idle_Disguise 和 Move_Patrol 均可被感知触发 │ │ ├── BD_IsAiPhase(Idle) │ │ └── BD_IsAiPhase(Patrol) │ ├── BD_IsSensorDetecting("aggro") │ ├── BD_UseAbility(ABL_E001_Activate) │ └── BD_SetAiPhase(Chase) ├── Sequence [追击] │ ├── BD_IsAiPhase(Chase) │ └── BD_UseAbility(ABL_E001_Chase) ← 内部自行调用 SetAiPhase(Patrol) └── Selector [巡逻] ← ⚠️ 必须是 Selector,不能是 Sequence ├── Sequence [待机] │ ├── BD_IsAiPhase(Idle) │ └── BD_WaitRandom(min, max) ← SetAiPhase(Idle) 已自动播放 AnimConfig.Idle,无需 BD_PlayAnimation └── Sequence [巡逻移动] ├── BD_IsAiPhase(Patrol) └── BD_Patrol ` #### 美术 Clip 清单 `Idle`(AnimConfig.Idle,伪装待机)、`Move`(AnimConfig.Walk,巡逻爬行)、`Flip`(**AnimConfig.Turn**)、`Skill_Start`、`Skill_Loop`、`Skill_End`、`Death`(AnimConfig.Dead) > **Flip 动画**:`EnemyMovement` 已原生支持转身动画,无需修改 BT。只需: > 1. `EnemyAnimationConfigSO.Turn` 配置 `Flip` Clip > 2. `EnemyMovement._enableTurnAnimation = true`(Inspector 勾选) > > `BD_Patrol` 调用 `MoveHorizontal()` → 触发 `UpdateFacing()` → 自动执行 `TurnCoroutine`(停止移动 → 播 Turn 动画 → 翻转 → 恢复),BT 层**零改动**。 > 受击/死亡/NavLink 穿越时 `CancelTurn()` 自动中断转身并立即完成翻转,无需额外处理。 --- ### E002 簧蛭 **核心难点**:固定位置(无 NavAgent/EnemyMovement),Skill_Loop 为脆弱窗口 #### SO 配置 **`ENM_E002_Stats.asset`**:MaxHP、AttackDamage;WalkSpeed/RunSpeed=0(固定)。 **`ABL_E002_CeilingStrike.asset`**:abilityId=`"e002_ceiling_strike"`,cooldown=悬挂+冷却,interruptOnHurt=false #### 新建 Ability **`CeilingHangStrikeAbility : EnemyAbilityBase`**: `csharp [SerializeField] private Animancer.ClipTransition _clip; [SerializeField] private Animancer.ClipTransition _loopClip; [SerializeField] private Animancer.ClipTransition _endClip; [SerializeField] private HitBox _attackHitBox; [SerializeField] private HurtBox _hurtBox; [SerializeField] private float _hangDuration = 2f; protected override IEnumerator ExecuteCoroutine() { Phase = AbilityRunState.Active; _animancer.Play(_clip); _attackHitBox.Activate(_config.attackSequence[0].damageSource, _transform); yield return EnemyAbilityWaits.Get(_clip.Clip.length); _attackHitBox.Deactivate(); _animancer.Play(_loopClip); _hurtBox.enabled = true; // 脆弱窗口 yield return EnemyAbilityWaits.Get(_hangDuration); _hurtBox.enabled = false; _animancer.Play(_endClip); yield return EnemyAbilityWaits.Get(_endClip.Clip.length); } protected override void OnInterrupted(InterruptReason reason) { _attackHitBox?.Deactivate(); if (_hurtBox != null) _hurtBox.enabled = false; } ` #### Prefab 结构 ` E002_HuangZhi ├── [EnemyBase] ← 无 EnemyMovement,无 NavAgent ├── [EnemySensorHub] → slots: [{attack_range, RangeSensor2D(正下方矩形)}] ├── [Rigidbody2D] Kinematic Gravity=0 ├── HurtBox/ [HurtBox enabled=false] [Collider2D trigger] ├── AttackHitBox/ [HitBox] [Collider2D trigger] └── Abilities/ [CeilingHangStrikeAbility] ` #### 行为树 ` Selector ├── BD_IsStateMatch(Dead) → BD_Wait(999) ← 固定单位无移动需停止,Die() 已自动播放死亡动画 └── Sequence ├── BD_IsSensorDetecting("attack_range") ← SensorSlotNames.AttackRange;下方玩家检测区域槽位 ├── BD_CanUseAbility(ABL_E002_CeilingStrike) └── BD_UseAbility(ABL_E002_CeilingStrike) ` --- ### E003 幼蛭 **核心难点**:HP=1 一击即死,双路生成(预置/E005生成),天花板 Kinematic→落地 Dynamic #### SO 配置 **StatsSO**:MaxHP=**1**,WalkSpeed(慢),RunSpeed(追击)。 **`ABL_E003_Fall.asset`**:abilityId=`"e003_fall"`,cooldown=0 #### Ability **新建 `AnimatedCeilingDropAbility : EnemyAbilityBase`**(完整封装 Fall 动画 + 物理下落,取代 sealed 的 `CeilingDropAbility`): `csharp [RequireComponent(typeof(Rigidbody2D))] public class AnimatedCeilingDropAbility : EnemyAbilityBase { [Header("动画")] [SerializeField] private Animancer.ClipTransition _fallLoopClip; // Fall(旋转下落,循环) [SerializeField] private Animancer.ClipTransition _moveClip; // Move(落地后地面移动,可选) [Header("物理下落")] [SerializeField] private float _fallGravityScale = 3.5f; [SerializeField] private float _maxFallTime = 3f; [SerializeField] private LayerMask _groundMask; [Header("落地")] [SerializeField] private float _recoveryTime = 0.1f; [SerializeField] private BodyContactDamage _contactDamage; private Rigidbody2D _rb; protected override void Awake() { base.Awake(); _rb = GetComponentInParent(); } protected override IEnumerator ExecuteCoroutine() { Phase = AbilityRunState.Active; // ① 播放 Fall 动画(能力脚本负责动画,不依赖 BT/BD_PlayAnimation) _animancer.Play(_fallLoopClip); // ② 切换物理:Kinematic → Dynamic + 重力 var origType = _rb.bodyType; var origGrav = _rb.gravityScale; _rb.bodyType = RigidbodyType2D.Dynamic; _rb.gravityScale = _fallGravityScale; _rb.velocity = Vector2.zero; // ③ 等待落地 float t = 0f; while (t < _maxFallTime) { t += Time.fixedDeltaTime; yield return new WaitForFixedUpdate(); if (t > 0.05f && IsGrounded()) break; } _rb.velocity = Vector2.zero; // ④ 落地后启用接触伤害 if (_contactDamage != null) _contactDamage.enabled = true; yield return EnemyAbilityWaits.Get(_recoveryTime); _enemy.SetAiPhase(AiPhase.Patrol); // 自动播放 AnimConfig.Walk = Move } private bool IsGrounded() { var hit = Physics2D.Raycast(_rb.position, Vector2.down, 0.6f, _groundMask); return hit.collider != null; } protected override void OnInterrupted(InterruptReason reason) { if (_rb != null) _rb.velocity = Vector2.zero; if (_contactDamage != null) _contactDamage.enabled = false; } } ` > ⚠️ **为何不复用 `CeilingDropAbility`**:`CeilingDropAbility` 是 `sealed` 类且不含任何动画逻辑,无法被子类化。 > 依据动画所有权原则,能力脚本对动画负完整责任,因此用 `AnimatedCeilingDropAbility` 统一封装动画 + 物理下落。 **双路初始化**(`E003_YouZhi : EnemyBase` 子类,能力脚本已含动画,直接调 Execute 即可): `csharp // 对象池生成路径(E005死亡触发) public override void OnSpawn() { base.OnSpawn(); if (_activateOnSpawn) Abilities.Get("e003_fall")?.Execute(); // 能力内部自行播放 Fall 动画 } // 预置路径(场景战斗触发器调用) public void ActivateFromCeiling() { Abilities.Get("e003_fall")?.Execute(); // 同上,无需外部播动画 } ` #### Prefab 结构 ` E003_YouZhi ├── [E003_YouZhi] [EnemyMovement] [EnemyNavAgent] [NavAgent] [TransformBasedMovement] ├── [EnemySensorHub] → slots: [{aggro, RangeSensor2D}] ├── [Rigidbody2D] Kinematic(AnimatedCeilingDropAbility 切换为 Dynamic) ├── HurtBox/ ├── ContactDamageZone/ [BodyContactDamage enabled=false] ← 落地后由能力 Enable └── Abilities/ [AnimatedCeilingDropAbility (_config=ABL_E003_Fall)] ` #### AnimConfig 映射(E003 特有) | AnimConfig 字段 | 映射 Clip | 说明 | |----------------|-----------|------| | `Idle` | Idle(天花板吸附待机) | EnemyBase.Start() 默认播放 | | `Walk` | Move(地面慢速爬行) | `SetAiPhase(Patrol)` 自动播放 | | `Run` | Skill(加速追击,循环) | `SetAiPhase(Chase)` 自动播放 | | `Dead` | Death | `EnemyBase.Die()` 自动播放 | > Fall 动画 Clip 直接配置在 `AnimatedCeilingDropAbility._fallLoopClip`(SerializeField),不占用 AnimConfig 字段。 > AnimConfig.Alert 字段**不再使用**(之前因绕过能力脚本才需要此映射,现已废除)。 #### 行为树 ` Selector ├── BD_IsStateMatch(Dead) → BD_StopMovement ← Die() 已自动播放 AnimConfig.Dead ├── Sequence [下落(一次)] │ ├── BD_IsAiPhase(Idle) │ └── BD_UseAbility(ABL_E003_Fall) ← AnimatedCeilingDropAbility 内部播 Fall 动画 + 物理下落 + SetAiPhase(Patrol) ├── Sequence [感知追击] │ ├── BD_IsSensorDetecting("aggro") ← range-based 感知门控,无需 IsAiPhase 预检 │ └── BD_ChasePlayer ← OnStart: SetAiPhase(Chase)+播 AnimConfig.Run=Skill;丢失目标返回 Failure └── BD_Patrol ← OnStart: SetAiPhase(Patrol)+播 AnimConfig.Walk=Move;兜底巡逻 ` > ⚠️ **BodyContactDamage 开关**:由 `AnimatedCeilingDropAbility.ExecuteCoroutine()` 在落地后直接 Enable(见上方能力代码 ④)。 > 无需在 BT 中额外处理,也无需重写 `SetAiPhase` 钩子。 --- ### E004 蛭母(小Boss) **核心难点**:三技能、Flip 仅在非技能时触发、死亡两阶段(Death_Pre 无敌)、出场剧情 #### SO 配置(5个 AbilitySO,放 `Assets/_Game/Data/Enemies/E004/`) | 文件 | abilityId | 关键配置 | |------|-----------|---------| | `ABL_E004_Appear.asset` | `"e004_appear"` | cooldown=0(一次性) | | `ABL_E004_Bite.asset` | `"e004_bite"` | preferredMaxRange=近战半径 | | `ABL_E004_HeadSlam.asset` | `"e004_headslam"` | preferredMaxRange=中距半径,interruptOnHurt=false(霸体砸地) | | `ABL_E004_Acid.asset` | `"e004_acid"` | preferredMinRange=2,无近距触发 | | `ABL_E004_Flip.asset` | `"e004_flip"` | cooldown=0.3 | #### AnimConfig 映射(E004 特有) | AnimConfig 字段 | 映射 Clip | 说明 | |----------------|-----------|------| | `Idle` | **Static**(1 帧休眠循环) | `EnemyBase.Start()` 自动播放;BT 激活前呈休眠姿态 | | `Walk` | Move(战斗中移动) | `SetAiPhase(Patrol)` 对应 | | `Dead` | Death(爆体消散) | `EnemyBase.Die()` 自动播放 | > Appear(出场嚎叫)动画 Clip 直接配置在 `AppearAbility._appearClip`(SerializeField),不占用 AnimConfig 字段。 > AnimConfig.Alert 字段**不使用**——出场动画由能力脚本负责,不通过 BT BD_PlayAnimation。 > ⚠️ **AnimConfig.Idle = Static 的含义**:EnemyBase.Start() 播放 Idle(=Static 1帧循环),BT 还未触发时 E004 呈现休眠状态; > BT 激活后出场能力播放 Appear,结束后进入战斗循环,Static 动画不会再出现。 #### Ability 实现 **出场能力 → 新建 `AppearAbility : EnemyAbilityBase`**: `csharp public class AppearAbility : EnemyAbilityBase { [SerializeField] private Animancer.ClipTransition _appearClip; // Appear(登场嚎叫,单次) protected override IEnumerator ExecuteCoroutine() { Phase = AbilityRunState.Active; _animancer.Play(_appearClip); yield return EnemyAbilityWaits.Get(_appearClip.Clip.length); _enemy.SetAiPhase(AiPhase.Combat); // 出场结束 → 进入战斗阶段(Combat 无自动动画,后续移动由 BT BD_MoveToPlayer 驱动) } } ` **Skill01 撕咬 → 直接用 `MeleeAttackAbility`**: ` 配置: - _hitBoxSlots: [{slotName="bite", hitBox=BiteHitBox}] - attackSequence[0].clip: Skill01 ClipTransition - attackSequence[0].hitBoxSlot: "bite" - attackSequence[0].hitBoxEnterT: 0.30 - attackSequence[0].hitBoxExitT: 0.60 ` **Skill02 头槌 → 新建 `RepeatSlamAbility : EnemyAbilityBase`**: `csharp [SerializeField] private Animancer.ClipTransition _startClip, _loopClip, _endClip; [SerializeField] private HitBox _hitBox; [SerializeField] private int _slamCount = 2; [SerializeField] private float _hitActiveTime = 0.15f; [SerializeField] private float _staggerDuration = 1.2f; protected override IEnumerator ExecuteCoroutine() { _animancer.Play(_startClip); yield return EnemyAbilityWaits.Get(_startClip.Clip.length); for (int i = 0; i < _slamCount; i++) { _animancer.Play(_loopClip); float pre = _loopClip.Clip.length - _hitActiveTime - 0.05f; yield return EnemyAbilityWaits.Get(pre); _hitBox.Activate(_config.attackSequence[0].damageSource, _transform); yield return EnemyAbilityWaits.Get(_hitActiveTime); _hitBox.Deactivate(); if (i < _slamCount - 1) yield return EnemyAbilityWaits.Get(0.1f); } // 霸体(AbilitySO.interruptOnHurt=false)期间 HurtBox 始终开启,E004 始终可被伤害 // 不做 _hurtBox.enabled 切换——霸体 ≠ 无敌,只是不被打断 _animancer.Play(_endClip); yield return EnemyAbilityWaits.Get(_endClip.Clip.length + _staggerDuration); } protected override void OnInterrupted(InterruptReason reason) { _hitBox?.Deactivate(); } ` **Skill03 酸液 → 直接用 `ProjectileAttackAbility`**: ` 配置: - _muzzle: AcidMuzzle Transform - attackSequence[0].clip: Skill_03 ClipTransition - attackSequence[0].projectileConfig: Proj_ZhiMu_Acid_ConfigSO (PoolKey="PROJ_ZhiMu_Acid", Speed=-, GravityScale=1, LaunchAngleDeg=45) - attackSequence[0].projectileCount: 3 - attackSequence[0].spreadAngleDeg: 30 - attackSequence[0].projectileFireT: 0.5 对应 ArcProjectile 弹体 ` **Flip → 新建 `FacePlayerAbility : EnemyAbilityBase`**: `csharp [SerializeField] private Animancer.ClipTransition _faceClip; protected override IEnumerator ExecuteCoroutine() { _enemy.Movement?.FaceTarget(_enemy.PlayerTransform.position); _animancer.Play(_faceClip); yield return EnemyAbilityWaits.Get(_faceClip.Clip.length); } ` Flip 触发判断:`FacePlayerAbility.CanUse` 重写,同时检测:①无其他技能正在执行、②玩家在背后: `csharp public override bool CanUse => base.CanUse && !_enemy.Abilities.All.Any(a => a != this && a.IsRunning) && IsPlayerBehind(); private bool IsPlayerBehind() { if (_enemy.PlayerTransform == null) return false; float dx = _enemy.PlayerTransform.position.x - _enemy.transform.position.x; return (_enemy.Movement.FacingDirection > 0 && dx < 0) || (_enemy.Movement.FacingDirection < 0 && dx > 0); } ` > ⚠️ **为什么在 CanUse 里而不是 BT 里检测"无其他技能运行"**:`BD_IsAbilityRunning` 节点需要指定**具体 AbilitySO 引用**,框架中**不存在"任意技能"变体**。因此"无其他技能运行"的前置条件必须在 `CanUse` 内部通过 `_enemy.Abilities.All` 迭代实现。BT 层只需 `BD_CanUseAbility(ABL_E004_Flip)`,其余逻辑由 `CanUse` 统一处理。 **死亡两阶段(`E004_ZhiMu : EnemyBase` 子类)**: `csharp [SerializeField] private Animancer.ClipTransition _deathPreClip; [SerializeField] private HurtBox _hurtBox; [SerializeField] private float _deathPreDuration = 3f; protected override void Die() { StartCoroutine(DeathSequence()); } private IEnumerator DeathSequence() { GetComponent()?.DisableBehavior(); _hurtBox.enabled = false; _animancer.Play(_deathPreClip); yield return EnemyAbilityWaits.Get(_deathPreDuration); base.Die(); // 触发 EnemyFeedback.OnDeath + 对象池回收 } ` #### Prefab 结构 ` E004_ZhiMu ├── [E004_ZhiMu] [EnemyMovement] [EnemyNavAgent] [NavAgent] [TransformBasedMovement] ├── [EnemySensorHub] → slots: [{aggro, RangeSensor2D}, {los, LOSSensor2D}] ├── [EnemyFeedback] [Rigidbody2D] ├── HurtBox/ BiteHitBox/ SlamHitBox/ ├── AcidMuzzle/ [Transform] └── Abilities/ ├── [AppearAbility] (abilityId="e004_appear") ├── [MeleeAttackAbility] (abilityId="e004_bite") ├── [RepeatSlamAbility] (abilityId="e004_headslam") ├── [ProjectileAttackAbility] (abilityId="e004_acid") └── [FacePlayerAbility] (abilityId="e004_flip") ` #### 行为树 ` Selector ├── BD_IsStateMatch(Dead) → BD_StopMovement ├── Sequence [出场(一次性)] │ ├── BD_IsAiPhase(Idle) │ └── BD_UseAbility(ABL_E004_Appear) ← AppearAbility 内部播 Appear 动画 + SetAiPhase(Combat) ├── Sequence [战斗] │ ├── BD_IsAiPhase(Combat) │ └── Selector │ ├── Sequence [Flip—技能间隙+背后] │ │ ├── BD_CanUseAbility(ABL_E004_Flip) ← CanUse 内部已包含"无其他技能运行+玩家在背后" │ │ └── BD_UseAbility(ABL_E004_Flip) │ ├── Sequence [Skill01 近距] │ │ ├── BD_CanUseAbility(ABL_E004_Bite, m_CheckRange=true) │ │ └── BD_UseAbility(ABL_E004_Bite) │ ├── Sequence [Skill02 中距] │ │ ├── BD_CanUseAbility(ABL_E004_HeadSlam, m_CheckRange=true) │ │ └── BD_UseAbility(ABL_E004_HeadSlam) │ ├── Sequence [Skill03 远程] │ │ ├── BD_CanUseAbility(ABL_E004_Acid) │ │ └── BD_UseAbility(ABL_E004_Acid) │ └── BD_MoveToPlayer └── BD_MoveToPlayer ` --- ### E005 肥蛭(精英怪) **核心难点**:撕咬后摇脆弱窗口、Death_Pre 无敌 + AnimationEvent 生成 E003 #### SO 配置 **`ABL_E005_Bite.asset`**:abilityId=`"e005_bite"`,preferredMaxRange=近战 **`ABL_E005_Acid.asset`**:abilityId=`"e005_acid"`,无范围限制 #### Ability 实现 **Skill01 撕咬(新建 `MeleeVulnerabilityAbility : EnemyAbilityBase`,含后摇脆弱窗口)**: `csharp [SerializeField] private Animancer.ClipTransition _attackClip; [SerializeField] private HitBox _hitBox; [SerializeField] private HurtBox _hurtBox; [SerializeField, Range(0,1)] private float _hitEnterT = 0.30f; [SerializeField, Range(0,1)] private float _hitExitT = 0.60f; [SerializeField] private float _staggerDuration = 1.0f; protected override IEnumerator ExecuteCoroutine() { Phase = AbilityRunState.Active; float len = _attackClip.Clip.length; _animancer.Play(_attackClip); float t = 0f; bool active = false; while (t < len) { t += Time.deltaTime; if (!active && t >= len * _hitEnterT) { _hitBox.Activate(_config.attackSequence[0].damageSource, _transform); active = true; } if (active && t >= len * _hitExitT) { _hitBox.Deactivate(); active = false; } yield return null; } if (active) _hitBox.Deactivate(); // 后摇脆弱窗口 _hurtBox.enabled = true; yield return EnemyAbilityWaits.Get(_staggerDuration); _hurtBox.enabled = false; } protected override void OnInterrupted(InterruptReason reason) { _hitBox?.Deactivate(); if (_hurtBox != null) _hurtBox.enabled = false; } ` **Skill02 酸液 → 直接用 `ProjectileAttackAbility`**(PoolKey=`"PROJ_FeiZhi_Acid"`,连续吐出 2 颗): ` 配置: - _muzzle: AcidMuzzle Transform - attackSequence[0].clip: Skill02 ClipTransition(第一次吐出) projectileFireT=0.6, projectileCount=1, postDelay=0.2(两次吐出间隔) - attackSequence[1].clip: Skill02 ClipTransition(第二次吐出,复用同一 Clip) projectileFireT=0.6, projectileCount=1 → 实现设计需求"连续两次吐出"(每次独立一颗,非同帧双发) ` > ⚠️ `ProjectileAttackAbility.projectileCount` 是**同帧同时发射数量**,`projectileCount=2` 会同时射出 2 颗。 > 设计要求"连续两次"(连续动作×2)必须使用 **2 个 `attackSequence` 条目**,每条目 `projectileCount=1`。 **死亡生成 E003(`E005_FeiZhi : EnemyBase` 子类)**: `csharp // Death_Pre 动画适当帧 AnimationEvent 调用 SpawnProjectile("spawn_e003") public override void SpawnProjectile(string payload) { if (payload != "spawn_e003") return; var pool = ServiceLocator.GetOrDefault(); for (int i = 0; i < _spawnCount; i++) { Vector2 offset = Random.insideUnitCircle * _spawnRadius; pool?.Spawn("ENM_YouZhi", (Vector2)transform.position + offset, Quaternion.identity); } } protected override void Die() { StartCoroutine(DeathSequence()); } private IEnumerator DeathSequence() { GetComponent()?.DisableBehavior(); _hurtBox.enabled = false; _animancer.Play(_deathPreClip); // Death_Pre(含spawn AnimationEvent) yield return EnemyAbilityWaits.Get(_deathPreDuration); base.Die(); } ` #### 行为树 ` Selector ├── BD_IsStateMatch(Dead) → BD_StopMovement ├── Sequence [Skill01 近距] │ ├── BD_CanUseAbility(ABL_E005_Bite, m_CheckRange=true) │ └── BD_UseAbility(ABL_E005_Bite) ├── Sequence [Skill02 远程] │ ├── BD_CanUseAbility(ABL_E005_Acid) │ └── BD_UseAbility(ABL_E005_Acid) └── BD_ChasePlayer ← SetAiPhase(Chase)+播 AnimConfig.Run=Move;丢失目标返回 Failure ` --- ### E006 讙 **核心难点**:动画原地,跳跃位移由 `LeapAttackAbility`(Rigidbody2D 冲量)实现 #### SO 配置 **`ABL_E006_Leap.asset`**:abilityId=`"e006_leap"`,cooldown=攻击间隔 attackSequence[0].clip:Skill ClipTransition #### Ability 直接复用 **`LeapAttackAbility`**(`Scripts/Enemies/Abilities/LeapAttackAbility.cs`): ` 配置: - _jumpHeight, _maxRange: 策划填写 - _windupTime: Skill 前摇帧对应秒数 - _recoveryTime: 落地硬直 - _groundMask: Ground Layer - _landingHitBox: 爪击碰撞盒(Activate 由能力代码在检测到 IsGrounded() 后直接调用,非 AnimationEvent) ` `LeapAttackAbility` 在起跳时直接通过 `Rigidbody2D.velocity = new Vector2(vx, vy)` 施加抛物线冲量(`_rb` 字段),动画仅做原地姿态,物理引擎负责实际位移——**完全对应设计文档需求**。 #### Prefab 结构 ` E006_Huan ├── [EnemyBase] [EnemyMovement] [EnemyNavAgent] [NavAgent] [TransformBasedMovement] ├── [EnemySensorHub] → slots: [{aggro, RangeSensor2D}, {wall_ahead, RaySensor2D}, {ledge, RaySensor2D}] ├── [Rigidbody2D] Dynamic ├── HurtBox/ LandingHitBox/ └── Abilities/ [LeapAttackAbility (abilityId="e006_leap")] ` #### 行为树 ` Selector ├── BD_IsStateMatch(Dead) → BD_StopMovement ← Die() 已自动播放 AnimConfig.Dead ├── Sequence [跳跃攻击] │ ├── BD_IsSensorDetecting("aggro") │ ├── BD_CanUseAbility(ABL_E006_Leap) │ └── BD_UseAbility(ABL_E006_Leap) └── BD_Patrol [wall_ahead+ledge槽驱动翻转,无传感器则Raycast兜底] ` > **Flip 动画**:`EnemyMovement` 已原生支持转身动画,无需修改 BT。只需: > 1. `EnemyAnimationConfigSO.Turn` 配置 `Flip` Clip > 2. `EnemyMovement._enableTurnAnimation = true`(Inspector 勾选) > > `BD_Patrol` 调用 `MoveHorizontal()` → 触发 `UpdateFacing()` → 自动执行 `TurnCoroutine`(停止移动 → 播 Turn 动画 → 翻转 → 恢复),BT 层**零改动**。 > 受击/死亡/NavLink 穿越时 `CancelTurn()` 自动中断转身并立即完成翻转,无需额外处理。 #### 美术 Clip 清单 `Idle`(AnimConfig.Idle)、`Move`(AnimConfig.Walk)、`Flip`(**AnimConfig.Turn**)、`Skill`(AttackSO.attackSequence[0].clip)、`Death`(AnimConfig.Dead) ## Boss 实现计划 --- ### 嘲风 Phase 1 #### BossSkillSO 配置(4个,`Assets/_Game/Data/Enemies/ChaoFeng/`) | 文件 | skillId | availablePhaseIndices | weight | |------|---------|----------------------|--------| | `ABL_ChaoFeng_Boomerang.asset` | `"boomerang"` | [0] | 1.0 | | `ABL_ChaoFeng_FanCombo.asset` | `"fan_combo"` | [0] | 1.5 | | `ABL_ChaoFeng_TornadoSmall.asset` | `"tornado_small"` | [0] | 1.2 | | `ABL_ChaoFeng_TornadoLarge.asset` | `"tornado_large"` | [0] | 0.8 | 每个 BossSkillSO 关键字段: - `skillAnimation`:ClipTransition(对应技能主动画) - `sequenceOnMiss`:SkillSequenceSO(AttackPatternSO 链) - `vulnerabilityWindows`:弱点窗口配置 #### 回旋扇弹体(唯一需要新建的弹体类) **新建 `ReturnProjectile : Projectile`**: `csharp public class ReturnProjectile : Projectile { private enum Stage { Forward, Returning } private Stage _stage; private Transform _ownerTransform; private Vector2 _startPos; [SerializeField] private float _maxRange = 8f; [SerializeField] private float _returnSpeed = 6f; public void SetOwner(Transform owner) => _ownerTransform = owner; protected override void OnInitialized() { _stage = Stage.Forward; _startPos = transform.position; _rb.velocity = Direction * _config.Speed; } private void FixedUpdate() { if (_stage == Stage.Forward) { if (Vector2.Distance(transform.position, _startPos) >= _maxRange) { _stage = Stage.Returning; _rb.velocity = Vector2.zero; } } else { if (_ownerTransform == null) { base.ReturnToPool(); return; } Vector2 dir = ((Vector2)_ownerTransform.position - (Vector2)transform.position).normalized; _rb.velocity = dir * _returnSpeed; if (Vector2.Distance(transform.position, _ownerTransform.position) < 0.5f) { _ownerTransform.GetComponentInParent()?.OnBoomerangReturned(); base.ReturnToPool(); } } } } // Projectile 基类已提供:_rb, _config, Direction, Initialize(config,info,dir,layer) // ⚠️ 使用 base.ReturnToPool() 而非自定义实现: // - Projectile.ReturnToPool() 正确流程:HitBox.Deactivate() + gameObject.SetActive(false) + IObjectPoolService.Despawn() // - IObjectPoolService 没有 ReturnToPool(GameObject) 方法,不可自行调用 ` #### ChaoFengBoss(继承 BossBase) > ⚠️ **BossBase 小改动(1次性)**:在 `BossBase.PhaseTransitionCoroutine` 开头调用新增的钩子: > ```csharp > // BossBase.cs 新增(PhaseTransitionCoroutine 首行): > protected virtual void OnBeginPhaseTransition(int targetPhase) { } > // PhaseTransitionCoroutine 第一行(IsPhaseTransitioning=true 后)调用: > OnBeginPhaseTransition(targetPhase); > ``` > `ChaoFengBoss` 重写此钩子,使过渡动画与浮空协程**在无敌期开始时**立即启动,而非在无敌期结束后的 `EnterPhase` 中启动。 **新建 `ChaoFengBoss : BossBase`**: `csharp public class ChaoFengBoss : BossBase { [SerializeField] private ChaoFengFloatController _floatController; [SerializeField] private ChaoFengKnockdownCounter _knockdownCounter; [SerializeField] private Animancer.ClipTransition _phaseTransitionClip; [SerializeField] private Animancer.ClipTransition _boomerangEndClip; [SerializeField] private Transform _boomerangMuzzle, _tornadoMuzzle, _windStoneMuzzle; // ⚠️ BossBase.BeginPhaseTransition 在无敌期结束后才调用 EnterPhase。 // 为使过渡动画在无敌期开始时播放、并确保浮空在 Phase 2 战斗开始前完成, // 需要在 BossBase.PhaseTransitionCoroutine 开头插入: // protected virtual void OnBeginPhaseTransition(int targetPhase) { } // 然后 ChaoFengBoss 重写此钩子以播放动画并启动浮空。 // 同时将 BD_BossPhaseTransition.invincibleDuration 设为 _riseDuration+buffer(约2.0s)。 protected override void OnBeginPhaseTransition(int targetPhase) { if (targetPhase == 1) { Animancer.Play(_phaseTransitionClip); // 在无敌期开始时播放过渡动画 StartCoroutine(_floatController.FloatUp()); // 同步开始浮空(_riseDuration≈1.5s) } } public override void EnterPhase(int phase) { base.EnterPhase(phase); // 动画与浮空已在 OnBeginPhaseTransition 中启动,无需重复处理 } // ⚠️ HurtBox 没有公开 OnDamageTaken 事件;必须通过 EnemyBase.OnDamageTaken 虚方法转发 protected override void OnDamageTaken(DamageInfo info) { base.OnDamageTaken(info); _knockdownCounter?.OnBossHit(info); } public void OnBoomerangReturned() => Animancer.Play(_boomerangEndClip); public override void SpawnProjectile(string payload) { var pool = ServiceLocator.GetOrDefault(); switch (payload) { case "boomerang": var go = pool?.Spawn("PROJ_Boomerang", _boomerangMuzzle.position, Quaternion.identity); go?.GetComponent()?.SetOwner(transform); break; case "tornado_small": pool?.Spawn("PROJ_TornadoSmall", _tornadoMuzzle.position, Quaternion.identity); break; case "tornado_large": if (PlayerTransform != null) pool?.Spawn("PROJ_TornadoLarge", PlayerTransform.position, Quaternion.identity); break; case "wind_stone": pool?.Spawn("PROJ_WindStone", _windStoneMuzzle.position, Quaternion.identity); break; } } } ` #### 击败演出序列(ChaoFengBoss.Die 重写) 设计需求(Boss设计-动作需求表-01.md):`Defeat_Struggle`(空中挣扎循环)→ 白屏 → `Defeat_Pant`(地面喘气循环)→ `Defeat_StandUp`(站起,单次)。 > ⚠️ `Stagger`(击落后硬直)复用 `Defeat_Pant` Clip,已由 `ChaoFengKnockdownCounter._staggerClip` 引用。 将以下字段和方法追加到 `ChaoFengBoss` 类: `csharp // 在 ChaoFengBoss 类中追加: [SerializeField] private Animancer.ClipTransition _defeatStruggleClip; // Defeat_Struggle(循环) [SerializeField] private Animancer.ClipTransition _defeatPantClip; // Defeat_Pant(循环,复用作Stagger) [SerializeField] private Animancer.ClipTransition _defeatStandUpClip; // Defeat_StandUp(单次) [SerializeField] private float _defeatPantDuration = 3f; // 倒地喘气时长 [SerializeField] private UnityEngine.Events.UnityEvent _onDefeatWhiteFlash; // 白屏时机回调(可接 CameraManager/VFX) protected override void Die() { StartCoroutine(DefeatSequence()); } private IEnumerator DefeatSequence() { GetComponent()?.DisableBehavior(); _knockdownCounter?.ForceEnd(); // 打断进行中的击落序列 // Phase 2(空中)先落地 if (CurrentPhase >= 1) yield return _floatController.FallDown(); // 空中挣扎(Defeat_Struggle) Animancer.Play(_defeatStruggleClip); yield return EnemyAbilityWaits.Get(_defeatStruggleClip.Clip.length); // 白屏效果(由挂载 UnityEvent 或 CameraManager GameEvent 触发) _onDefeatWhiteFlash?.Invoke(); // 倒地喘气(Defeat_Pant 循环) Animancer.Play(_defeatPantClip); yield return EnemyAbilityWaits.Get(_defeatPantDuration); // 站起(Defeat_StandUp 单次) Animancer.Play(_defeatStandUpClip); yield return EnemyAbilityWaits.Get(_defeatStandUpClip.Clip.length); // 广播战斗结束、触发结算过场 base.Die(); } ` **`ChaoFengKnockdownCounter` 需追加 `ForceEnd()` 方法**,供 `DefeatSequence` 打断进行中的击落协程: `csharp // 在 ChaoFengKnockdownCounter 中追加: public void ForceEnd() { StopAllCoroutines(); _inKnockdown = false; _count = 0; } ` #### 完整行为树(Phase 1 + Phase 2 合并) > ⚠️ **不要将 Phase 1 和 Phase 2 写成两棵独立的行为树**。嘲风只有一棵 BT,Phase 2 子树通过 `BD_IsHPBelow(0.5)` 守门,Phase 过渡节点**必须内嵌于 Phase 2 Sequence 中**,而非作为独立兄弟节点——见下方结构说明。 ` Selector [嘲风] ├── Sequence [Phase 2(HP低于50%)] │ ├── BD_IsHPBelow(0.5) │ ├── BD_BossPhaseTransition(targetPhase=1, invincibleDuration=2.0) │ │ ← invincibleDuration≥_riseDuration(约1.5s)+缓冲,确保动画与浮空在 Phase 2 战斗前完成 │ │ ← 首次: Running(过渡中,无敌)→ Success(完成,CurrentPhase=1) │ │ ← 后续每 tick: 因守护(CurrentPhase>=1)立即返回 Success,Sequence 直接进入下方战斗节点 │ └── Selector [Phase 2 战斗] │ ├── BD_UseBossSkillWeighted ← 仅 wind_stone(availablePhaseIndices=[1]) │ └── BD_Wait(0.5) │ └── Selector [Phase 1 地面战斗] ← HP>50% 时走此分支 ├── BD_UseBossSkillWeighted ← 4个Phase0技能加权随机 └── BD_MoveToPlayer ` > ⚠️ **为何不能将 Sequence[Phase过渡] 与 Sequence[Phase2战斗] 并列为兄弟节点**: > 若两者为兄弟(`Selector → Seq[过渡] + Seq[Phase2战斗]`),过渡完成后 Seq[过渡] 每 tick 返回 Success, > 父 Selector 在第一个子节点成功后立即返回 Success,**永远不会执行到 Seq[Phase2战斗]**,导致 Phase 2 战斗逻辑失效。 > **正确做法**:将 `BD_BossPhaseTransition` 作为 Phase 2 Sequence 的中间节点,过渡完成后 Sequence 继续执行后续战斗节点。 > > `BD_BossPhaseTransition.OnUpdate()` 中的阶段守护(`if (_boss.CurrentPhase >= m_TargetPhase) return TaskStatus.Success`) > 已应用到 `Assets/_Game/Scripts/Enemies/AI/BD_BossPhaseTransition.cs`,保证重入时立即返回 Success 而不重触发过渡。 击落由 `ChaoFengKnockdownCounter` 独立事件驱动,不走行为树。 --- ### 嘲风 Phase 2 + 击落机制 #### Phase 2 BossSkillSO **`ABL_ChaoFeng_WindStone.asset`**:skillId=`"wind_stone"`,availablePhaseIndices=[1] #### 漂浮控制(新建 `ChaoFengFloatController`) `csharp public class ChaoFengFloatController : MonoBehaviour { [SerializeField] private float _floatHeight = 5f; [SerializeField] private float _riseDuration = 1.5f; [SerializeField] private float _fallDuration = 0.8f; [SerializeField] private Rigidbody2D _rb; private float _groundY; private void Start() => _groundY = transform.position.y; public IEnumerator FloatUp() { _rb.bodyType = RigidbodyType2D.Kinematic; _rb.velocity = Vector2.zero; yield return TweenY(transform.position.y, _groundY + _floatHeight, _riseDuration); } public IEnumerator FallDown() { yield return TweenY(transform.position.y, _groundY, _fallDuration); _rb.bodyType = RigidbodyType2D.Dynamic; } private IEnumerator TweenY(float from, float to, float duration) { for (float t = 0f; t < duration; t += Time.deltaTime) { _rb.MovePosition(new Vector2(transform.position.x, Mathf.Lerp(from, to, t / duration))); yield return null; } _rb.MovePosition(new Vector2(transform.position.x, to)); } } ` > 若项目已引入 DOTween,可用 `transform.DOMoveY()` 替代 Coroutine TweenY,效果更平滑。 #### 击落计数(新建 `ChaoFengKnockdownCounter`) > ⚠️ **架构约束**:`HurtBox` 没有公开的 `OnDamageTaken` C# 事件。 > 正确做法:`ChaoFengBoss.OnDamageTaken(DamageInfo)` 虚方法重写后**直接调用**本组件的 `OnBossHit` 方法,无需订阅 HurtBox 事件。 `csharp public class ChaoFengKnockdownCounter : MonoBehaviour { [SerializeField] private int _threshold = 8; [SerializeField] private ChaoFengBoss _boss; [SerializeField] private ChaoFengFloatController _floatCtrl; [SerializeField] private Animancer.ClipTransition _knockdownHitClip; [SerializeField] private Animancer.ClipTransition _staggerClip; // 复用 Defeat_Pant [SerializeField] private float _staggerDuration = 3f; private int _count; private bool _inKnockdown; // 由 ChaoFengBoss.OnDamageTaken() 重写后直接调用(不订阅 HurtBox 事件) public void OnBossHit(DamageInfo info) { if (_inKnockdown || _boss.CurrentPhase != 1) return; // ⚠️ 玩家是否在空中的判断方式待确认(见Q6) // 临时实现:所有命中均计数 _count++; if (_count >= _threshold) { _count = 0; StartCoroutine(KnockdownSequence()); } } private IEnumerator KnockdownSequence() { _inKnockdown = true; _boss.GetComponentInChildren()?.InterruptCurrentSkill(); _boss.Animancer.Play(_knockdownHitClip); yield return _floatCtrl.FallDown(); _boss.Animancer.Play(_staggerClip); yield return EnemyAbilityWaits.Get(_staggerDuration); yield return _floatCtrl.FloatUp(); _inKnockdown = false; } } ` #### 完整 Prefab 结构 ` ChaoFeng ├── [ChaoFengBoss] [EnemyMovement] [EnemyNavAgent] [NavAgent] [TransformBasedMovement] ├── [BossSkillExecutor] ├── [ChaoFengFloatController] ├── [ChaoFengKnockdownCounter] ├── [EnemyFeedback] [Rigidbody2D] ├── HurtBox/ ├── Phase1HitBoxes/ → FanCombo_HitBox[x3], Tornado_HitBox ├── Muzzles/ → BoomerangMuzzle, TornadoMuzzle, WindStoneMuzzle └── BehaviorTree [BehaviorDesigner] ` 击落由 `ChaoFengKnockdownCounter` 独立事件驱动,不走行为树。 --- ## 资产路径规范 ` Assets/_Game/ ├── Scripts/Enemies/ │ ├── Abilities/ │ │ ├── PlayClipAbility.cs ← 播放单 Clip,无副作用(E001 激活) │ │ ├── ContactChaseAbility.cs ← 追击循环 + 接触伤害(E001 追击) │ │ ├── CeilingHangStrikeAbility.cs ← 天花板三段攻击+脆弱窗口(E002) │ │ ├── AnimatedCeilingDropAbility.cs ← 带动画的天花板跌落(E003) │ │ ├── AppearAbility.cs ← 出场动画+进入战斗(E004,可复用) │ │ ├── RepeatSlamAbility.cs ← 可配置次数砸地(E004 头槌) │ │ ├── FacePlayerAbility.cs ← 朝向玩家翻转含动画(E004 Flip) │ │ └── MeleeVulnerabilityAbility.cs ← 近战+后摇脆弱窗口(E005 撕咬) │ ├── Boss/ │ │ ├── ChaoFengBoss.cs │ │ ├── ChaoFengFloatController.cs │ │ ├── ChaoFengKnockdownCounter.cs │ │ └── ReturnProjectile.cs │ ├── E003_YouZhi.cs │ ├── E004_ZhiMu.cs │ └── E005_FeiZhi.cs ├── Data/Enemies/ │ ├── E001/ │ │ ├── ENM_E001_Stats.asset ← EnemyStatsSO(ENM_ 前缀) │ │ ├── ABL_E001_Activate.asset ← PlayClipAbility SO(ABL_ 前缀) │ │ └── ABL_E001_Chase.asset ← ContactChaseAbility SO │ ├── E002/ │ │ ├── ENM_E002_Stats.asset │ │ └── ABL_E002_CeilingStrike.asset │ ├── E003/ │ │ ├── ENM_E003_Stats.asset │ │ └── ABL_E003_Fall.asset │ ├── E004/ │ │ ├── ENM_E004_Stats.asset │ │ ├── ABL_E004_Appear.asset │ │ ├── ABL_E004_Bite.asset │ │ ├── ABL_E004_HeadSlam.asset │ │ ├── ABL_E004_Acid.asset │ │ └── ABL_E004_Flip.asset │ ├── E005/ │ │ ├── ENM_E005_Stats.asset │ │ ├── ABL_E005_Bite.asset │ │ └── ABL_E005_Acid.asset │ ├── E006/ │ │ ├── ENM_E006_Stats.asset │ │ └── ABL_E006_Leap.asset │ └── ChaoFeng/ ← Boss 嘲风(Data/Enemies/ 下,无独立 Data/Boss/ 目录) │ ├── ENM_ChaoFeng_Stats.asset │ ├── ABL_ChaoFeng_Boomerang.asset │ ├── ABL_ChaoFeng_FanCombo.asset │ ├── ABL_ChaoFeng_TornadoSmall.asset │ ├── ABL_ChaoFeng_TornadoLarge.asset │ └── ABL_ChaoFeng_WindStone.asset ← Phase 2 技能 ├── Prefabs/Enemies/ │ ├── E001/ ENM_CaoZhi.prefab ← Address: ENM_CaoZhi Group: Enemies Labels: Enemy │ ├── E002/ ENM_HuangZhi.prefab ← Address: ENM_HuangZhi Group: Enemies Labels: Enemy │ ├── E003/ ENM_YouZhi.prefab ← Address: ENM_YouZhi Group: Enemies Labels: Enemy, Poolable, Preload(可被 E005 生成) │ ├── E004/ ENM_ZhiMu.prefab ← Address: ENM_ZhiMu Group: Enemies Labels: Enemy │ ├── E005/ ENM_FeiZhi.prefab ← Address: ENM_FeiZhi Group: Enemies Labels: Enemy │ ├── E006/ ENM_Huan.prefab ← Address: ENM_Huan Group: Enemies Labels: Enemy │ └── ChaoFeng/ ENM_ChaoFeng.prefab ← Address: ENM_ChaoFeng Group: Boss_ChaoFeng ├── Prefabs/Combat/Projectiles/ │ ├── PROJ_Boomerang.prefab ← Address: PROJ_Boomerang Group: Projectiles Labels: Poolable, Preload │ ├── PROJ_AcidBlob.prefab ← Address: PROJ_AcidBlob Group: Projectiles Labels: Poolable, Preload │ └── [其余抛射物 PROJ_*.prefab 均纳入对象池] ├── Art/Characters/Enemies/ │ ├── E001/ Sprites/ Animations/ Atlases/ Materials/ │ ├── E002/ Sprites/ Animations/ Atlases/ Materials/ │ ├── E003/ Sprites/ Animations/ Atlases/ Materials/ │ ├── E004/ Sprites/ Animations/ Atlases/ Materials/ │ ├── E005/ Sprites/ Animations/ Atlases/ Materials/ │ ├── E006/ Sprites/ Animations/ Atlases/ Materials/ │ └── ChaoFeng/ Sprites/ Animations/ Atlases/ Materials/ └── Art/Effects/ └── [VFX Prefab 对应帧序列图在 Art/Effects/Sprites/ 下,命名 VFX_{Description}.png] ` ### AnimationClip 命名规则 | 目录 | 文件命名 | 示例 | |------|---------|------| | `Art/Characters/Enemies/{EnemyID}/Sprites/` | `{ID}_{Name}_{Action}.png` | `E001_CaoZhi_Idle.png` | | `Art/Characters/Enemies/{EnemyID}/Animations/` | `{Action}.anim`(无前缀) | `Idle.anim`、`Skill_Start.anim` | | `Art/Characters/Enemies/{EnemyID}/Animations/` | `{ID}_{Name}_Animator.controller` | `E001_CaoZhi_Animator.controller` | | `Art/Characters/Enemies/{EnemyID}/Materials/` | `ENM_{ID}.mat` | `ENM_E001.mat` | | `Art/Characters/Enemies/{EnemyID}/Atlases/` | `Atlas_Enemy_{ID}.spriteatlas` | `Atlas_Enemy_E001.spriteatlas` | ### Addressables 标签汇总 | 资产类型 | 命名 | Address | Group | Labels | |---------|------|---------|-------|--------| | 小怪 Prefab(E001/E002/E004-E006) | `ENM_{Name}.prefab` | `ENM_{Name}` | `Enemies` | `Enemy` | | E003 幼蛭(可被 E005 生成) | `ENM_YouZhi.prefab` | `ENM_YouZhi` | `Enemies` | `Enemy`, `Poolable`, `Preload` | | Boss 嘲风 Prefab | `ENM_ChaoFeng.prefab` | `ENM_ChaoFeng` | `Boss_ChaoFeng` | — | | 抛射物 Prefab | `PROJ_{Name}.prefab` | `PROJ_{Name}` | `Projectiles` | `Poolable`, `Preload` | | VFX Prefab | `VFX_{Name}.prefab` | `VFX_{Name}` | `VFX_Common` | `Poolable`, `Preload` | | StatsSO / AbilitySO / AnimConfigSO | `ENM_*.asset` / `ABL_*.asset` | **不注册 Addressable** | — | Inspector 直接引用 | > ⚠️ 代码中加载敌人 Prefab 时,**必须**使用 `AddressKeys.Labels.Enemy` 常量(禁止硬编码 `"Enemy"` 字符串)。E005 生成 E003 时,pool key = `"ENM_YouZhi"`(与 Address 一致)。 --- ## 实现顺序 | 阶段 | 内容 | 新建代码 | |------|------|---------| | 1 | E006 讙 | 无(LeapAttackAbility 直接复用) | | 1 | E003 幼蛭 | E003_YouZhi.cs(继承EnemyBase,OnSpawn/ActivateFromCeiling) | | 2 | E001 草蛭 | PlayClipAbility.cs, ContactChaseAbility.cs | | 2 | E002 簧蛭 | CeilingHangStrikeAbility.cs | | 3 | E005 肥蛭 | MeleeVulnerabilityAbility.cs, E005_FeiZhi.cs | | 4 | E004 蛭母 | AppearAbility.cs, RepeatSlamAbility.cs, FacePlayerAbility.cs, E004_ZhiMu.cs | | 5 | 嘲风 Phase 1 | ChaoFengBoss.cs, ReturnProjectile.cs | | 5 | 嘲风 Phase 2 | ChaoFengFloatController.cs, ChaoFengKnockdownCounter.cs | --- ## 待策划确认项 | # | 问题 | 影响范围 | |---|------|---------| | Q1 | 嘲风 Phase 2 能否用 Phase 1 技能? | BossSkillSO.availablePhaseIndices | | Q2 | 击落计数是否打断风石施法? | ChaoFengKnockdownCounter 中断逻辑 | | Q3 | 挥扇三连第3击是否有后方碰撞盒? | AttackPatternSO HitBox 形状 | | Q4 | E004 Flip 背后检测方案(Sensor槽还是 FacingDirection 比较)? | EnemySensorHub 配置 | | Q5 | E005 是否有 Flip 动画? | 是否新建 Flip Ability | | Q6 | 玩家空中判断方式(DamageInfo 携带/PlayerController 事件)? | ChaoFengKnockdownCounter.IsAttackerAirborne | | Q7 | 各角色 HP/速度/伤害/CD 数值 | 所有 StatsSO / AbilitySO 填写 | | Q8 | 嘲风 Phase 2 浮空待机是否复用 Phase 1 Idle? | 美术制作量 |