Files
zeling_v2/Docs/Plan/小怪与Boss实现计划-01.md

1335 lines
55 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 小怪与 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<EnemyAbilityBase>(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<AiPhase>`)。
### 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-BLayer 配置确认
- 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.prefabE005死亡生成 |
| `PROJ_ZhiMu_Acid` | PROJ_ZhiMu_Acid.prefabArcProjectile |
| `PROJ_FeiZhi_Acid` | PROJ_FeiZhi_Acid.prefabArcProjectile |
| `PROJ_Boomerang` | PROJ_Boomerang.prefabReturnProjectile |
| `PROJ_TornadoSmall` | PROJ_TornadoSmall.prefab |
| `PROJ_TornadoLarge` | PROJ_TornadoLarge.prefab |
| `PROJ_WindStone` | PROJ_WindStone.prefabArcProjectile |
> ⚠️ **Pool Key 必须与 Addressable Address 完全一致**大小写、格式均匹配。Address 规则:小怪 Prefab → `ENM_*`,抛射物 Prefab → `PROJ_*`。
---
## 小怪实现计划
---
### E001 草蛭
**核心难点**:伪装默认状态,感知后 Skill_Start→Skill_Loop 追击,自定义 Clip 通过 Ability 播放
#### SO 配置
**`ENM_E001_Stats.asset`**EnemyStatsSOMaxHP、WalkSpeed巡逻、RunSpeed追击、DetectRange。
**`ABL_E001_Activate.asset`**abilityId=`"e001_activate"`cooldown=0
**`ABL_E001_Chase.asset`**abilityId=`"e001_chase"`cooldown=0preferredMaxRange=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.DeadBT 只需停止移动
├── 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/EnemyMovementSkill_Loop 为脆弱窗口
#### SO 配置
**`ENM_E002_Stats.asset`**MaxHP、AttackDamageWalkSpeed/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**WalkSpeedRunSpeed追击
**`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<Rigidbody2D>();
}
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] KinematicAnimatedCeilingDropAbility 切换为 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<BehaviorTree>()?.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<IObjectPoolService>();
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<BehaviorTree>()?.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].clipSkill 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`SkillSequenceSOAttackPatternSO 链)
- `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<ChaoFengBoss>()?.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<IObjectPoolService>();
switch (payload)
{
case "boomerang":
var go = pool?.Spawn("PROJ_Boomerang", _boomerangMuzzle.position, Quaternion.identity);
go?.GetComponent<ReturnProjectile>()?.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<BehaviorTree>()?.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 写成两棵独立的行为树**。嘲风只有一棵 BTPhase 2 子树通过 `BD_IsHPBelow(0.5)` 守门Phase 过渡节点**必须内嵌于 Phase 2 Sequence 中**,而非作为独立兄弟节点——见下方结构说明。
`
Selector [嘲风]
├── Sequence [Phase 2HP低于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立即返回 SuccessSequence 直接进入下方战斗节点
│ └── Selector [Phase 2 战斗]
│ ├── BD_UseBossSkillWeighted ← 仅 wind_stoneavailablePhaseIndices=[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<BossSkillExecutor>()?.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 ← EnemyStatsSOENM_ 前缀)
│ │ ├── ABL_E001_Activate.asset ← PlayClipAbility SOABL_ 前缀)
│ │ └── 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 |
|---------|------|---------|-------|--------|
| 小怪 PrefabE001/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继承EnemyBaseOnSpawn/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 | 美术制作量 |