Files
zeling_v2/Docs/Plan/小怪与Boss实现计划-01.md
2026-06-08 16:05:00 +08:00

58 KiB
Raw 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. 感知系统约束(自研 PhysicsPerceptionSystem

⚠️ 本节已更新:项目已自研 PhysicsPerceptionSystem 完全替代 SensorToolkit,不再使用 EnemySensorHub / SensorToolkit 命名空间。_Game 代码零引用 SensorToolkit。Prefab 上挂的是 PhysicsPerceptionSystem(纯物理实现),其 _slots[] 槽位数组配置各感知槽。

架构

  • 接口 BaseGames.Enemies.Perception.IPerceptionSystemHasSlot / IsDetecting(slot, target) / HasAnyDetection(slot) / GetFirstDetection(slot)
  • 实现 PhysicsPerceptionSystem:支持 7 种槽位类型 RangeCircle / BatchLOS / FanCast / BoxCast / Sight / RayCast / TriggerZone含错帧更新tickInterval、动态禁用、Enter/Exit 事件;视线由 SightBatchSystem 全局预算管理LOD
  • EnemyBase.Awake() 通过 GetComponentInChildren<IPerceptionSystem>() 注册,对外暴露 EnemyBase.SensorHubIPerceptionSystem 类型)

标准槽位常量(定义于 Perception/SensorSlotNames.cs,禁止散布魔法字符串):

常量 字符串键 槽位类型 用途
SensorSlotNames.Aggro aggro RangeCircle 主要警戒/追击范围
SensorSlotNames.Alert alert RangeCircle 警觉半径(比 aggro 小,待机/巡逻→Alert 切换)
SensorSlotNames.AttackMelee attack_melee RangeCircle 近战攻击距离触发
SensorSlotNames.AttackRange attack_range RangeCircle 远程攻击距离触发
SensorSlotNames.Patrol patrol RangeCircle 巡逻范围限制(超出触发返回)
SensorSlotNames.LOS los BatchLOS 无遮挡视线OverlapCircle + 单射线遮挡)
SensorSlotNames.Sight sight Sight 视锥 + 强制 LOS 遮挡检测("看见玩家"核心传感器)

⚠️ wall_ahead / ledge 槽已废除:前方墙体/悬崖检测不再走感知系统,改由 EnemyMovement.IsWallAhead / EnemyMovement.IsLedgeAheadEnemyMovement 内置物理射线)提供。

BD_IsSensorDetecting 实现:查询 gameObject.GetComponent<IPerceptionSystem>();字段 m_SlotName(应填 SensorSlotNames 对应字符串键)+ m_AnyTargettrue 时用 HasAnyDetectionfalse 时对 EnemyBase.PlayerTransformIsDetecting)。

BD_Patrol 直接读取 EnemyMovement.IsWallAhead / IsLedgeAhead 触发翻转,不依赖任何感知槽(含转身冷却防抖)。

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_Alert.assetabilityId="e001_alert"cooldown=1.5(激活/警觉序列PlayClipAbility
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 string _aggroSlotName = SensorSlotNames.Aggro; // 感知通过 EnemyBase.SensorHubIPerceptionSystem查询不自挂感知组件引用

protected override IEnumerator ExecuteCoroutine() { Phase = AbilityRunState.Active; _animancer.Play(_loopClip); _contactDamage.enabled = true;

while (true)
{
    if (_enemy.PlayerTransform == null) break;
    if (_enemy.SensorHub != null &&
        !_enemy.SensorHub.IsDetecting(_aggroSlotName, _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] ├── [PhysicsPerceptionSystem] → _slots: [{aggro, RangeCircle}] ← 墙/崖检测由 EnemyMovement 内置射线,无需槽 ├── [Rigidbody2D] Dynamic [Collider2D] ├── HurtBox/ [HurtBox] [Collider2D trigger=true Layer=EnemyHurtBox] ├── ContactDamageZone/ [BodyContactDamage] [HitBox] [Collider2D trigger=true Layer=EnemyHitBox] └── Abilities/ ├── [PlayClipAbility] (_config=ABL_E001_Alert) └── [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_Alert) │ └── 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 ├── [PhysicsPerceptionSystem] → _slots: [{attack_range, BoxCast正下方矩形}] ├── [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] ├── [PhysicsPerceptionSystem] → _slots: [{aggro, RangeCircle}] ├── [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] ├── [PhysicsPerceptionSystem] → _slots: [{aggro, RangeCircle}, {sight, Sight}] ├── [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] ├── [PhysicsPerceptionSystem] → _slots: [{sight, Sight正面视锥}] ← 墙/崖检测由 EnemyMovement 内置射线 ├── [Rigidbody2D] Dynamic ├── HurtBox/ LandingHitBox/ └── Abilities/ [LeapAttackAbility (abilityId="e006_leap")]

行为树

Selector ├── BD_IsStateMatch(Dead) → BD_StopMovement ← Die() 已自动播放 AnimConfig.Dead ├── Sequence [跳跃攻击] │ ├── BD_IsSensorDetecting("sight") ← 正面视锥感知FanCast/Sight对应设计"视线检测正面扇形" │ ├── BD_CanUseAbility(ABL_E006_Leap) │ └── BD_UseAbility(ABL_E006_Leap) └── BD_Patrol [EnemyMovement.IsWallAhead/IsLedgeAhead 内置射线驱动翻转,无需感知槽]

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 已实现仅玩家在空中IGroundedActor.IsGrounded==false时计数
    if (!IsPlayerAirborne()) return;
    _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] ├── [PhysicsPerceptionSystem] → _slots: [{aggro, RangeCircle}, {sight, Sight}] ├── [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_Alert.asset ← PlayClipAbility SOABL_ 前缀abilityId=e001_alert │ │ └── 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 比较)? 现实现FacePlayerAbility.CanUse 内用 Movement.FacingDirection 比较,不依赖感知槽
Q5 E005 是否有 Flip 动画? 是否新建 Flip Ability
Q6 玩家空中判断方式 已实现:IGroundedActor 接口Core由 PlayerMovement 实现;ChaoFengKnockdownCounterBossBase.PlayerTransform 查询,仅玩家离地时计数(取不到接口时保守按空中处理)
Q7 各角色 HP/速度/伤害/CD 数值 所有 StatsSO / AbilitySO 填写
Q8 嘲风 Phase 2 浮空待机是否复用 Phase 1 Idle 美术制作量