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
{
///
/// 敌人基类(架构 07_EnemyModule §1)。
/// 实现 IDamageable,为 Behavior Designer 任务提供统一虚方法接口。
/// 包含:BD 接口、受击、死亡流程。
/// ⚠️ _nav 字段类型为 IPathAgent(在 BaseGames.Enemies.Navigation 中实现具体类)。
/// 实现 IPoolable:配合 PooledObject 支持对象池复用,避免频繁 Destroy/Instantiate。
///
public class EnemyBase : MonoBehaviour, IDamageable, IPoolable
{
[Header("标识")]
[SerializeField] private string _enemyId; // 任务系统 / Boss 进程追踪用,如 "Enemy_SpiderGuard"
public string EnemyId => _enemyId;
/// 死亡时触发(ChallengeRoomManager 波次结算用)。
public event System.Action OnDied;
///
/// 对象池取出并完成 重置后触发。
/// 配置型出生行为组件(如 EnemyAbilityTrigger)订阅此事件,实现零代码出生触发。
///
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("用于身体几何(宽高/边缘/探测起点)的碰撞体。留空 = 使用本物体上的 Collider2D。" +
"移动探测、导航内缩、冲刺终点等全部以此为准,不要在各处自行取碰撞体。")]
[SerializeField] private Collider2D _bodyCollider;
[Header("区域(可选)")]
[Tooltip("地图固定巡逻/追击区域;配置后 BD_ChasePlayer 以区域边界替代 MaxChaseDistance,BD_ReturnToHome 归位至区域中心。留空则沿用出生点 + MaxChaseDistance 旧逻辑。")]
[SerializeField] private EnemyPatrolZone _patrolZone;
[Header("事件频道")]
[SerializeField] private BaseGames.Core.Events.StringEventChannelSO _onEnemyDied;
///
/// 玩家生成事件频道(由 PlayerController.Start() 广播)。
/// 配置后替代 FindWithTag,避免 N 个敌人同帧全场景标签扫描。
///
[SerializeField] private BaseGames.Core.Events.TransformEventChannelSO _onPlayerSpawned;
// ── 导航代理(IPathAgent;由 EnemyNavAgent 实现)───────────────────
// 通过接口引用,避免对 Navigation 程序集的直接依赖。
// 由子类 / Inspector 注入,或者运行时 GetComponent() 获取。
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;
// 身体几何权威(懒创建;OnValidate 改引用后置空重建)
private EnemyBody _body;
// ── 对象池支持 ─────────────────────────────────────────────────────
///
/// 本 GameObject 上的 PooledObject 组件(可选)。
/// Prefab 挂有此组件时,Die() 使用归还池取代 Destroy,实现对象池复用。
///
private PooledObject _pooledObject;
// ── 配置型行为模块(零代码)─────────────────────────────────────────
// 可选挂载的通用行为组件,替代过去的每敌人专属子类。Awake 时收集一次。
private Behaviors.IEnemyDeathSequence _deathSequence;
private readonly System.Collections.Generic.List _spawnHandlers
= new System.Collections.Generic.List(2);
// 死亡前摇演出进行中:纳入 IsInvincible,并阻止重复触发 Die。
private bool _deathSequenceActive;
// ── 状态 ──────────────────────────────────────────────────────────
private EnemyStateType _currentState;
public EnemyStateType CurrentState => _currentState;
// ── 导航语义(归位 / 搜查)────────────────────────────────────────────
/// Awake/Start 时记录的初始世界坐标,供 BD_ReturnToHome 归位使用。
public Vector2 HomePosition { get; private set; }
///
/// 玩家最后一次可见的世界坐标。
/// 由 BD_ChasePlayer 在持有视线时每帧更新;视线丢失后保留最后记录值供 BD_InvestigateLastKnown 使用。
///
public Vector2 LastKnownPlayerPosition { get; set; }
///
/// 地图固定巡逻/追击区域(可选)。
/// 配置后 BD_ChasePlayer 以区域边界为追击上限;BD_ReturnToHome 归位至区域中心。
/// 未配置时退回旧逻辑(HomePosition + MaxChaseDistance)。
///
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 _stateObjs
= new System.Collections.Generic.Dictionary();
// ── 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);
}
///
/// 受击后钩子(已确认未死亡时调用)。子类可重写以触发额外逻辑(如资源积累)。
///
protected virtual void OnDamageTaken(DamageInfo info) { }
///
/// 击飞来袭方向(由 TakeDamage 写入,供 EnemyKnockUpState.Enter() 读取)。
///
internal Vector2 PendingLaunchDir => _pendingLaunchDir;
private Vector2 _pendingLaunchDir;
///
/// 协程兜底:在无对应 Animancer 动画时按时长自动恢复到 Controlled 状态。
/// 仅在 AnimConfig 对应 Clip 为 null 时由状态类调用。
///
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(true));
public EnemyMovement Movement => _movement;
public EnemyStats Stats => _stats;
/// 敌人配置 SO,供 BD Task / 状态对象读取配置数据(如 knockUpDuration)。
public EnemyStatsSO StatsSO => _statsSO;
public AnimancerComponent Animancer => _animancer;
public EnemyAnimationConfigSO AnimConfig => _animConfig;
/// 能力注册表(架构 §8.3)。Awake 时自动收集所有 EnemyAbilityBase 组件。
public EnemyAbilityRegistry Abilities => _abilities;
private readonly EnemyAbilityRegistry _abilities = new EnemyAbilityRegistry();
/// 攻击选择器(从已注册能力里筛出 category==Attack 的候选,Awake 时构建)。
public Abilities.EnemyAttackSelector AttackSelector => _attackSelector;
private Abilities.EnemyAttackSelector _attackSelector;
/// 身体碰撞体几何的唯一权威查询口。移动/导航/能力一律经此获取身体尺寸。
public IEnemyBody Body => _body ??= new EnemyBody(gameObject, _bodyCollider);
/// 由 _onPlayerSpawned 事件缓存的玩家 Transform,供 BD 任务读取。
public Transform PlayerTransform => _playerTransform;
/// 感知 Hub;供 BD 任务及 QuotaManager 暂停/恢复感知使用。
public Perception.IPerceptionSystem SensorHub => _sensorHub;
private Perception.IPerceptionSystem _sensorHub;
/// 威胁评估器(可选):为原始 LOS 结果叠加反应延迟,使感知更自然。
public Perception.EnemyThreatAssessor ThreatAssessor => _threatAssessor;
private Perception.EnemyThreatAssessor _threatAssessor;
/// 状态效果管理器(冻结、灼烧、睡眠等)。
public StatusEffects.EnemyStatusEffectManager StatusEffects => _statusEffects;
private StatusEffects.EnemyStatusEffectManager _statusEffects;
/// 决策层组件(BrainGraph)。供 EnemyQuotaManager 做活跃数量 LOD 裁剪等使用。
public EnemyAiBrain Brain => _brain;
private EnemyAiBrain _brain;
/// 是否处于交战(追击/攻击)中——供群体警戒判断"已交战不降级"。由 Chase 能力置位。
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)
=> MoveInDirectionWithSpeed(dir, speed, false);
///
/// 显式速度移动; = true 时允许冲出崖沿并自由落体
/// (committed 冲锋用,避免被移动层的悬崖夹紧停在边缘)。
/// 该许可随移动意图存续,StopMovement() 时自动复位。
///
public virtual void MoveInDirectionWithSpeed(float dir, float speed, bool allowLedgeCross)
{
if (_movement == null) return;
_movement.PendingInput.MoveDir = dir;
_movement.PendingInput.MoveSpeed = speed;
_movement.PendingInput.AllowLedgeCross = allowLedgeCross;
_movement.PendingInput.WantStop = false; // 移动意图覆盖停止脉冲
}
public virtual void StopMovement()
{
_nav?.StopNavigation();
if (_movement != null) _movement.PendingInput.WantStop = true;
}
/// 施加状态效果(需要 EnemyStatusEffectManager 组件)。同类型效果自动刷新。
public void ApplyStatusEffect(StatusEffects.IStatusEffect effect)
=> _statusEffects?.Apply(effect);
/// 移除指定类型的状态效果(若存在)。
public void RemoveStatusEffect(StatusEffects.StatusEffectType type)
=> _statusEffects?.Remove(type);
/// 查询指定类型状态效果是否激活。
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;
/// 视线检测结果(由 PhysicsPerceptionSystem 的 LOS / Sight slot 提供)。
public bool HasLineOfSight =>
_sensorHub != null &&
(_sensorHub.HasAnyDetection(Perception.SensorSlotNames.LOS) ||
_sensorHub.HasAnyDetection(Perception.SensorSlotNames.Sight));
public virtual bool IsPlayerVisible()
=> _threatAssessor != null ? _threatAssessor.IsThreatDetected : HasLineOfSight;
/// 追逐感知:玩家是否在"追逐触发圆"(aggro 槽)内 → 触发追击。感知系统只回答"在不在",状态转换归 AI。
public bool InChaseZone()
=> _sensorHub != null && _playerTransform != null &&
_sensorHub.IsDetecting(Perception.SensorSlotNames.Aggro, _playerTransform.gameObject);
///
/// 视野感知:玩家是否在视野(los/sight 槽)内。用于警觉升级与追击维持;
/// 无视野槽时回退用追逐感知(aggro)。感知系统只回答"在不在",状态转换归 AI。
///
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;
}
/// 朝向世界坐标点(通过输入信号,下一 FixedUpdate 消费)。
public void FaceTarget(Vector2 worldPos)
{
if (_movement == null) return;
_movement.PendingInput.WantFace = true;
_movement.PendingInput.FaceTargetPos = worldPos;
_movement.PendingInput.FaceDir = 0;
}
/// 直接指定朝向方向(+1 右 / -1 左,通过输入信号)。
public void FaceDirection(int dir)
{
if (_movement == null) return;
_movement.PendingInput.WantFace = true;
_movement.PendingInput.FaceDir = dir;
}
///
/// 搜查"环顾"子步骤:停止移动,播放原地环顾动画。
/// 由搜查行为触发;动画细节由角色自己决定,外部无需感知 AnimConfig。
///
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);
///
/// 调整决策 Tick 频率(非警觉=降频,警觉=高频)。
/// 由外部(警戒/状态效果)调用(架构 07_EnemyModule §13.5)。
///
public virtual void SetAggroTickRate(bool isAggro)
{
// LOD tick 速率控制迁移到后续 AiScheduler 阶段
}
// ── 弹反(Parry)响应 ──────────────────────────────────────────────
private bool _wasParried;
private float _parryTimestamp;
// BD 树因阶段切换/死亡未能消费弹反事件时,超过此时长自动过期
private const float ParryEventTTL = 2f;
///
/// 消费弹反事件标志(读取并清除)。
/// BD_OnParried Conditional Task 在每次 Tick 时调用此方法。
/// 若 BD 树因阶段切换或死亡未能及时消费,超过 TTL 后自动过期返回 false。
///
public bool ConsumeParryEvent()
{
if (!_wasParried) return false;
if (Time.time - _parryTimestamp > ParryEventTTL)
{
_wasParried = false;
return false;
}
_wasParried = false;
return true;
}
///
/// 被弹反时调用:强制进入 Stagger 状态并在 秒后恢复。
/// 由近战攻击碰到玩家弹反框时触发(例如 BossParryDetector 或通用 ParryDetector)。
///
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;
/// 是否处于一次性招式动画锁定中(步态不应覆盖)。
public bool IsAnimLocked => Time.time < _animLockUntil;
/// 锁定动画 秒,期间步态动画不覆盖(供能力播放起手/收招等一次性动作)。
public void LockAnim(float duration) => _animLockUntil = Time.time + Mathf.Max(0f, duration);
///
/// 按当前移动模式播放步态动画(由 EnemyLocomotion 在模式变化时调用)。
/// Approach(追击)不在此驱动——追击动画由能力(Skill_Start/Loop/End)全权驱动,避免抢动画。
/// 招式动画锁定期间()不覆盖。
///
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 调用)────────────────────
///
/// 生成弹幕 / 技能投射物(由动画事件 SpawnProjectile 触发)。
/// 基类实现:路由到所有挂载的 组件
/// (如 EnemySpawnerOnEvent),由组件按 payload 自行匹配并生成——实现零代码生成配置。
/// 子类(如 RangedEnemy / ChaoFengBoss)可重写以自定义发射逻辑。
///
public virtual void SpawnProjectile(string payload)
{
for (int i = 0; i < _spawnHandlers.Count; i++)
_spawnHandlers[i]?.HandleSpawn(payload);
}
/// 切换二阶段形态(Boss 等特殊敌人重写此方法)。
public virtual void TriggerPhaseTwo() { }
/// 动画播放完毕回调(用于单次动画后返回 Idle 等逻辑)。
public virtual void OnAnimationComplete(string payload) { }
/// 设置嘶吼状态(影响 Blackboard / 状态机行为)。
public virtual void SetRoaring(bool isRoaring) { }
// 防止状态 Enter/Exit 内部再次调用 ForceState 造成无限递归
private bool _isStateTransitioning;
// ── 状态控制 ──────────────────────────────────────────────────────
///
/// 强制切换物理/战斗状态。
/// ⚠️ Dead 是终态:进入后不允许外部再转换到其他状态。
/// 对象池复用时请通过 重置,该方法调用 。
///
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;
}
///
/// 对象池复用重置专用(跳过 Dead 终态守卫)。
/// 仅由 调用,不对外暴露。
///
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() ?? new NullPathAgent();
_locomotion = GetComponentInChildren(true);
if (_locomotion == null)
Debug.LogError($"EnemyBase 未找到 EnemyLocomotion 组件:{name}", this);
if (_movement == null) _movement = GetComponent();
_poiseSource = GetComponent();
_sensorHub = GetComponentInChildren();
_statusEffects = GetComponent();
_threatAssessor = GetComponent();
_pooledObject = GetComponent();
_brain = GetComponent();
_abilities.CollectFrom(gameObject);
BuildAttackSelector();
_colliders = GetComponentsInChildren(true);
// 身体几何权威校验:解析不到碰撞体 → 显式报错(根因暴露,不静默兜底)
if (Body.Collider == null)
Debug.LogError($"[EnemyBase] {name} 找不到身体碰撞体:请在 Inspector 指定 _bodyCollider," +
"或确保本物体上挂有 Collider2D。移动探测/导航内缩将不可用。", this);
// 收集配置型行为模块(零代码扩展点)
_deathSequence = GetComponentInChildren(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 中处理
}
///
/// 从已注册能力里收集 category==Attack 的候选,构建攻击选择器。
/// 根因校验:攻击招 rangeRadius<=0(永远够不着)显式报错,不静默兜底。
///
private void BuildAttackSelector()
{
var candidates = new System.Collections.Generic.List();
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 != BaseGames.Enemies.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() { }
///
/// 通知决策层终止(死亡演出 / 出场演出等期间调用,防止决策继续覆盖演出逻辑)。
/// 语义为"通知决策层终止",与 PerformDeath 的 Died 信号幂等(重复 Died 无害)。
/// 供配置型行为组件(如 EnemyDeathSequence)调用,故为 public。
///
public void StopBehaviorTree()
{
_brain?.Send(BaseGames.AI.AiSignal.Died);
}
///
/// 死亡入口。若挂载了 死亡演出组件,
/// 则先委托其播放无敌前摇(期间 为 true),演出结束后回调
/// 执行真正的死亡清理;否则直接清理。
/// 子类(如 BossBase)重写时仍调用 base.Die() 即可获得此委托行为。
///
protected virtual void Die()
{
if (_currentState == EnemyStateType.Dead || _deathSequenceActive) return;
if (_deathSequence != null)
{
_deathSequenceActive = true; // 演出期间纳入 IsInvincible,阻止重复 Die
_deathSequence.Play(PerformDeath);
return;
}
PerformDeath();
}
///
/// 实际死亡清理:切 Dead 终态、清状态效果、中断能力、关碰撞体、播死亡动画、
/// 归还对象池 / 销毁、广播死亡事件。由 直接调用,
/// 或由死亡演出组件在前摇结束后回调。
///
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 ─────────────────────────────────────────────────────
///
/// 对象从池中取出时调用,重置运行时状态。
/// 使用对象池时,须在 Prefab 根节点挂载 并确保 Awake 已缓存 。
///
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();
}
///
/// 对象归还到池时调用,清理临时状态。
///
public virtual void OnDespawn()
{
_abilities.InterruptAll(InterruptReason.Dead);
_nav?.StopNavigation();
// GO 停用时 EnemyAiBrain.Update 自然停止,无需显式处理。
}
#if UNITY_EDITOR
/// Set to true during batch editor placement to suppress mid-wiring OnValidate warnings.
public static bool SuppressValidationWarnings { get; set; }
protected virtual void OnValidate()
{
_body = null; // 身体碰撞体引用可能在 Inspector 被改,缓存无条件失效(与警告抑制无关)
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);
if (_bodyCollider == null && GetComponent() == null)
Debug.LogWarning($"[EnemyBase] {gameObject.name} 找不到身体碰撞体(未指定 _bodyCollider 且本物体无 Collider2D)。", 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 }
}