877 lines
40 KiB
C#
877 lines
40 KiB
C#
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("区域(可选)")]
|
||
[Tooltip("地图固定巡逻/追击区域;配置后 BD_ChasePlayer 以区域边界替代 MaxChaseDistance,BD_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;
|
||
|
||
#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<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; }
|
||
|
||
/// <summary>
|
||
/// 地图固定巡逻/追击区域(可选)。
|
||
/// 配置后 BD_ChasePlayer 以区域边界为追击上限;BD_ReturnToHome 归位至区域中心。
|
||
/// 未配置时退回旧逻辑(HomePosition + MaxChaseDistance)。
|
||
/// </summary>
|
||
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<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>感知 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;
|
||
#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>原始视线检测结果(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>朝向世界坐标点(通过输入信号,下一 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>
|
||
/// 调整 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 时切换到 Alert;Chase / 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 Task(BD_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.IPerceptionSystem>();
|
||
_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);
|
||
Core.ServiceLocator.GetOrDefault<AI.BatchLOSSystem>()?.Register(this);
|
||
}
|
||
|
||
protected virtual void OnDisable()
|
||
{
|
||
Core.ServiceLocator.GetOrDefault<AI.BatchLOSSystem>()?.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
|
||
|
||
/// <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;
|
||
|
||
// 感知范围圆形 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 }
|
||
}
|