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

55 KiB
Raw Permalink Blame History

小怪与 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. 能力注册机制

EnemyAbilityRegistryEnemyBase.Awake() 时调用 GetComponentsInChildren<EnemyAbilityBase>(true) 自动扫描所有子 GameObjectEnemyAbilitySO.abilityId 注册。

  • 能力组件必须挂在 Prefab 的子对象上(或根对象的子层级)
  • abilityId 必须全局唯一,不可重复
  • BD Task BD_UseAbilityBD_CanUseAbilityBD_IsAbilityRunning 都通过 abilityId 或 SO 引用查找能力

3. 导航系统约束

  • 框架使用 PathBerserker2dNavAgent + TransformBasedMovement + EnemyNavAgent
  • EnemyBase.MoveTo(Vector2)_nav.RequestMoveTo(target)NavAgent.UpdatePath(target)
  • EnemyMovement.MoveHorizontal(dir) 使用 EnemyStatsSO.WalkSpeedMoveWithSpeed(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_aheadledge 槽的 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# 事件:OnDiedevent System.Action)、OnAiPhaseChangedevent 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/E006EnemyMovement._enableTurnAnimation=true + AnimConfig.Turn 驱动,BD_Patrol 触发 MoveHorizontal() 自动播转身动画,无需任何 Ability
  • 战斗 FlipE004玩家绕到背后时主动触发是一个有 CanUse 条件门控的战斗技能,需要 FacePlayerAbility 作为独立 Ability 注册到 BT。

通用前置任务

P0-A动画 Clip 制作规范

  • 动画 Clip 放在 Assets/_Game/Art/Characters/Enemies/{EnemyID}/Animations/
  • Clip 名与需求表完全一致(区分大小写)
  • 每个能力引用的 Clip 通过 EnemyAttackSO.clipClipTransition 类型)绑定,不通过 EnemyAnimationConfigSO

P0-BLayer 配置确认

  • Enemy HitBox LayerEnemyHitBox
  • Enemy HurtBox LayerEnemyHurtBox
  • 弹体 LayerEnemyProjectile
  • 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.assetEnemyStatsSOMaxHP、WalkSpeed巡逻、RunSpeed追击、DetectRange。

ABL_E001_Activate.assetabilityId="e001_activate"cooldown=0
ABL_E001_Chase.assetabilityId="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 清单

IdleAnimConfig.Idle伪装待机MoveAnimConfig.Walk巡逻爬行FlipAnimConfig.Turn)、Skill_StartSkill_LoopSkill_EndDeathAnimConfig.Dead

Flip 动画EnemyMovement 已原生支持转身动画,无需修改 BT。只需

  1. EnemyAnimationConfigSO.Turn 配置 Flip Clip
  2. EnemyMovement._enableTurnAnimation = trueInspector 勾选)

