using UnityEngine; using Animancer; using BaseGames.Combat; using BaseGames.Core.Events; using BaseGames.Core.Pool; using BaseGames.Enemies.States; using BaseGames.Enemies.Abilities; #if GRAPH_DESIGNER using Opsive.BehaviorDesigner.Runtime; #endif namespace BaseGames.Enemies { /// /// 敌人基类(架构 07_EnemyModule §1)。 /// 实现 IDamageable,为 Behavior Designer 任务提供统一虚方法接口。 /// 包含:BD 接口、受击、死亡流程。 /// ⚠️ _nav 字段类型为 IPathAgent(在 BaseGames.Enemies.Navigation 中实现具体类)。 /// 实现 IPoolable:配合 PooledObject 支持对象池复用,避免频繁 Destroy/Instantiate。 /// public class EnemyBase : MonoBehaviour, IDamageable, ILOSRequester, IPoolable { [Header("标识")] [SerializeField] private string _enemyId; // 任务系统 / Boss 进程追踪用,如 "Enemy_SpiderGuard" public string EnemyId => _enemyId; /// 死亡时触发(ChallengeRoomManager 波次结算用)。 public event System.Action OnDied; [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 以区域边界替代 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; #if GRAPH_DESIGNER [Header("BT Tick 分级(LOD)")] [Tooltip("Idle 阶段 BT Tick 最小间隔(s);0 = 每帧全速。")] [SerializeField] private float _btIdleTickInterval = 0.30f; [Tooltip("Patrol/ReturnHome 阶段 BT Tick 间隔(s)")] [SerializeField] private float _btPatrolTickInterval = 0.15f; [Tooltip("Alert/Investigate 阶段 BT Tick 间隔(s)")] [SerializeField] private float _btAlertTickInterval = 0.08f; [Tooltip("Chase 阶段 BT Tick 间隔(s)")] [SerializeField] private float _btChaseTickInterval = 0.05f; [Tooltip("Combat 阶段 BT Tick 间隔(s);0 = 每帧全速")] [SerializeField] private float _btCombatTickInterval = 0f; #endif // ── 导航代理(IPathAgent;由 EnemyNavAgent 实现)─────────────────── // 通过接口引用,避免对 Navigation 程序集的直接依赖。 // 由子类 / Inspector 注入,或者运行时 GetComponent() 获取。 protected IPathAgent _nav; // 霸体来源(由 EnemyPoiseComponent.Awake() 自动注入,TakeDamage 时读取) private IPoiseSource _poiseSource; protected readonly CompositeDisposable _subs = new(); // 碰撞体缓存:Awake 时收集一次,避免 Die()/OnSpawn() 中频繁 GetComponentsInChildren 分配 private Collider2D[] _colliders; // ── 对象池支持 ───────────────────────────────────────────────────── /// /// 本 GameObject 上的 PooledObject 组件(可选)。 /// Prefab 挂有此组件时,Die() 使用归还池取代 Destroy,实现对象池复用。 /// private PooledObject _pooledObject; // ── 状态 ────────────────────────────────────────────────────────── private EnemyStateType _currentState; public EnemyStateType CurrentState => _currentState; // ── AI 行为阶段(独立于物理/战斗状态)──────────────────────────────── private AiPhase _currentAiPhase = AiPhase.Idle; /// 当前 AI 行为阶段(BD 任务读取;ForceState 不会改变此值)。 public AiPhase CurrentAiPhase => _currentAiPhase; /// AI 行为阶段变更时触发,可用于驱动外部系统(音效/视觉效果等)。 public event System.Action OnAiPhaseChanged; // ── 导航语义(归位 / 搜查)──────────────────────────────────────────── /// 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 AiPhase _dbg_AiPhase; [SerializeField] private bool _dbg_HasPlayer; [SerializeField] private Vector2 _dbg_LastKnownPos; #if GRAPH_DESIGNER [SerializeField] private float _dbg_BtTickInterval; #endif #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; 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; } // ── 受击分级(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; 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(); /// 由 _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; #if GRAPH_DESIGNER public BehaviorTree BehaviorTree => _behaviorTree; #endif // ── 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; } /// 施加状态效果(需要 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; /// 原始视线检测结果(BatchLOSSystem 写入,无感知延迟修正)。 public bool HasLineOfSight => _losResult; public virtual bool IsPlayerVisible() => _threatAssessor != null ? _threatAssessor.IsThreatDetected : _losResult; 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); /// /// 调整 BehaviorTree Tick 频率(非警觉=降频,警觉=高频)。 /// 由 BD_SetAlert 调用(架构 07_EnemyModule §13.5)。 /// public virtual void SetAggroTickRate(bool isAggro) { #if GRAPH_DESIGNER _btCurrentInterval = isAggro ? _btAlertTickInterval : _btIdleTickInterval; #endif } // ── 警戒传播(Group Alert)──────────────────────────────────────── private static readonly UnityEngine.Collider2D[] _alertBuffer = new UnityEngine.Collider2D[32]; // ── 弹反(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); } /// /// 通知半径内的其他敌人进入 Alert 阶段(Group Alert 广播)。 /// 配合 BD_BroadcastAlert 在进入 Chase 阶段时调用。 /// public void AlertNearby(float radius) { if (radius <= 0f) return; int count = Physics2D.OverlapCircleNonAlloc(transform.position, radius, _alertBuffer); for (int i = 0; i < count; i++) { if (_alertBuffer[i] == null) continue; var enemy = _alertBuffer[i].GetComponentInParent(); if (enemy == null || enemy == this) continue; enemy.ReceiveAlert(_playerTransform, LastKnownPlayerPosition); } } /// /// 接收来自邻近敌人的警戒广播。 /// 当前处于 Idle / Patrol 时切换到 Alert;Chase / Combat 中不降级。 /// public void ReceiveAlert(Transform sharedPlayerTransform, Vector2 lastKnownPos) { if (!IsAlive) return; if (_currentAiPhase == AiPhase.Chase || _currentAiPhase == AiPhase.Combat) return; // 同步玩家引用(若本敌人尚未感知到玩家) if (sharedPlayerTransform != null && _playerTransform == null) _playerTransform = sharedPlayerTransform; LastKnownPlayerPosition = lastKnownPos; SetAiPhase(AiPhase.Alert); } [Header("AI 行为阶段")] [Tooltip("SetAiPhase 变更时是否自动播放 AnimConfig 中对应的阶段动画(如 Alert/Investigate)")] [SerializeField] private bool _autoPlayPhaseAnimation = true; /// /// 设置 AI 行为阶段,并广播 事件。 /// 若 为 true,自动播放 AnimConfig 对应动画。 /// 由 BD Task(BD_SetAiPhase / BD_ChasePlayer 等)调用; /// 不触发受击/死亡等 EnemyStateType 流程。 /// public void SetAiPhase(AiPhase phase) { if (_currentAiPhase == phase) return; _currentAiPhase = phase; OnAiPhaseChanged?.Invoke(phase); #if GRAPH_DESIGNER // 按行为阶段动态调整 BT Tick 频率(5 档 LOD) _btCurrentInterval = phase switch { AiPhase.Patrol => _btPatrolTickInterval, AiPhase.Alert => _btAlertTickInterval, AiPhase.Investigate => _btAlertTickInterval, AiPhase.Chase => _btChaseTickInterval, AiPhase.Combat => _btCombatTickInterval, AiPhase.ReturnHome => _btPatrolTickInterval, _ => _btIdleTickInterval, // Idle + 未来新阶段兜底 }; #endif if (_autoPlayPhaseAnimation && _animancer != null && _animConfig != null) { var clip = phase switch { AiPhase.Alert => _animConfig.Alert, AiPhase.Investigate => _animConfig.Investigate ?? _animConfig.Walk, AiPhase.Patrol => _animConfig.Walk, AiPhase.ReturnHome => _animConfig.Walk, AiPhase.Chase => _animConfig.Run, AiPhase.Idle => _animConfig.Idle, _ => null, }; if (clip != null) _animancer.Play(clip); } } // ── 动画事件钩子(由 EnemyAnimationEvents 调用)──────────────────── /// 生成弹幕 / 技能投射物。payload 为配置 Id,由子类查表实现。 public virtual void SpawnProjectile(string 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(); if (_movement == null) _movement = GetComponent(); _poiseSource = GetComponent(); _sensorHub = GetComponentInChildren(); _statusEffects = GetComponent(); _threatAssessor = GetComponent(); _pooledObject = GetComponent(); _abilities.CollectFrom(gameObject); _colliders = GetComponentsInChildren(true); 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 中处理 #if GRAPH_DESIGNER _behaviorTree = GetComponent(); if (_behaviorTree != null) { _behaviorTree.StartWhenEnabled = false; _behaviorTree.PauseWhenDisabled = true; // Manual 模式:由 EnemyBase.Update 按 AiPhase 分级节流 Tick。 _behaviorTree.UpdateMode = Opsive.BehaviorDesigner.Runtime.Components.UpdateMode.Manual; _btManualMode = true; _btCurrentInterval = _btIdleTickInterval; _behaviorTree.StartBehavior(); } #endif } 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 GRAPH_DESIGNER // BT Tick LOD:按当前 AiPhase 分级节流,降低空闲状态的 BT 开销。 if (_btManualMode && _behaviorTree != null) { float interval = _btCurrentInterval; if (interval <= 0f) { _behaviorTree.Tick(); } else { _btTickTimer += Time.deltaTime; if (_btTickTimer >= interval) { _btTickTimer -= interval; _behaviorTree.Tick(); } } } #endif #if UNITY_EDITOR _dbg_CurrentState = _currentState; _dbg_AiPhase = _currentAiPhase; _dbg_HasPlayer = _playerTransform != null; _dbg_LastKnownPos = LastKnownPlayerPosition; #if GRAPH_DESIGNER _dbg_BtTickInterval = _btCurrentInterval; #endif #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); Core.ServiceLocator.GetOrDefault()?.Register(this); } protected virtual void OnDisable() { Core.ServiceLocator.GetOrDefault()?.Unregister(this); _subs.Clear(); } protected virtual void OnDestroy() { } // LOS 缓存(BatchLOSSystem 写入;降级时由 3 帧节流 Raycast 写入) private bool _losResult; // ── ILOSRequester ────────────────────────────────────────────────── public Vector2 LOSOrigin => (Vector2)transform.position + _statsSO.EyeOffset; public Vector2 LOSTarget => _playerTransform != null ? (Vector2)_playerTransform.position : (Vector2)transform.position; public LayerMask LOSBlockingMask => _statsSO.LOSBlockingMask; public void ReceiveLOSResult(bool hasLineOfSight) { _losResult = hasLineOfSight; } // BehaviorTree 引用(#if GRAPH_DESIGNER 保护,避免空引用) #if GRAPH_DESIGNER private BehaviorTree _behaviorTree; private bool _btManualMode; private float _btTickTimer; private float _btCurrentInterval; #endif /// /// 停止行为树(子类 Die() 预演出阶段可调用,防止 BT 继续 Tick 覆盖演出逻辑)。 /// 内部使用 #if GRAPH_DESIGNER 保护,子类无需处理条件编译。 /// protected void StopBehaviorTree() { #if GRAPH_DESIGNER _behaviorTree?.StopBehavior(); #endif } protected virtual void Die() { if (_currentState == EnemyStateType.Dead) return; ForceState(EnemyStateType.Dead); // 死亡时清除所有状态效果 _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); _currentAiPhase = AiPhase.Idle; // 重置对象池复用相关的运行时感知数据 // 注意:_playerTransform 不重置(场景中玩家仍存在),只重置追踪历史 LastKnownPlayerPosition = transform.position; _wasParried = false; // 重置生命值 if (_stats != null && _statsSO != null) _stats.Initialize(_statsSO); // 重置能力冷却 _abilities.InterruptAll(InterruptReason.Dead); #if GRAPH_DESIGNER _behaviorTree?.StartBehavior(); #endif } /// /// 对象归还到池时调用,清理临时状态,停止 BT。 /// public virtual void OnDespawn() { _abilities.InterruptAll(InterruptReason.Dead); _nav?.StopNavigation(); #if GRAPH_DESIGNER if (_behaviorTree != null) _behaviorTree.enabled = false; #endif } #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() { 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 = _currentAiPhase switch { AiPhase.Idle => Color.gray, AiPhase.Patrol => Color.green, AiPhase.Alert => Color.yellow, AiPhase.Chase => new Color(1f, 0.5f, 0f), AiPhase.Combat => Color.red, AiPhase.Investigate => Color.cyan, AiPhase.ReturnHome => Color.blue, _ => Color.white, }; UnityEditor.Handles.color = phaseColor; UnityEditor.Handles.Label( transform.position + Vector3.up * 1.2f, $"[{_currentAiPhase}] {_currentState}"); } // ── 运行时:LOS 连线 ──────────────────────────────────────── if (!Application.isPlaying || _playerTransform == null) return; float drawDetectRange = _sensorHub != null ? _sensorHub.GetSensorRadius(Perception.SensorSlotNames.Aggro) : -1f; Vector3 eyeWorld = transform.position + new Vector3(_statsSO.EyeOffset.x, _statsSO.EyeOffset.y, 0f); Vector3 playerPos = _playerTransform.position; float sqrDist = (playerPos - transform.position).sqrMagnitude; bool inRange = drawDetectRange >= 0f && sqrDist <= drawDetectRange * drawDetectRange; // 眼睛位置小圆点(金黄) Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.85f); Gizmos.DrawWireSphere(eyeWorld, 0.07f); if (inRange || _losResult) { Gizmos.color = _losResult ? new Color(1f, 0.5f, 0f, 0.85f) : new Color(0.6f, 0.6f, 0.6f, 0.25f); Gizmos.DrawLine(eyeWorld, playerPos); } #endif } private void OnDrawGizmosSelected() { #if UNITY_EDITOR if (_statsSO == null) return; // 感知范围圆形 Gizmo 由 PhysicsPerceptionSystemEditor [DrawGizmo] 统一绘制, // 此处不重复绘制。 // 运行时:选中时绘制 AiPhase 彩色外圆(突出显示当前状态) if (Application.isPlaying) { Color phaseColor = _currentAiPhase switch { AiPhase.Idle => Color.gray, AiPhase.Patrol => Color.green, AiPhase.Alert => Color.yellow, AiPhase.Chase => new Color(1f, 0.5f, 0f), AiPhase.Combat => Color.red, AiPhase.Investigate => Color.cyan, AiPhase.ReturnHome => Color.blue, _ => Color.white, }; 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 } }