refactor(enemy): 敌人专属子类改为零代码配置型行为组件

This commit is contained in:
2026-06-09 15:56:44 +08:00
parent 7781ac4755
commit ebf0c97320
22 changed files with 496 additions and 242 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fddc5216bee45cf4188bb31675c201a7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
using UnityEngine;
namespace BaseGames.Enemies.Behaviors
{
/// <summary>
/// 出生 / 外部时机触发执行指定能力(零代码替代每敌人专属的出生触发脚本)。
/// <list type="bullet">
/// <item>勾选 <c>executeOnSpawn</c>:对象池取出并完成 <see cref="EnemyBase.OnSpawn"/> 重置后自动执行能力
/// (例如精英怪死亡时生成的小怪落地即下坠)。</item>
/// <item>公共方法 <see cref="Trigger"/>:供场景战斗触发器 / UnityEvent / 动画事件在外部时机调用
/// (例如场景预置的天花板敌人被战斗触发器激活下坠)。</item>
/// </list>
/// 能力本身由挂载的 <c>EnemyAbilityBase</c> 组件(按 <c>EnemyAbilitySO.abilityId</c> 注册)实现,
/// 本组件只负责"在某个时机调用某个能力"。
/// </summary>
[DisallowMultipleComponent]
public class EnemyAbilityTrigger : MonoBehaviour
{
[Header("能力")]
[Tooltip("要执行的能力 Id须与某个 EnemyAbilityBase 的 EnemyAbilitySO.abilityId 一致)")]
[SerializeField] private string _abilityId = "ability_id";
[Header("触发时机")]
[Tooltip("对象池生成 / OnSpawn 重置完成后自动执行(用于被生成出来即触发的敌人)")]
[SerializeField] private bool _executeOnSpawn = true;
private EnemyBase _enemy;
private void Awake()
{
_enemy = GetComponentInParent<EnemyBase>();
if (_enemy == null)
Debug.LogError($"[EnemyAbilityTrigger] {name} 找不到 EnemyBase。", this);
}
private void OnEnable()
{
if (_enemy != null) _enemy.Spawned += OnEnemySpawned;
}
private void OnDisable()
{
if (_enemy != null) _enemy.Spawned -= OnEnemySpawned;
}
private void OnEnemySpawned()
{
if (_executeOnSpawn) Trigger();
}
/// <summary>
/// 立即执行配置的能力(外部时机触发:场景触发器 / UnityEvent / 动画事件调用)。
/// </summary>
public void Trigger()
{
_enemy?.Abilities.Get(_abilityId)?.Execute();
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: f2460e8735a4dc5409fe6b0949bd65c0
guid: 3198979f0bd38e1429478e7937b280b3
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,29 @@
namespace BaseGames.Enemies.Behaviors
{
/// <summary>
/// 死亡前摇演出组件接口(零代码替代每敌人专属的 <c>Die()</c> 重写)。
/// <para>
/// <see cref="EnemyBase.Die"/> 检测到子物体挂载此接口组件时,委托其播放无敌前摇演出,
/// 演出结束后必须回调 <paramref name="onComplete"/>(即 <see cref="EnemyBase.PerformDeath"/>
/// 执行真正的死亡清理。演出期间 <see cref="EnemyBase.IsInvincible"/> 为 true。
/// </para>
/// </summary>
public interface IEnemyDeathSequence
{
/// <summary>开始播放死亡前摇演出;演出结束后必须调用 <paramref name="onComplete"/>。</summary>
void Play(System.Action onComplete);
}
/// <summary>
/// 动画事件生成处理器接口(零代码替代每敌人专属的 <c>SpawnProjectile()</c> 重写)。
/// <para>
/// <see cref="EnemyBase.SpawnProjectile"/> 会把 payload 路由到所有挂载的此接口组件,
/// 由组件自行按 payload 匹配决定是否生成(如从对象池生成小怪 / 弹幕)。
/// </para>
/// </summary>
public interface IEnemySpawnEventHandler
{
/// <summary>处理一次动画事件生成请求。<paramref name="payload"/> 为动画事件携带的配置 Id。</summary>
void HandleSpawn(string payload);
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: d86a36c2999f88842a212d095749c349
guid: 6757468022586aa41aab08314ca5a24f
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections;
using Animancer;
using BaseGames.Combat;
using UnityEngine;
namespace BaseGames.Enemies.Behaviors
{
/// <summary>
/// 死亡前摇无敌演出(零代码替代每敌人专属的 <c>Die()</c> 重写)。
/// <para>
/// <see cref="EnemyBase.Die"/> 会委托本组件:停行为树 / 停移动、停用受击框,播放前摇动画并等待,
/// 期间敌人处于无敌(<see cref="EnemyBase.IsInvincible"/>),演出结束后回调真正的死亡清理。
/// 前摇动画上可放置 SpawnProjectile 动画事件,配合 <see cref="EnemySpawnerOnEvent"/> 在演出中生成小怪。
/// </para>
/// </summary>
[DisallowMultipleComponent]
public class EnemyDeathSequence : MonoBehaviour, IEnemyDeathSequence
{
[Header("前摇动画")]
[Tooltip("死亡前摇动画(无敌演出);为空则跳过演出直接进入死亡清理")]
[SerializeField] private ClipTransition _deathPreClip;
[Tooltip("前摇演出时长(秒)")]
[Min(0f)][SerializeField] private float _duration = 3f;
[Header("演出期间")]
[Tooltip("演出期间停用的受击框(防止演出中被打断或二次受伤);对象池复用时 OnSpawn 自动恢复")]
[SerializeField] private HurtBox[] _hurtBoxesToDisable;
[Tooltip("演出开始时停止行为树(防止 BT 继续 Tick 覆盖演出动画)")]
[SerializeField] private bool _stopBehaviorTree = true;
[Tooltip("演出开始时停止移动")]
[SerializeField] private bool _stopMovement = true;
private EnemyBase _enemy;
private AnimancerComponent _animancer;
private void Awake()
{
_enemy = GetComponentInParent<EnemyBase>();
_animancer = _enemy != null ? _enemy.Animancer : GetComponentInParent<AnimancerComponent>();
if (_enemy == null)
Debug.LogError($"[EnemyDeathSequence] {name} 找不到 EnemyBase。", this);
}
// 对象池复用:出生时恢复受击框(演出中曾被停用)
private void OnEnable()
{
if (_enemy != null) _enemy.Spawned += RestoreHurtBoxes;
}
private void OnDisable()
{
if (_enemy != null) _enemy.Spawned -= RestoreHurtBoxes;
}
public void Play(Action onComplete)
{
StartCoroutine(Sequence(onComplete));
}
private IEnumerator Sequence(Action onComplete)
{
if (_stopBehaviorTree) _enemy?.StopBehaviorTree();
if (_stopMovement) _enemy?.StopMovement();
SetHurtBoxesEnabled(false);
if (_deathPreClip.Clip != null && _animancer != null)
{
_animancer.Play(_deathPreClip);
if (_duration > 0f) yield return new WaitForSeconds(_duration);
}
onComplete?.Invoke();
}
private void RestoreHurtBoxes() => SetHurtBoxesEnabled(true);
private void SetHurtBoxesEnabled(bool enabled)
{
if (_hurtBoxesToDisable == null) return;
foreach (var hb in _hurtBoxesToDisable)
if (hb != null) hb.enabled = enabled;
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: cf8f8c7225dca9c42b5a451b177319b9
guid: 4a4a8aad8881b4543a0918321e7efe3e
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,46 @@
using BaseGames.Core;
using BaseGames.Core.Pool;
using UnityEngine;
namespace BaseGames.Enemies.Behaviors
{
/// <summary>
/// 动画事件触发的对象池生成(零代码替代每敌人专属的 <c>SpawnProjectile()</c> 重写)。
/// <para>
/// <see cref="EnemyBase.SpawnProjectile"/> 会把 payload 路由到本组件;当 payload 与
/// <c>payloadKey</c> 匹配时,在自身周围随机位置从对象池生成 <c>count</c> 个指定 key 的对象
/// (例如精英怪死亡前摇中生成 N 个小怪)。
/// </para>
/// 使用方式:在动画(如死亡前摇)相应帧放置 SpawnProjectile 动画事件payload 填 <c>payloadKey</c>。
/// 同一敌人可挂多个本组件(不同 payloadKey以生成不同对象。
/// </summary>
public class EnemySpawnerOnEvent : MonoBehaviour, IEnemySpawnEventHandler
{
[Header("触发匹配")]
[Tooltip("匹配的动画事件 payload如 \"spawn_e003\");留空则匹配任意 payload")]
[SerializeField] private string _payloadKey = "";
[Header("生成")]
[Tooltip("对象池 keyIObjectPoolService.Spawn 的 key须经 AddressKeys 常量约定,如 \"ENM_YouZhi\"")]
[SerializeField] private string _poolKey = "";
[Tooltip("生成数量")]
[Min(1)][SerializeField] private int _count = 3;
[Tooltip("生成点相对自身的随机半径")]
[Min(0f)][SerializeField] private float _radius = 1.5f;
public void HandleSpawn(string payload)
{
if (!string.IsNullOrEmpty(_payloadKey) && payload != _payloadKey) return;
if (string.IsNullOrEmpty(_poolKey)) return;
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
if (pool == null) return;
for (int i = 0; i < _count; i++)
{
Vector2 offset = Random.insideUnitCircle * _radius;
pool.Spawn(_poolKey, (Vector2)transform.position + offset, Quaternion.identity);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fd9abafdb6bedcc4b8fd011bde9208b6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,34 +0,0 @@
using System.Collections;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// E003 幼蛭。HP=1一击即死。
/// 支持双路初始化:
/// - 预置路径:场景战斗触发器调用 <see cref="ActivateFromCeiling"/>
/// - 对象池路径E005 死亡时通过 <see cref="OnSpawn"/> 自动触发
/// 能力脚本 <c>AnimatedCeilingDropAbility</c> 负责 Fall 动画 + 物理下落 + SetAiPhase(Patrol)。
/// </summary>
public class E003_YouZhi : EnemyBase
{
[Tooltip("对象池生成时是否立即执行下落能力E005 触发生成路径)")]
[SerializeField] private bool _activateOnSpawn = true;
public override void OnSpawn()
{
base.OnSpawn();
if (_activateOnSpawn)
Abilities.Get("e003_fall")?.Execute();
}
/// <summary>
/// 场景预置路径由场景触发器EventTrigger / Animator Event调用触发天花板跌落。
/// </summary>
public void ActivateFromCeiling()
{
Abilities.Get("e003_fall")?.Execute();
}
}
}

View File

@@ -1,45 +0,0 @@
using System.Collections;
using Animancer;
using BaseGames.Combat;
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// E004 蛭母小Boss
/// 特性:
/// - 出场演出AppearAbility
/// - 三技能(撕咬/头槌/酸液)+ 翻身FacePlayerAbility
/// - 死亡两阶段Death_Pre 无敌演出 → base.Die()
/// </summary>
public class E004_ZhiMu : EnemyBase
{
[Header("死亡演出")]
[SerializeField] private ClipTransition _deathPreClip;
[SerializeField] private HurtBox _hurtBox;
[SerializeField] private float _deathPreDuration = 3f;
protected override void Die()
{
StartCoroutine(DeathSequence());
}
private IEnumerator DeathSequence()
{
// 停止行为树防止覆盖演出动画
StopBehaviorTree();
StopMovement();
if (_hurtBox != null)
_hurtBox.enabled = false;
if (_deathPreClip.Clip != null)
{
Animancer.Play(_deathPreClip);
yield return new WaitForSeconds(_deathPreDuration);
}
base.Die();
}
}
}

View File

@@ -1,66 +0,0 @@
using System.Collections;
using Animancer;
using BaseGames.Combat;
using BaseGames.Core;
using BaseGames.Core.Pool;
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// E005 肥蛭(精英怪)。
/// 特性:
/// - 撕咬MeleeVulnerabilityAbility含后摇脆弱窗口
/// - 死亡两阶段Death_Pre 无敌演出(含 AnimationEvent spawn_e003→ base.Die()
/// </summary>
public class E005_FeiZhi : EnemyBase
{
[Header("死亡演出")]
[SerializeField] private ClipTransition _deathPreClip;
[SerializeField] private HurtBox _hurtBox;
[SerializeField] private float _deathPreDuration = 3f;
[Header("生成 E003Death_Pre AnimationEvent 触发)")]
[SerializeField] private int _spawnCount = 3;
[SerializeField] private float _spawnRadius = 1.5f;
/// <summary>
/// Death_Pre 动画适当帧的 AnimationEvent 调用 SpawnProjectile("spawn_e003") 生成幼蛭。
/// </summary>
public override void SpawnProjectile(string payload)
{
if (payload != "spawn_e003") return;
var pool = BaseGames.Core.ServiceLocator.GetOrDefault<IObjectPoolService>();
if (pool == null) return;
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()
{
StopBehaviorTree();
StopMovement();
if (_hurtBox != null)
_hurtBox.enabled = false;
if (_deathPreClip.Clip != null)
{
Animancer.Play(_deathPreClip);
yield return new WaitForSeconds(_deathPreDuration);
}
base.Die();
}
}
}

View File

@@ -27,6 +27,12 @@ namespace BaseGames.Enemies
/// <summary>死亡时触发ChallengeRoomManager 波次结算用)。</summary>
public event System.Action OnDied;
/// <summary>
/// 对象池取出并完成 <see cref="OnSpawn"/> 重置后触发。
/// 配置型出生行为组件(如 EnemyAbilityTrigger订阅此事件实现零代码出生触发。
/// </summary>
public event System.Action Spawned;
[Header("配置 SO")]
[SerializeField] protected EnemyStatsSO _statsSO;
[SerializeField] protected EnemyAnimationConfigSO _animConfig;
@@ -83,6 +89,14 @@ namespace BaseGames.Enemies
/// </summary>
private PooledObject _pooledObject;
// ── 配置型行为模块(零代码)─────────────────────────────────────────
// 可选挂载的通用行为组件替代过去的每敌人专属子类。Awake 时收集一次。
private Behaviors.IEnemyDeathSequence _deathSequence;
private readonly System.Collections.Generic.List<Behaviors.IEnemySpawnEventHandler> _spawnHandlers
= new System.Collections.Generic.List<Behaviors.IEnemySpawnEventHandler>(2);
// 死亡前摇演出进行中:纳入 IsInvincible并阻止重复触发 Die。
private bool _deathSequenceActive;
// ── 状态 ──────────────────────────────────────────────────────────
private EnemyStateType _currentState;
public EnemyStateType CurrentState => _currentState;
@@ -129,7 +143,7 @@ namespace BaseGames.Enemies
= new System.Collections.Generic.Dictionary<EnemyStateType, IEnemyState>();
// ── IDamageable ───────────────────────────────────────────────────
public bool IsAlive => _currentState != EnemyStateType.Dead;
public virtual bool IsInvincible => _currentState == EnemyStateType.Dead;
public virtual bool IsInvincible => _currentState == EnemyStateType.Dead || _deathSequenceActive;
public int Defense => _stats != null ? _stats.Defense : 0;
public void TakeDamage(DamageInfo info)
@@ -478,8 +492,17 @@ namespace BaseGames.Enemies
// ── 动画事件钩子(由 EnemyAnimationEvents 调用)────────────────────
/// <summary>生成弹幕 / 技能投射物。payload 为配置 Id由子类查表实现。</summary>
public virtual void SpawnProjectile(string payload) { }
/// <summary>
/// 生成弹幕 / 技能投射物(由动画事件 SpawnProjectile 触发)。
/// 基类实现:路由到所有挂载的 <see cref="Behaviors.IEnemySpawnEventHandler"/> 组件
/// (如 EnemySpawnerOnEvent由组件按 payload 自行匹配并生成——实现零代码生成配置。
/// 子类(如 RangedEnemy / ChaoFengBoss可重写以自定义发射逻辑。
/// </summary>
public virtual void SpawnProjectile(string payload)
{
for (int i = 0; i < _spawnHandlers.Count; i++)
_spawnHandlers[i]?.HandleSpawn(payload);
}
/// <summary>切换二阶段形态Boss 等特殊敌人重写此方法)。</summary>
public virtual void TriggerPhaseTwo() { }
@@ -560,6 +583,10 @@ namespace BaseGames.Enemies
_abilities.CollectFrom(gameObject);
_colliders = GetComponentsInChildren<Collider2D>(true);
// 收集配置型行为模块(零代码扩展点)
_deathSequence = GetComponentInChildren<Behaviors.IEnemyDeathSequence>(true);
GetComponentsInChildren(true, _spawnHandlers);
Debug.Assert(_statsSO != null, "[EnemyBase] _statsSO 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this);
Debug.Assert(_stats != null, "[EnemyBase] _stats 未绑定,请在 Prefab Inspector 中绑定 EnemyStats 组件。", this);
Debug.Assert(_movement != null, "[EnemyBase] _movement 未找到,请确保同 GameObject 上挂有 EnemyMovement 组件。", this);
@@ -667,19 +694,46 @@ namespace BaseGames.Enemies
#endif
/// <summary>
/// 停止行为树(子类 Die() 预演出阶段可调用,防止 BT 继续 Tick 覆盖演出逻辑)。
/// 内部使用 #if GRAPH_DESIGNER 保护,子类无需处理条件编译。
/// 停止行为树(死亡演出 / 出场演出等期间调用,防止 BT 继续 Tick 覆盖演出逻辑)。
/// 内部使用 #if GRAPH_DESIGNER 保护,调用方无需处理条件编译。
/// 供配置型行为组件(如 EnemyDeathSequence调用故为 public。
/// </summary>
protected void StopBehaviorTree()
public void StopBehaviorTree()
{
#if GRAPH_DESIGNER
_behaviorTree?.StopBehavior();
#endif
}
/// <summary>
/// 死亡入口。若挂载了 <see cref="Behaviors.IEnemyDeathSequence"/> 死亡演出组件,
/// 则先委托其播放无敌前摇(期间 <see cref="IsInvincible"/> 为 true演出结束后回调
/// <see cref="PerformDeath"/> 执行真正的死亡清理;否则直接清理。
/// 子类(如 BossBase重写时仍调用 base.Die() 即可获得此委托行为。
/// </summary>
protected virtual void Die()
{
if (_currentState == EnemyStateType.Dead || _deathSequenceActive) return;
if (_deathSequence != null)
{
_deathSequenceActive = true; // 演出期间纳入 IsInvincible阻止重复 Die
_deathSequence.Play(PerformDeath);
return;
}
PerformDeath();
}
/// <summary>
/// 实际死亡清理:切 Dead 终态、清状态效果、中断能力、关碰撞体、播死亡动画、
/// 归还对象池 / 销毁、广播死亡事件。由 <see cref="Die"/> 直接调用,
/// 或由死亡演出组件在前摇结束后回调。
/// </summary>
protected void PerformDeath()
{
if (_currentState == EnemyStateType.Dead) return;
_deathSequenceActive = false;
ForceState(EnemyStateType.Dead);
// 死亡时清除所有状态效果
@@ -733,6 +787,7 @@ namespace BaseGames.Enemies
// 注意_playerTransform 不重置(场景中玩家仍存在),只重置追踪历史
LastKnownPlayerPosition = transform.position;
_wasParried = false;
_deathSequenceActive = false;
// 重置生命值
if (_stats != null && _statsSO != null)
@@ -744,6 +799,9 @@ namespace BaseGames.Enemies
#if GRAPH_DESIGNER
_behaviorTree?.StartBehavior();
#endif
// 通知配置型出生行为组件(如 EnemyAbilityTrigger执行出生触发逻辑
Spawned?.Invoke();
}
/// <summary>