1335 lines
55 KiB
Markdown
1335 lines
55 KiB
Markdown
# 小怪与 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-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<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] 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<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].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<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 写成两棵独立的行为树**。嘲风只有一棵 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<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 ← 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? | 美术制作量 | |