BD_Patrol 调用 MoveHorizontal() → 触发 UpdateFacing() → 自动执行 TurnCoroutine(停止移动 → 播 Turn 动画 → 翻转 → 恢复BT 层零改动
受击/死亡/NavLink 穿越时 CancelTurn() 自动中断转身并立即完成翻转,无需额外处理。


E002 簧蛭

核心难点:固定位置(无 NavAgent/EnemyMovementSkill_Loop 为脆弱窗口

SO 配置

ENM_E002_Stats.assetMaxHP、AttackDamageWalkSpeed/RunSpeed=0固定
ABL_E002_CeilingStrike.assetabilityId="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 配置

StatsSOMaxHP=1WalkSpeedRunSpeed追击
ABL_E003_Fall.assetabilityId="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;
}

} `

⚠️ 为何不复用 CeilingDropAbilityCeilingDropAbilitysealed 类且不含任何动画逻辑,无法被子类化。
依据动画所有权原则,能力脚本对动画负完整责任,因此用 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._fallLoopClipSerializeField不占用 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个 AbilitySOAssets/_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 Static1 帧休眠循环) EnemyBase.Start() 自动播放BT 激活前呈休眠姿态
Walk Move战斗中移动 SetAiPhase(Patrol) 对应
Dead Death爆体消散 EnemyBase.Die() 自动播放

Appear出场嚎叫动画 Clip 直接配置在 AppearAbility._appearClipSerializeField不占用 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.assetabilityId="e005_bite"preferredMaxRange=近战
ABL_E005_Acid.assetabilityId="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 酸液 → 直接用 ProjectileAttackAbilityPoolKey="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

死亡生成 E003E005_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 讙

核心难点:动画原地,跳跃位移由 LeapAttackAbilityRigidbody2D 冲量)实现

SO 配置

ABL_E006_Leap.assetabilityId="e006_leap"cooldown=攻击间隔
attackSequence[0].clipSkill ClipTransition

Ability

直接复用 LeapAttackAbilityScripts/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 = trueInspector 勾选)

BD_Patrol 调用 MoveHorizontal() → 触发 UpdateFacing() → 自动执行 TurnCoroutine(停止移动 → 播 Turn 动画 → 翻转 → 恢复BT 层零改动
受击/死亡/NavLink 穿越时 CancelTurn() 自动中断转身并立即完成翻转,无需额外处理。

美术 Clip 清单

IdleAnimConfig.IdleMoveAnimConfig.WalkFlipAnimConfig.Turn)、SkillAttackSO.attackSequence[0].clipDeathAnimConfig.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 关键字段:

  • skillAnimationClipTransition对应技能主动画
  • sequenceOnMissSkillSequenceSOAttackPatternSO 链)
  • 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.mdDefeat_Struggle(空中挣扎循环)→ 白屏 → Defeat_Pant(地面喘气循环)→ Defeat_StandUp(站起,单次)。

⚠️ Stagger(击落后硬直)复用 Defeat_Pant Clip已由 ChaoFengKnockdownCounter._staggerClip 引用。

将以下字段和方法追加到 ChaoFengBoss 类:

`csharp // 在 ChaoFengBoss 类中追加: [SerializeField] private Animancer.ClipTransition _defeatStruggleClip; // Defeat_Struggle循环 [SerializeField] private Animancer.ClipTransition _defeatPantClip; // Defeat_Pant循环复用作Stagger [SerializeField] private Animancer.ClipTransition _defeatStandUpClip; // Defeat_StandUp单次 [SerializeField] private float _defeatPantDuration = 3f; // 倒地喘气时长 [SerializeField] private UnityEngine.Events.UnityEvent _onDefeatWhiteFlash; // 白屏时机回调(可接 CameraManager/VFX

protected override void Die() { StartCoroutine(DefeatSequence()); }

private IEnumerator DefeatSequence() { GetComponent()?.DisableBehavior(); _knockdownCounter?.ForceEnd(); // 打断进行中的击落序列

// Phase 2空中先落地
if (CurrentPhase >= 1)
    yield return _floatController.FallDown();

// 空中挣扎Defeat_Struggle
Animancer.Play(_defeatStruggleClip);
yield return EnemyAbilityWaits.Get(_defeatStruggleClip.Clip.length);

// 白屏效果(由挂载 UnityEvent 或 CameraManager GameEvent 触发)
_onDefeatWhiteFlash?.Invoke();

// 倒地喘气Defeat_Pant 循环)
Animancer.Play(_defeatPantClip);
yield return EnemyAbilityWaits.Get(_defeatPantDuration);

// 站起Defeat_StandUp 单次)
Animancer.Play(_defeatStandUpClip);
yield return EnemyAbilityWaits.Get(_defeatStandUpClip.Clip.length);

// 广播战斗结束、触发结算过场
base.Die();

} `

ChaoFengKnockdownCounter 需追加 ForceEnd() 方法,供 DefeatSequence 打断进行中的击落协程:

csharp // 在 ChaoFengKnockdownCounter 中追加: public void ForceEnd() { StopAllCoroutines(); _inKnockdown = false; _count = 0; }

完整行为树Phase 1 + Phase 2 合并)

⚠️ 不要将 Phase 1 和 Phase 2 写成两棵独立的行为树。嘲风只有一棵 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.assetskillId="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.animSkill_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 美术制作量