Files
zeling_v2/Assets/_Game/Scripts/Enemies/EnemyBase.cs
T

814 lines
39 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using Animancer;
using BaseGames.AI;
using BaseGames.Combat;
using BaseGames.Core.Events;
using BaseGames.Core.Pool;
using BaseGames.Enemies.States;
using BaseGames.Enemies.Abilities;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人基类(架构 07_EnemyModule §1)。
/// 实现 IDamageable,为 Behavior Designer 任务提供统一虚方法接口。
/// 包含:BD 接口、受击、死亡流程。
/// ⚠️ _nav 字段类型为 IPathAgent(在 BaseGames.Enemies.Navigation 中实现具体类)。
/// 实现 IPoolable:配合 PooledObject 支持对象池复用,避免频繁 Destroy/Instantiate。
/// </summary>
public class EnemyBase : MonoBehaviour, IDamageable, IPoolable
{
[Header("标识")]
[SerializeField] private string _enemyId; // 任务系统 / Boss 进程追踪用,如 "Enemy_SpiderGuard"
public string EnemyId => _enemyId;
/// <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;
[Header("子组件(Prefab Inspector 绑定)")]
[SerializeField] protected EnemyStats _stats;
[SerializeField] protected EnemyMovement _movement;
[SerializeField] protected EnemyCombat _combat;
[SerializeField] protected AnimancerComponent _animancer;
[SerializeField] protected EnemyFeedback _feedback;
[SerializeField] protected HurtBox _hurtBox;
[Header("区域(可选)")]
[Tooltip("地图固定巡逻/追击区域;配置后 BD_ChasePlayer 以区域边界替代 MaxChaseDistanceBD_ReturnToHome 归位至区域中心。留空则沿用出生点 + MaxChaseDistance 旧逻辑。")]
[SerializeField] private EnemyPatrolZone _patrolZone;
[Header("事件频道")]
[SerializeField] private BaseGames.Core.Events.StringEventChannelSO _onEnemyDied;
/// <summary>
/// 玩家生成事件频道(由 PlayerController.Start() 广播)。
/// 配置后替代 FindWithTag,避免 N 个敌人同帧全场景标签扫描。
/// </summary>
[SerializeField] private BaseGames.Core.Events.TransformEventChannelSO _onPlayerSpawned;
// ── 导航代理(IPathAgent;由 EnemyNavAgent 实现)───────────────────
// 通过接口引用,避免对 Navigation 程序集的直接依赖。
// 由子类 / Inspector 注入,或者运行时 GetComponent<IPathAgent>() 获取。
protected IPathAgent _nav;
// 移动执行器(IEnemyLocomotion;由 EnemyLocomotion 在 Navigation 程序集实现,运行时发现)
protected IEnemyLocomotion _locomotion;
// 霸体来源(由 EnemyPoiseComponent.Awake() 自动注入,TakeDamage 时读取)
private IPoiseSource _poiseSource;
protected readonly CompositeDisposable _subs = new();
// 碰撞体缓存:Awake 时收集一次,避免 Die()/OnSpawn() 中频繁 GetComponentsInChildren 分配
private Collider2D[] _colliders;
// ── 对象池支持 ─────────────────────────────────────────────────────
/// <summary>
/// 本 GameObject 上的 PooledObject 组件(可选)。
/// Prefab 挂有此组件时,Die() 使用归还池取代 Destroy,实现对象池复用。
/// </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;
// ── 导航语义(归位 / 搜查)────────────────────────────────────────────
/// <summary>Awake/Start 时记录的初始世界坐标,供 BD_ReturnToHome 归位使用。</summary>
public Vector2 HomePosition { get; private set; }
/// <summary>
/// 玩家最后一次可见的世界坐标。
/// 由 BD_ChasePlayer 在持有视线时每帧更新;视线丢失后保留最后记录值供 BD_InvestigateLastKnown 使用。
/// </summary>
public Vector2 LastKnownPlayerPosition { get; set; }
/// <summary>
/// 地图固定巡逻/追击区域(可选)。
/// 配置后 BD_ChasePlayer 以区域边界为追击上限;BD_ReturnToHome 归位至区域中心。
/// 未配置时退回旧逻辑(HomePosition + MaxChaseDistance)。
/// </summary>
public EnemyPatrolZone PatrolZone => _patrolZone;
#if UNITY_EDITOR
[Header("── 运行时调试(仅 Editor)──")]
[SerializeField] private EnemyStateType _dbg_CurrentState;
[SerializeField] private bool _dbg_HasPlayer;
[SerializeField] private Vector2 _dbg_LastKnownPos;
#endif
// POCO 状态对象字典:枚举保持对外 API 不变。
// 子类可在 Awake() 重写条目注入自定义状态对象。
protected readonly System.Collections.Generic.Dictionary<EnemyStateType, IEnemyState> _stateObjs
= new System.Collections.Generic.Dictionary<EnemyStateType, IEnemyState>();
// ── IDamageable ───────────────────────────────────────────────────
public bool IsAlive => _currentState != EnemyStateType.Dead;
public virtual bool IsInvincible => _currentState == EnemyStateType.Dead || _deathSequenceActive;
public int Defense => _stats != null ? _stats.Defense : 0;
public void TakeDamage(DamageInfo info)
{
if (IsInvincible) return;
_stats?.TakeDamage(info.FinalDamage);
_feedback?.OnHit(info);
if (_stats != null && _stats.CurrentHP <= 0)
{
Die();
return;
}
// ── 持续伤害(DoT):只扣血与受击反馈,不打断/不重置硬直状态 ──
// 否则燃烧/中毒每个 Tick 都会把敌人重新打入 Hurt 并 InterruptAll,形成硬直锁。
if (info.Flags.HasFlag(DamageFlags.IsDoT))
{
OnDamageTaken(info);
return;
}
// ── 受击分级(KnockUp > Stagger > Hurt)──────────────────────
PoiseLevel curPoise = _poiseSource?.GetCurrentPoiseLevel() ?? PoiseLevel.None;
bool causesStagger = info.Flags.HasFlag(DamageFlags.ForceBreak)
|| (int)info.Break > (int)curPoise;
// KnockUp 判断:携带 Launch 标志,且(无阈值限制 或 伤害超过阈值)
bool causesKnockUp = false;
if (causesStagger && info.Flags.HasFlag(DamageFlags.Launch) && _statsSO != null)
{
int threshold = _statsSO.HitTiers.launchThreshold;
causesKnockUp = (threshold <= 0) || (info.FinalDamage >= threshold);
}
EnemyStateType nextState;
InterruptReason reason;
if (causesKnockUp)
{
// 存储来袭方向供 EnemyKnockUpState 使用
_pendingLaunchDir = info.KnockbackDirection;
nextState = EnemyStateType.KnockUp;
reason = InterruptReason.KnockUp;
}
else if (causesStagger)
{
nextState = EnemyStateType.Stagger;
reason = InterruptReason.Stagger;
}
else
{
nextState = EnemyStateType.Hurt;
reason = InterruptReason.Hurt;
}
ForceState(nextState);
_abilities.InterruptAll(reason);
OnDamageTaken(info);
}
/// <summary>
/// 受击后钩子(已确认未死亡时调用)。子类可重写以触发额外逻辑(如资源积累)。
/// </summary>
protected virtual void OnDamageTaken(DamageInfo info) { }
/// <summary>
/// 击飞来袭方向(由 TakeDamage 写入,供 EnemyKnockUpState.Enter() 读取)。
/// </summary>
internal Vector2 PendingLaunchDir => _pendingLaunchDir;
private Vector2 _pendingLaunchDir;
/// <summary>
/// 协程兜底:在无对应 Animancer 动画时按时长自动恢复到 Controlled 状态。
/// 仅在 AnimConfig 对应 Clip 为 null 时由状态类调用。
/// </summary>
public void ScheduleStateRecovery(EnemyStateType fromState, float delay)
{
StartCoroutine(StateRecoveryRoutine(fromState, delay));
}
private System.Collections.IEnumerator StateRecoveryRoutine(EnemyStateType fromState, float delay)
{
yield return new WaitForSeconds(delay);
if (_currentState == fromState)
ForceState(EnemyStateType.Controlled);
}
// BD 任务访问接口(公共只读属性)────────────────────────────────
public IPathAgent Nav => _nav;
// 惰性发现兜底:关域重载后 Awake 设的实例字段可能残留为 null(编辑器快速迭代),
// 首次访问时按需补发现,保证编辑器与构建下都不为空。
public IEnemyLocomotion Locomotion => _locomotion ?? (_locomotion = GetComponentInChildren<IEnemyLocomotion>(true));
public EnemyMovement Movement => _movement;
public EnemyStats Stats => _stats;
/// <summary>敌人配置 SO,供 BD Task / 状态对象读取配置数据(如 knockUpDuration)。</summary>
public EnemyStatsSO StatsSO => _statsSO;
public AnimancerComponent Animancer => _animancer;
public EnemyAnimationConfigSO AnimConfig => _animConfig;
/// <summary>能力注册表(架构 §8.3)。Awake 时自动收集所有 EnemyAbilityBase 组件。</summary>
public EnemyAbilityRegistry Abilities => _abilities;
private readonly EnemyAbilityRegistry _abilities = new EnemyAbilityRegistry();
/// <summary>攻击选择器(从已注册能力里筛出 category==Attack 的候选,Awake 时构建)。</summary>
public Abilities.EnemyAttackSelector AttackSelector => _attackSelector;
private Abilities.EnemyAttackSelector _attackSelector;
/// <summary>由 _onPlayerSpawned 事件缓存的玩家 Transform,供 BD 任务读取。</summary>
public Transform PlayerTransform => _playerTransform;
/// <summary>感知 Hub;供 BD 任务及 QuotaManager 暂停/恢复感知使用。</summary>
public Perception.IPerceptionSystem SensorHub => _sensorHub;
private Perception.IPerceptionSystem _sensorHub;
/// <summary>威胁评估器(可选):为原始 LOS 结果叠加反应延迟,使感知更自然。</summary>
public Perception.EnemyThreatAssessor ThreatAssessor => _threatAssessor;
private Perception.EnemyThreatAssessor _threatAssessor;
/// <summary>状态效果管理器(冻结、灼烧、睡眠等)。</summary>
public StatusEffects.EnemyStatusEffectManager StatusEffects => _statusEffects;
private StatusEffects.EnemyStatusEffectManager _statusEffects;
/// <summary>决策层组件(BrainGraph)。供 EnemyQuotaManager 做活跃数量 LOD 裁剪等使用。</summary>
public EnemyAiBrain Brain => _brain;
private EnemyAiBrain _brain;
/// <summary>是否处于交战(追击/攻击)中——供群体警戒判断"已交战不降级"。由 Chase 能力置位。</summary>
public bool IsEngaged { get; private set; }
public void SetEngaged(bool engaged) => IsEngaged = engaged;
// ── BD 行为树接口(虚方法)────────────────────────────────────────
public virtual void MoveTo(Vector2 target)
=> _nav?.RequestMoveTo(target);
public virtual void MoveInDirection(float dir)
{
if (_movement == null) return;
_movement.PendingInput.MoveDir = dir;
_movement.PendingInput.WantStop = false; // 移动意图覆盖停止脉冲
}
public virtual void MoveInDirectionWithSpeed(float dir, float speed)
{
if (_movement == null) return;
_movement.PendingInput.MoveDir = dir;
_movement.PendingInput.MoveSpeed = speed;
_movement.PendingInput.WantStop = false; // 移动意图覆盖停止脉冲
}
public virtual void StopMovement()
{
_nav?.StopNavigation();
if (_movement != null) _movement.PendingInput.WantStop = true;
}
/// <summary>施加状态效果(需要 EnemyStatusEffectManager 组件)。同类型效果自动刷新。</summary>
public void ApplyStatusEffect(StatusEffects.IStatusEffect effect)
=> _statusEffects?.Apply(effect);
/// <summary>移除指定类型的状态效果(若存在)。</summary>
public void RemoveStatusEffect(StatusEffects.StatusEffectType type)
=> _statusEffects?.Remove(type);
/// <summary>查询指定类型状态效果是否激活。</summary>
public bool HasStatusEffect(StatusEffects.StatusEffectType type)
=> _statusEffects != null && _statusEffects.HasEffect(type);
public virtual void BeginAttack(AttackType type)
{
_combat?.StartAttack(type);
_stats?.ResetAttackCooldown();
}
public virtual bool CanAttack()
=> _stats != null && _stats.AttackCooldownTimer <= 0f;
public virtual bool IsPlayerInRange(float range)
=> _stats != null && _stats.SqrDistanceToPlayer <= range * range;
/// <summary>视线检测结果(由 PhysicsPerceptionSystem 的 LOS / Sight slot 提供)。</summary>
public bool HasLineOfSight =>
_sensorHub != null &&
(_sensorHub.HasAnyDetection(Perception.SensorSlotNames.LOS) ||
_sensorHub.HasAnyDetection(Perception.SensorSlotNames.Sight));
public virtual bool IsPlayerVisible()
=> _threatAssessor != null ? _threatAssessor.IsThreatDetected : HasLineOfSight;
/// <summary>追逐感知:玩家是否在"追逐触发圆"(aggro 槽)内 → 触发追击。感知系统只回答"在不在",状态转换归 AI。</summary>
public bool InChaseZone()
=> _sensorHub != null && _playerTransform != null &&
_sensorHub.IsDetecting(Perception.SensorSlotNames.Aggro, _playerTransform.gameObject);
/// <summary>
/// 视野感知:玩家是否在视野(los/sight 槽)内。用于警觉升级与追击维持;
/// 无视野槽时回退用追逐感知(aggro)。感知系统只回答"在不在",状态转换归 AI。
/// </summary>
public bool InVisionZone()
{
if (_sensorHub == null || _playerTransform == null) return false;
bool hasVisionSlot = _sensorHub.HasSlot(Perception.SensorSlotNames.LOS)
|| _sensorHub.HasSlot(Perception.SensorSlotNames.Sight);
if (!hasVisionSlot) return InChaseZone(); // 缺省回退到追逐感知
var go = _playerTransform.gameObject;
return _sensorHub.IsDetecting(Perception.SensorSlotNames.LOS, go)
|| _sensorHub.IsDetecting(Perception.SensorSlotNames.Sight, go);
}
public virtual void FacePlayer()
{
if (_movement == null || _playerTransform == null) return;
_movement.PendingInput.WantFace = true;
_movement.PendingInput.FaceTargetPos = _playerTransform.position;
_movement.PendingInput.FaceDir = 0;
}
/// <summary>朝向世界坐标点(通过输入信号,下一 FixedUpdate 消费)。</summary>
public void FaceTarget(Vector2 worldPos)
{
if (_movement == null) return;
_movement.PendingInput.WantFace = true;
_movement.PendingInput.FaceTargetPos = worldPos;
_movement.PendingInput.FaceDir = 0;
}
/// <summary>直接指定朝向方向(+1 右 / -1 左,通过输入信号)。</summary>
public void FaceDirection(int dir)
{
if (_movement == null) return;
_movement.PendingInput.WantFace = true;
_movement.PendingInput.FaceDir = dir;
}
/// <summary>
/// 搜查"环顾"子步骤:停止移动,播放原地环顾动画。
/// 由搜查行为触发;动画细节由角色自己决定,外部无需感知 AnimConfig。
/// </summary>
public void BeginLookAround()
{
StopMovement();
if (_animancer != null && _animConfig != null)
{
var clip = _animConfig.Investigate ?? _animConfig.Idle;
if (clip != null) _animancer.Play(clip);
}
}
public virtual void Knockback(DamageInfo info)
{
if (info.Flags.HasFlag(DamageFlags.NoKnockback)) return;
_movement?.ApplyKnockback(info.KnockbackDirection, info.KnockbackForce);
// 统一路径:击退必须经过状态机,确保能力被中断且动画一致。
ForceState(EnemyStateType.Hurt);
_abilities.InterruptAll(InterruptReason.Hurt);
}
public virtual void JumpTo(Vector2 target)
=> _movement?.JumpToTarget(target);
/// <summary>
/// 调整决策 Tick 频率(非警觉=降频,警觉=高频)。
/// 由外部(警戒/状态效果)调用(架构 07_EnemyModule §13.5)。
/// </summary>
public virtual void SetAggroTickRate(bool isAggro)
{
// LOD tick 速率控制迁移到后续 AiScheduler 阶段
}
// ── 弹反(Parry)响应 ──────────────────────────────────────────────
private bool _wasParried;
private float _parryTimestamp;
// BD 树因阶段切换/死亡未能消费弹反事件时,超过此时长自动过期
private const float ParryEventTTL = 2f;
/// <summary>
/// 消费弹反事件标志(读取并清除)。
/// BD_OnParried Conditional Task 在每次 Tick 时调用此方法。
/// 若 BD 树因阶段切换或死亡未能及时消费,超过 TTL 后自动过期返回 false。
/// </summary>
public bool ConsumeParryEvent()
{
if (!_wasParried) return false;
if (Time.time - _parryTimestamp > ParryEventTTL)
{
_wasParried = false;
return false;
}
_wasParried = false;
return true;
}
/// <summary>
/// 被弹反时调用:强制进入 Stagger 状态并在 <paramref name="staggerDuration"/> 秒后恢复。
/// 由近战攻击碰到玩家弹反框时触发(例如 BossParryDetector 或通用 ParryDetector)。
/// </summary>
public virtual void ReceiveParry(float staggerDuration = 0.5f)
{
if (!IsAlive) return;
_wasParried = true;
_parryTimestamp = Time.time;
ForceState(EnemyStateType.Stagger);
_abilities.InterruptAll(InterruptReason.Stagger);
ScheduleStateRecovery(EnemyStateType.Stagger, staggerDuration);
}
// 动画锁:一次性招式动画(如追击 Skill_Start/Skill_End)播放期间,禁止步态动画覆盖它。
private float _animLockUntil;
/// <summary>是否处于一次性招式动画锁定中(步态不应覆盖)。</summary>
public bool IsAnimLocked => Time.time < _animLockUntil;
/// <summary>锁定动画 <paramref name="duration"/> 秒,期间步态动画不覆盖(供能力播放起手/收招等一次性动作)。</summary>
public void LockAnim(float duration) => _animLockUntil = Time.time + Mathf.Max(0f, duration);
/// <summary>
/// 按当前移动模式播放步态动画(由 EnemyLocomotion 在模式变化时调用)。
/// Approach(追击)不在此驱动——追击动画由能力(Skill_Start/Loop/End)全权驱动,避免抢动画。
/// 招式动画锁定期间(<see cref="IsAnimLocked"/>)不覆盖。
/// </summary>
public void PlayLocomotionClip(LocomotionMode mode)
{
if (_animancer == null || _animConfig == null || IsAnimLocked) return;
var clip = mode switch
{
LocomotionMode.Idle => _animConfig.Idle,
LocomotionMode.Patrol => _animConfig.Walk,
LocomotionMode.Face => _animConfig.Alert,
LocomotionMode.Approach => null, // 追击动画归能力驱动
_ => null,
};
if (clip != null) _animancer.Play(clip);
}
// ── 动画事件钩子(由 EnemyAnimationEvents 调用)────────────────────
/// <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() { }
/// <summary>动画播放完毕回调(用于单次动画后返回 Idle 等逻辑)。</summary>
public virtual void OnAnimationComplete(string payload) { }
/// <summary>设置嘶吼状态(影响 Blackboard / 状态机行为)。</summary>
public virtual void SetRoaring(bool isRoaring) { }
// 防止状态 Enter/Exit 内部再次调用 ForceState 造成无限递归
private bool _isStateTransitioning;
// ── 状态控制 ──────────────────────────────────────────────────────
/// <summary>
/// 强制切换物理/战斗状态。
/// ⚠️ Dead 是终态:进入后不允许外部再转换到其他状态。
/// 对象池复用时请通过 <see cref="OnSpawn"/> 重置,该方法调用 <see cref="ForceStateRespawn"/>。
/// </summary>
public void ForceState(EnemyStateType newState)
{
// Dead 是终态:阻止任何"复活"转换,防止死亡后协程意外恢复
if (_currentState == EnemyStateType.Dead && newState != EnemyStateType.Dead)
return;
// 防止 Enter/Exit 内嵌套调用 ForceState 导致无限递归
if (_isStateTransitioning)
{
Debug.LogWarning($"[EnemyBase] ForceState({newState}) 在状态转换期间被递归调用,已忽略。", this);
return;
}
_isStateTransitioning = true;
// Exit 当前状态
if (_stateObjs.TryGetValue(_currentState, out var prev))
prev.Exit(this);
_currentState = newState;
// Enter 新状态
if (_stateObjs.TryGetValue(newState, out var next))
next.Enter(this);
_isStateTransitioning = false;
}
/// <summary>
/// 对象池复用重置专用(跳过 Dead 终态守卫)。
/// 仅由 <see cref="OnSpawn"/> 调用,不对外暴露。
/// </summary>
private void ForceStateRespawn(EnemyStateType newState)
{
if (_stateObjs.TryGetValue(_currentState, out var prev))
prev.Exit(this);
_currentState = newState;
if (_stateObjs.TryGetValue(newState, out var next))
next.Enter(this);
}
// ── Unity 生命周期 ────────────────────────────────────────────────
protected virtual void Awake()
{
// 初始化 POCO 状态对象(子类可在调用 base.Awake() 后替换字典条目)
_stateObjs[EnemyStateType.Controlled] = new EnemyControlledState();
_stateObjs[EnemyStateType.Hurt] = new EnemyHurtState();
_stateObjs[EnemyStateType.Stagger] = new EnemyStaggerState();
_stateObjs[EnemyStateType.KnockUp] = new EnemyKnockUpState();
_stateObjs[EnemyStateType.Dead] = new EnemyDeadState();
_nav = GetComponent<IPathAgent>() ?? new NullPathAgent();
_locomotion = GetComponentInChildren<IEnemyLocomotion>(true);
if (_locomotion == null)
Debug.LogError($"EnemyBase 未找到 EnemyLocomotion 组件:{name}", this);
if (_movement == null) _movement = GetComponent<EnemyMovement>();
_poiseSource = GetComponent<IPoiseSource>();
_sensorHub = GetComponentInChildren<Perception.IPerceptionSystem>();
_statusEffects = GetComponent<StatusEffects.EnemyStatusEffectManager>();
_threatAssessor = GetComponent<Perception.EnemyThreatAssessor>();
_pooledObject = GetComponent<PooledObject>();
_brain = GetComponent<EnemyAiBrain>();
_abilities.CollectFrom(gameObject);
BuildAttackSelector();
_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);
_stats.Initialize(_statsSO);
// 订阅玩家生成事件(PlayerController.Start 广播),避免每个敌人独立 FindWithTag
// 订阅在 OnEnable 中处理
}
/// <summary>
/// 从已注册能力里收集 category==Attack 的候选,构建攻击选择器。
/// 根因校验:攻击招 rangeRadius<=0(永远够不着)显式报错,不静默兜底。
/// </summary>
private void BuildAttackSelector()
{
var candidates = new System.Collections.Generic.List<Abilities.IAttackCandidate>();
var all = _abilities?.All;
if (all != null)
{
for (int i = 0; i < all.Count; i++)
{
var ab = all[i];
if (ab == null || ab.Config == null) continue;
if (ab.Config.category != Abilities.AbilityCategory.Attack) continue;
if (ab.Config.rangeRadius <= 0f)
Debug.LogError($"[EnemyBase] 攻击招 '{ab.Config.abilityId}' 的 rangeRadius<=0" +
"永远够不着玩家。请在其 EnemyAbilitySO 上配置攻击触发半径。", ab);
candidates.Add(ab);
}
}
_attackSelector = new Abilities.EnemyAttackSelector(candidates);
}
protected virtual void Update()
{
_stats?.TickAttackCooldown(Time.deltaTime);
// 使用 sqrMagnitude 替代 Vector2.Distance,避免每帧开平方计算
if (_playerTransform != null && _stats != null)
_stats.SqrDistanceToPlayer = ((Vector2)_playerTransform.position - (Vector2)transform.position).sqrMagnitude;
#if UNITY_EDITOR
_dbg_CurrentState = _currentState;
_dbg_HasPlayer = _playerTransform != null;
_dbg_LastKnownPos = LastKnownPlayerPosition;
#endif
}
protected virtual void Start()
{
// 记录出生位置,供 BD_ReturnToHome 归位使用
HomePosition = transform.position;
LastKnownPlayerPosition = transform.position;
// 若事件未配置或玩家尚未广播,匹降为一次性查找
if (_playerTransform == null)
{
var playerGO = GameObject.FindWithTag("Player");
if (playerGO != null) _playerTransform = playerGO.transform;
}
// 播放 Idle 动画(若 Animancer 和配置都就绪)
if (_animancer != null && _animConfig != null && _animConfig.Idle != null)
_animancer.Play(_animConfig.Idle);
}
// ── 内部 ──────────────────────────────────────────────────────────
protected Transform _playerTransform;
private void SetPlayerTransform(Transform player) => _playerTransform = player;
protected virtual void OnEnable()
{
_onPlayerSpawned?.Subscribe(SetPlayerTransform).AddTo(_subs);
}
protected virtual void OnDisable()
{
_subs.Clear();
}
protected virtual void OnDestroy() { }
/// <summary>
/// 通知决策层终止(死亡演出 / 出场演出等期间调用,防止决策继续覆盖演出逻辑)。
/// 语义为"通知决策层终止",与 PerformDeath 的 Died 信号幂等(重复 Died 无害)。
/// 供配置型行为组件(如 EnemyDeathSequence)调用,故为 public。
/// </summary>
public void StopBehaviorTree()
{
_brain?.Send(BaseGames.AI.AiSignal.Died);
}
/// <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);
// 通知决策层敌人已死亡
_brain?.Send(BaseGames.AI.AiSignal.Died);
// 死亡时清除所有状态效果
_statusEffects?.Clear();
// 死亡时强制中断所有能力(忽略 interruptOnHurt 等过滤)
_abilities.InterruptAll(InterruptReason.Dead);
// 禁用所有碰撞体
if (_colliders != null)
foreach (var col in _colliders) if (col != null) col.enabled = false;
// 播放死亡动画
if (_animancer != null && _animConfig != null && _animConfig.Dead != null)
{
var state = _animancer.Play(_animConfig.Dead);
if (_pooledObject != null)
state.Events(this).OnEnd = () => _pooledObject.ReturnToPool();
else
state.Events(this).OnEnd = () => Destroy(gameObject);
}
else
{
if (_pooledObject != null)
_pooledObject.ReturnToPoolDelayed(1.5f);
else
Destroy(gameObject, 1.5f);
}
_feedback?.OnDeath();
_onEnemyDied?.Raise(_enemyId);
OnDied?.Invoke();
}
// ── IPoolable ─────────────────────────────────────────────────────
/// <summary>
/// 对象从池中取出时调用,重置运行时状态。
/// 使用对象池时,须在 Prefab 根节点挂载 <see cref="PooledObject"/> 并确保 Awake 已缓存 <see cref="_pooledObject"/>。
/// </summary>
public virtual void OnSpawn()
{
// 恢复碰撞体
if (_colliders != null)
foreach (var col in _colliders) if (col != null) col.enabled = true;
// 重置状态(对象池复用:跳过 Dead 终态守卫,强制恢复到 Controlled)
ForceStateRespawn(EnemyStateType.Controlled);
IsEngaged = false;
// 重置对象池复用相关的运行时感知数据
// 注意:_playerTransform 不重置(场景中玩家仍存在),只重置追踪历史
LastKnownPlayerPosition = transform.position;
_wasParried = false;
_deathSequenceActive = false;
// 重置生命值
if (_stats != null && _statsSO != null)
_stats.Initialize(_statsSO);
// 重置能力冷却
_abilities.InterruptAll(InterruptReason.Dead);
// 重置决策层(回到 Entry、清临时态)
if (_brain != null) _brain.enabled = true; // 复用时恢复决策组件(可能曾被 QuotaManager 裁剪禁用)
_brain?.ResetBrain();
// 通知配置型出生行为组件(如 EnemyAbilityTrigger)执行出生触发逻辑
Spawned?.Invoke();
}
/// <summary>
/// 对象归还到池时调用,清理临时状态。
/// </summary>
public virtual void OnDespawn()
{
_abilities.InterruptAll(InterruptReason.Dead);
_nav?.StopNavigation();
// GO 停用时 EnemyAiBrain.Update 自然停止,无需显式处理。
}
#if UNITY_EDITOR
/// <summary>Set to true during batch editor placement to suppress mid-wiring OnValidate warnings.</summary>
public static bool SuppressValidationWarnings { get; set; }
protected virtual void OnValidate()
{
if (SuppressValidationWarnings) return;
if (_statsSO == null)
Debug.LogWarning($"[EnemyBase] {gameObject.name} 缺少 EnemyStatsSO 配置(运行时会 NullRef)。", this);
if (_stats == null)
Debug.LogWarning($"[EnemyBase] {gameObject.name} 未绑定 EnemyStats 组件引用。", this);
if (_animancer == null)
Debug.LogWarning($"[EnemyBase] {gameObject.name} 未绑定 AnimancerComponent 引用。", this);
}
#endif
private void OnDrawGizmos()
{
#if UNITY_EDITOR
if (_statsSO == null) return;
// 感知范围圆形 Gizmo 由 PhysicsPerceptionSystemEditor [DrawGizmo] 统一绘制,
// 此处不重复绘制,避免叠加覆盖导致 gizmoColor 设置无效。
// ── 运行时:AI 状态标签(常态可见,无需选中)────────────────
if (Application.isPlaying)
{
Color phaseColor = Color.yellow;
UnityEditor.Handles.color = phaseColor;
UnityEditor.Handles.Label(
transform.position + Vector3.up * 1.2f,
$"[{(_brain != null ? _brain.CurrentStateName : "-")}] {_currentState}");
}
#endif
}
private void OnDrawGizmosSelected()
{
#if UNITY_EDITOR
if (_statsSO == null) return;
// 感知范围圆形 Gizmo 由 PhysicsPerceptionSystemEditor [DrawGizmo] 统一绘制,
// 此处不重复绘制。
// 运行时:选中时绘制 AI 状态外圆(突出显示当前决策状态)
if (Application.isPlaying)
{
Color phaseColor = Color.yellow;
Gizmos.color = phaseColor;
Gizmos.DrawWireSphere(transform.position, 0.5f);
}
#endif
}
}
// ── 枚举(架构 07 §1)────────────────────────────────────────────────
public enum EnemyStateType { Controlled, Hurt, Stagger, KnockUp, Dead }
public enum AttackType { Melee, Ranged, Special }
}