Files
zeling_v2/Assets/_Game/Scripts/Enemies/EnemyBase.cs
Joywayer bcd8b0e90b feat: Update enemy AI and movement systems
- Enhanced Physics2D layer collision report with new interactions between Player and Enemy layers.
- Refactored BD_InvestigateLastKnown to streamline animation handling and improve readability.
- Simplified BD_MaintainCombatDistance by consolidating movement stop logic.
- Updated BD_MoveToPlayer to set AI phase on start.
- Improved BD_Patrol logic with better handling of stuck states and path failures.
- Enhanced BD_PatrolWaypoints to manage stuck conditions and retry logic more effectively.
- Refined BD_ReturnToHome to remove unnecessary animation calls.
- Updated BD_WalkRandom to ensure AI phase is set correctly on start.
- Improved EnemyAbilityBase to delegate target facing to the movement system.
- Enhanced EnemyBase with new movement methods for better control.
- Refactored EnemyMovement to introduce a new input system for handling movement and facing.
- Added EnemyMoveInput struct to encapsulate movement intentions.
- Updated Physics2DSettings to reflect new layer collision matrix.
- Introduced RTK CLI instructions for optimized command usage.
2026-05-29 17:01:59 +08:00

914 lines
42 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.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
{
/// <summary>
/// 敌人基类(架构 07_EnemyModule §1
/// 实现 IDamageable为 Behavior Designer 任务提供统一虚方法接口。
/// 包含BD 接口、受击、死亡流程。
/// ⚠️ _nav 字段类型为 IPathAgent在 BaseGames.Enemies.Navigation 中实现具体类)。
/// 实现 IPoolable配合 PooledObject 支持对象池复用,避免频繁 Destroy/Instantiate。
/// </summary>
public class EnemyBase : MonoBehaviour, IDamageable, ILOSRequester, IPoolable
{
[Header("标识")]
[SerializeField] private string _enemyId; // 任务系统 / Boss 进程追踪用,如 "Enemy_SpiderGuard"
public string EnemyId => _enemyId;
/// <summary>死亡时触发ChallengeRoomManager 波次结算用)。</summary>
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("事件频道")]
[SerializeField] private BaseGames.Core.Events.StringEventChannelSO _onEnemyDied;
/// <summary>
/// 玩家生成事件频道(由 PlayerController.Start() 广播)。
/// 配置后替代 FindWithTag避免 N 个敌人同帧全场景标签扫描。
/// </summary>
[SerializeField] private BaseGames.Core.Events.TransformEventChannelSO _onPlayerSpawned;
#if GRAPH_DESIGNER
[Header("BT Tick 分级LOD")]
[Tooltip("Idle 阶段 BT Tick 最小间隔s0 = 每帧全速。")]
[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 间隔s0 = 每帧全速")]
[SerializeField] private float _btCombatTickInterval = 0f;
#endif
// ── 导航代理IPathAgent由 EnemyNavAgent 实现)───────────────────
// 通过接口引用,避免对 Navigation 程序集的直接依赖。
// 由子类 / Inspector 注入,或者运行时 GetComponent<IPathAgent>() 获取。
protected IPathAgent _nav;
// 霸体来源(由 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;
// ── 状态 ──────────────────────────────────────────────────────────
private EnemyStateType _currentState;
public EnemyStateType CurrentState => _currentState;
// ── AI 行为阶段(独立于物理/战斗状态)────────────────────────────────
private AiPhase _currentAiPhase = AiPhase.Idle;
/// <summary>当前 AI 行为阶段BD 任务读取ForceState 不会改变此值)。</summary>
public AiPhase CurrentAiPhase => _currentAiPhase;
/// <summary>AI 行为阶段变更时触发,可用于驱动外部系统(音效/视觉效果等)。</summary>
public event System.Action<AiPhase> OnAiPhaseChanged;
// ── 导航语义(归位 / 搜查)────────────────────────────────────────────
/// <summary>Awake/Start 时记录的初始世界坐标,供 BD_ReturnToHome 归位使用。</summary>
public Vector2 HomePosition { get; private set; }
/// <summary>
/// 玩家最后一次可见的世界坐标。
/// 由 BD_ChasePlayer 在持有视线时每帧更新;视线丢失后保留最后记录值供 BD_InvestigateLastKnown 使用。
/// </summary>
public Vector2 LastKnownPlayerPosition { get; set; }
#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<EnemyStateType, IEnemyState> _stateObjs
= new System.Collections.Generic.Dictionary<EnemyStateType, IEnemyState>();
// ── 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);
}
/// <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;
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>由 _onPlayerSpawned 事件缓存的玩家 Transform供 BD 任务读取。</summary>
public Transform PlayerTransform => _playerTransform;
/// <summary>感知 HubSensorToolkit供 QuotaManager 暂停/恢复 Sensor 使用。</summary>
public Perception.IPerceptionSystem SensorHub => _sensorHub;
private Perception.EnemySensorHub _sensorHub;
/// <summary>威胁评估器(可选):为原始 LOS 结果叠加反应延迟,使感知更自然。</summary>
public Perception.EnemyThreatAssessor ThreatAssessor => _threatAssessor;
private Perception.EnemyThreatAssessor _threatAssessor;
/// <summary>状态效果管理器(冻结、灼烧、睡眠等)。</summary>
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;
}
/// <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>
/// 检查玩家是否在感知范围内。若 <see cref="EnemyStatsSO.DetectAngleDeg"/> > 0
/// 同时验证玩家是否在自身朝向的扇形角度内。
/// </summary>
public virtual bool IsPlayerInDetectRange()
{
if (_stats == null || _playerTransform == null) return false;
float detectRange = _statsSO != null ? _statsSO.DetectRange : 6f;
if (_stats.SqrDistanceToPlayer > detectRange * detectRange) return false;
float angleDeg = _statsSO?.DetectAngleDeg ?? 0f;
if (angleDeg <= 0f) return true; // 0 = 关闭方向限制
Vector2 toPlayer = ((Vector2)_playerTransform.position - (Vector2)transform.position).normalized;
float facingDir = _movement != null ? _movement.FacingDirection : 1f;
var forward = new Vector2(facingDir, 0f);
float angle = Vector2.Angle(forward, toPlayer);
return angle <= angleDeg;
}
/// <summary>原始视线检测结果BatchLOSSystem 写入,无感知延迟修正)。</summary>
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;
}
/// <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>
/// 调整 BehaviorTree Tick 频率(非警觉=降频,警觉=高频)。
/// 由 BD_SetAlert 调用(架构 07_EnemyModule §13.5)。
/// </summary>
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;
/// <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);
}
/// <summary>
/// 通知半径内的其他敌人进入 Alert 阶段Group Alert 广播)。
/// 配合 BD_BroadcastAlert 在进入 Chase 阶段时调用。
/// </summary>
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<EnemyBase>();
if (enemy == null || enemy == this) continue;
enemy.ReceiveAlert(_playerTransform, LastKnownPlayerPosition);
}
}
/// <summary>
/// 接收来自邻近敌人的警戒广播。
/// 当前处于 Idle / Patrol 时切换到 AlertChase / Combat 中不降级。
/// </summary>
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;
/// <summary>
/// 设置 AI 行为阶段,并广播 <see cref="OnAiPhaseChanged"/> 事件。
/// 若 <see cref="_autoPlayPhaseAnimation"/> 为 true自动播放 AnimConfig 对应动画。
/// 由 BD TaskBD_SetAiPhase / BD_ChasePlayer 等)调用;
/// 不触发受击/死亡等 EnemyStateType 流程。
/// </summary>
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 调用)────────────────────
/// <summary>生成弹幕 / 技能投射物。payload 为配置 Id由子类查表实现。</summary>
public virtual void SpawnProjectile(string 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();
if (_movement == null) _movement = GetComponent<EnemyMovement>();
_poiseSource = GetComponent<IPoiseSource>();
_sensorHub = GetComponentInChildren<Perception.EnemySensorHub>();
_statusEffects = GetComponent<StatusEffects.EnemyStatusEffectManager>();
_threatAssessor = GetComponent<Perception.EnemyThreatAssessor>();
_pooledObject = GetComponent<PooledObject>();
_abilities.CollectFrom(gameObject);
_colliders = GetComponentsInChildren<Collider2D>(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<BehaviorTree>();
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);
}
protected virtual void OnDisable()
{
_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
/// <summary>
/// 停止行为树(子类 Die() 预演出阶段可调用,防止 BT 继续 Tick 覆盖演出逻辑)。
/// 内部使用 #if GRAPH_DESIGNER 保护,子类无需处理条件编译。
/// </summary>
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 ─────────────────────────────────────────────────────
/// <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);
_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
}
/// <summary>
/// 对象归还到池时调用,清理临时状态,停止 BT。
/// </summary>
public virtual void OnDespawn()
{
_abilities.InterruptAll(InterruptReason.Dead);
_nav?.StopNavigation();
#if GRAPH_DESIGNER
if (_behaviorTree != null) _behaviorTree.enabled = false;
#endif
}
#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;
// ── 侦测范围(淡橙;若配置扇形角则绘制扇形弧)+ 攻击范围(淡红圆)────
{
var c = new Vector3(transform.position.x, transform.position.y, 0f);
var prevM = UnityEditor.Handles.matrix;
UnityEditor.Handles.matrix = Matrix4x4.identity;
// 攻击范围(全圆)
UnityEditor.Handles.color = new Color(1f, 0.15f, 0.15f, 0.15f);
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.AttackRange);
UnityEditor.Handles.color = new Color(1f, 0.15f, 0.15f, 0.55f);
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.AttackRange);
// 侦测范围
float angleDeg = _statsSO.DetectAngleDeg;
if (angleDeg > 0f)
{
// 扇形感知:绘制弧形扇区
float facing = Application.isPlaying && _movement != null ? _movement.FacingDirection : 1f;
Vector3 forward3 = new Vector3(facing, 0f, 0f);
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.12f);
UnityEditor.Handles.DrawSolidArc(c, Vector3.back, forward3, angleDeg, _statsSO.DetectRange);
UnityEditor.Handles.DrawSolidArc(c, Vector3.back, forward3, -angleDeg, _statsSO.DetectRange);
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.6f);
UnityEditor.Handles.DrawWireArc(c, Vector3.back, forward3, angleDeg, _statsSO.DetectRange);
UnityEditor.Handles.DrawWireArc(c, Vector3.back, forward3, -angleDeg, _statsSO.DetectRange);
}
else
{
// 全圆感知
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.15f);
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.DetectRange);
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.55f);
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.DetectRange);
}
UnityEditor.Handles.matrix = prevM;
}
// ── 运行时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;
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 = sqrDist <= _statsSO.DetectRange * _statsSO.DetectRange;
// 眼睛位置小圆点(金黄)
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;
// 选中时加亮范围圆
{
var c = new Vector3(transform.position.x, transform.position.y, 0f);
var prevM = UnityEditor.Handles.matrix;
UnityEditor.Handles.matrix = Matrix4x4.identity;
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.25f);
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.DetectRange);
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.90f);
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.DetectRange);
UnityEditor.Handles.color = new Color(1f, 0.2f, 0.2f, 0.25f);
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.AttackRange);
UnityEditor.Handles.color = new Color(1f, 0.2f, 0.2f, 0.90f);
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.AttackRange);
UnityEditor.Handles.matrix = prevM;
}
// 运行时:选中时绘制 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 }
}