55 KiB
小怪与 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 == falseBD_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()覆写中(通过 AnimationEventSpawnProjectile回调触发),不经过 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。只需:
EnemyAnimationConfigSO.Turn配置FlipClipEnemyMovement._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()?.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。只需:
EnemyAnimationConfigSO.Turn配置FlipClipEnemyMovement._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开头调用新增的钩子:// 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_PantClip,已由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没有公开的OnDamageTakenC# 事件。
正确做法: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? | 美术制作量 |